From 6981148727706a112af095a10a9317a47e5284f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=AD=86?= <2685475410@qq.com> Date: Fri, 28 Aug 2026 15:22:22 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20chore:=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E7=95=AA=E8=8C=84=E5=B0=8F=E8=AF=B4=20API=20=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增项目基础框架,包含核心 API 实现、算法模块、HTTP 处理器等完整功能模块 --- .gitignore | 46 + README.md | 350 ++++++++ cmd/fanqie/main.go | 182 ++++ config/config.yaml | 82 ++ emoji/data.json | 55 ++ emoji/images/emoji_dr_1.png | Bin 0 -> 5009 bytes emoji/images/emoji_dr_10.png | Bin 0 -> 5252 bytes emoji/images/emoji_dr_11.png | Bin 0 -> 4941 bytes emoji/images/emoji_dr_12.png | Bin 0 -> 5079 bytes emoji/images/emoji_dr_13.png | Bin 0 -> 4619 bytes emoji/images/emoji_dr_14.png | Bin 0 -> 4919 bytes emoji/images/emoji_dr_15.png | Bin 0 -> 4869 bytes emoji/images/emoji_dr_16.png | Bin 0 -> 5156 bytes emoji/images/emoji_dr_17.png | Bin 0 -> 5401 bytes emoji/images/emoji_dr_18.png | Bin 0 -> 4860 bytes emoji/images/emoji_dr_19.png | Bin 0 -> 5280 bytes emoji/images/emoji_dr_2.png | Bin 0 -> 5446 bytes emoji/images/emoji_dr_20.png | Bin 0 -> 4974 bytes emoji/images/emoji_dr_21.png | Bin 0 -> 5504 bytes emoji/images/emoji_dr_22.png | Bin 0 -> 4822 bytes emoji/images/emoji_dr_23.png | Bin 0 -> 5095 bytes emoji/images/emoji_dr_24.png | Bin 0 -> 5623 bytes emoji/images/emoji_dr_25.png | Bin 0 -> 4771 bytes emoji/images/emoji_dr_26.png | Bin 0 -> 5057 bytes emoji/images/emoji_dr_27.png | Bin 0 -> 4927 bytes emoji/images/emoji_dr_28.png | Bin 0 -> 5010 bytes emoji/images/emoji_dr_29.png | Bin 0 -> 5366 bytes emoji/images/emoji_dr_3.png | Bin 0 -> 4752 bytes emoji/images/emoji_dr_30.png | Bin 0 -> 5369 bytes emoji/images/emoji_dr_31.png | Bin 0 -> 5062 bytes emoji/images/emoji_dr_32.png | Bin 0 -> 3540 bytes emoji/images/emoji_dr_33.png | Bin 0 -> 4003 bytes emoji/images/emoji_dr_34.png | Bin 0 -> 4186 bytes emoji/images/emoji_dr_35.png | Bin 0 -> 4750 bytes emoji/images/emoji_dr_36.png | Bin 0 -> 4595 bytes emoji/images/emoji_dr_37.png | Bin 0 -> 4975 bytes emoji/images/emoji_dr_38.png | Bin 0 -> 5595 bytes emoji/images/emoji_dr_39.png | Bin 0 -> 5499 bytes emoji/images/emoji_dr_4.png | Bin 0 -> 5353 bytes emoji/images/emoji_dr_40.png | Bin 0 -> 5105 bytes emoji/images/emoji_dr_41.png | Bin 0 -> 4263 bytes emoji/images/emoji_dr_42.png | Bin 0 -> 5306 bytes emoji/images/emoji_dr_43.png | Bin 0 -> 5220 bytes emoji/images/emoji_dr_44.png | Bin 0 -> 5423 bytes emoji/images/emoji_dr_45.png | Bin 0 -> 5465 bytes emoji/images/emoji_dr_46.png | Bin 0 -> 4569 bytes emoji/images/emoji_dr_47.png | Bin 0 -> 4113 bytes emoji/images/emoji_dr_48.png | Bin 0 -> 6513 bytes emoji/images/emoji_dr_49.png | Bin 0 -> 4823 bytes emoji/images/emoji_dr_5.png | Bin 0 -> 5519 bytes emoji/images/emoji_dr_6.png | Bin 0 -> 5040 bytes emoji/images/emoji_dr_7.png | Bin 0 -> 4956 bytes emoji/images/emoji_dr_8.png | Bin 0 -> 5179 bytes emoji/images/emoji_dr_9.png | Bin 0 -> 5046 bytes go.mod | 64 ++ go.sum | 246 ++++++ internal/algos/algo8404.go | 174 ++++ internal/algos/argus.go | 177 ++++ internal/algos/bytebuf.go | 53 ++ internal/algos/gorgon.go | 137 +++ internal/algos/ladon.go | 121 +++ internal/algos/pkcs7.go | 78 ++ internal/algos/protobuf.go | 404 +++++++++ internal/algos/signer.go | 219 +++++ internal/algos/simon.go | 85 ++ internal/algos/sm3.go | 154 ++++ internal/algos/switcher.go | 159 ++++ internal/common/http.go | 192 +++++ internal/common/logger.go | 123 +++ internal/common/utils.go | 32 + internal/config/config.go | 234 +++++ internal/config/watcher.go | 120 +++ internal/fqapi/audio.go | 135 +++ internal/fqapi/audio_proxy.go | 113 +++ internal/fqapi/cache_manager.go | 528 ++++++++++++ internal/fqapi/catalog.go | 207 +++++ internal/fqapi/category_front.go | 44 + internal/fqapi/category_landing.go | 56 ++ internal/fqapi/content.go | 279 ++++++ internal/fqapi/content_parser.go | 187 ++++ internal/fqapi/content_utils.go | 265 ++++++ internal/fqapi/detail.go | 55 ++ internal/fqapi/device_config.go | 102 +++ internal/fqapi/device_key_pool.go | 582 +++++++++++++ internal/fqapi/device_register.go | 384 +++++++++ internal/fqapi/dhcry.go | 185 ++++ internal/fqapi/emoji.go | 230 +++++ internal/fqapi/gorgon_utils.go | 117 +++ internal/fqapi/item_summary.go | 45 + internal/fqapi/manga.go | 616 +++++++++++++ internal/fqapi/manga_proxy.go | 72 ++ internal/fqapi/media_utils.go | 192 +++++ internal/fqapi/multi_content.go | 273 ++++++ internal/fqapi/multi_detail.go | 215 +++++ internal/fqapi/register_key.go | 249 ++++++ internal/fqapi/review.go | 371 ++++++++ internal/fqapi/search.go | 59 ++ internal/fqapi/ttencrypt.go | 196 +++++ internal/fqapi/video.go | 266 ++++++ internal/handlers/book.go | 90 ++ internal/handlers/category.go | 46 + internal/handlers/common.go | 174 ++++ internal/handlers/content.go | 156 ++++ internal/handlers/manga_proxy.go | 44 + internal/handlers/media.go | 132 +++ internal/handlers/review.go | 1281 ++++++++++++++++++++++++++++ internal/handlers/status.go | 1030 ++++++++++++++++++++++ 107 files changed, 12463 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/fanqie/main.go create mode 100644 config/config.yaml create mode 100644 emoji/data.json create mode 100644 emoji/images/emoji_dr_1.png create mode 100644 emoji/images/emoji_dr_10.png create mode 100644 emoji/images/emoji_dr_11.png create mode 100644 emoji/images/emoji_dr_12.png create mode 100644 emoji/images/emoji_dr_13.png create mode 100644 emoji/images/emoji_dr_14.png create mode 100644 emoji/images/emoji_dr_15.png create mode 100644 emoji/images/emoji_dr_16.png create mode 100644 emoji/images/emoji_dr_17.png create mode 100644 emoji/images/emoji_dr_18.png create mode 100644 emoji/images/emoji_dr_19.png create mode 100644 emoji/images/emoji_dr_2.png create mode 100644 emoji/images/emoji_dr_20.png create mode 100644 emoji/images/emoji_dr_21.png create mode 100644 emoji/images/emoji_dr_22.png create mode 100644 emoji/images/emoji_dr_23.png create mode 100644 emoji/images/emoji_dr_24.png create mode 100644 emoji/images/emoji_dr_25.png create mode 100644 emoji/images/emoji_dr_26.png create mode 100644 emoji/images/emoji_dr_27.png create mode 100644 emoji/images/emoji_dr_28.png create mode 100644 emoji/images/emoji_dr_29.png create mode 100644 emoji/images/emoji_dr_3.png create mode 100644 emoji/images/emoji_dr_30.png create mode 100644 emoji/images/emoji_dr_31.png create mode 100644 emoji/images/emoji_dr_32.png create mode 100644 emoji/images/emoji_dr_33.png create mode 100644 emoji/images/emoji_dr_34.png create mode 100644 emoji/images/emoji_dr_35.png create mode 100644 emoji/images/emoji_dr_36.png create mode 100644 emoji/images/emoji_dr_37.png create mode 100644 emoji/images/emoji_dr_38.png create mode 100644 emoji/images/emoji_dr_39.png create mode 100644 emoji/images/emoji_dr_4.png create mode 100644 emoji/images/emoji_dr_40.png create mode 100644 emoji/images/emoji_dr_41.png create mode 100644 emoji/images/emoji_dr_42.png create mode 100644 emoji/images/emoji_dr_43.png create mode 100644 emoji/images/emoji_dr_44.png create mode 100644 emoji/images/emoji_dr_45.png create mode 100644 emoji/images/emoji_dr_46.png create mode 100644 emoji/images/emoji_dr_47.png create mode 100644 emoji/images/emoji_dr_48.png create mode 100644 emoji/images/emoji_dr_49.png create mode 100644 emoji/images/emoji_dr_5.png create mode 100644 emoji/images/emoji_dr_6.png create mode 100644 emoji/images/emoji_dr_7.png create mode 100644 emoji/images/emoji_dr_8.png create mode 100644 emoji/images/emoji_dr_9.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/algos/algo8404.go create mode 100644 internal/algos/argus.go create mode 100644 internal/algos/bytebuf.go create mode 100644 internal/algos/gorgon.go create mode 100644 internal/algos/ladon.go create mode 100644 internal/algos/pkcs7.go create mode 100644 internal/algos/protobuf.go create mode 100644 internal/algos/signer.go create mode 100644 internal/algos/simon.go create mode 100644 internal/algos/sm3.go create mode 100644 internal/algos/switcher.go create mode 100644 internal/common/http.go create mode 100644 internal/common/logger.go create mode 100644 internal/common/utils.go create mode 100644 internal/config/config.go create mode 100644 internal/config/watcher.go create mode 100644 internal/fqapi/audio.go create mode 100644 internal/fqapi/audio_proxy.go create mode 100644 internal/fqapi/cache_manager.go create mode 100644 internal/fqapi/catalog.go create mode 100644 internal/fqapi/category_front.go create mode 100644 internal/fqapi/category_landing.go create mode 100644 internal/fqapi/content.go create mode 100644 internal/fqapi/content_parser.go create mode 100644 internal/fqapi/content_utils.go create mode 100644 internal/fqapi/detail.go create mode 100644 internal/fqapi/device_config.go create mode 100644 internal/fqapi/device_key_pool.go create mode 100644 internal/fqapi/device_register.go create mode 100644 internal/fqapi/dhcry.go create mode 100644 internal/fqapi/emoji.go create mode 100644 internal/fqapi/gorgon_utils.go create mode 100644 internal/fqapi/item_summary.go create mode 100644 internal/fqapi/manga.go create mode 100644 internal/fqapi/manga_proxy.go create mode 100644 internal/fqapi/media_utils.go create mode 100644 internal/fqapi/multi_content.go create mode 100644 internal/fqapi/multi_detail.go create mode 100644 internal/fqapi/register_key.go create mode 100644 internal/fqapi/review.go create mode 100644 internal/fqapi/search.go create mode 100644 internal/fqapi/ttencrypt.go create mode 100644 internal/fqapi/video.go create mode 100644 internal/handlers/book.go create mode 100644 internal/handlers/category.go create mode 100644 internal/handlers/common.go create mode 100644 internal/handlers/content.go create mode 100644 internal/handlers/manga_proxy.go create mode 100644 internal/handlers/media.go create mode 100644 internal/handlers/review.go create mode 100644 internal/handlers/status.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc906de --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Go +bin/ +pkg/ +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary +*.test +*.out + +# Output of the go coverage tool +*.html +_coverage/ + +# Go workspace file +go.work + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Environment variables +.env +.env.local +.env.*.local + +# OS +.DS_Store +Thumbs.db + +# Python (already present) +__pycache__ +*.py[cod] +*$py.class +*.egg-info/ +.eggs/ + +# Build output +/dist/ +/build/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7575fc3 --- /dev/null +++ b/README.md @@ -0,0 +1,350 @@ +# 番茄小说 API + +Go 语言实现的番茄小说非官方 API 服务,完整实现了签名算法、设备注册、密钥管理等核心功能。 + +## 功能特性 + +### 核心功能 + +- **书籍搜索** - 关键词搜索,支持分页 +- **书籍详情** - 获取书籍基本信息、封面、作者等 +- **章节目录** - 获取完整章节列表 +- **章节内容** - 获取正文内容,支持纯文本/HTML模式 +- **批量接口** - 支持批量获取书籍详情和章节内容 + +### 内容类型 + +- 📖 小说/电子书 +- 🎨 漫画 +- 🎵 音频/有声书 +- 🎬 视频 + +### 评论功能 + +- 章节段评(段落评论) +- 书籍书评 +- 评论 HTML 渲染 + +### 分类体系 + +- 分类首页数据 +- 分类详情页(支持筛选、排序、分页) + +### 技术特性 + +- **签名算法** - Gorgon、8404、Argus、Ladon、Khronos +- **设备密钥池** - 多设备轮换、自动补充 +- **SQLite 缓存** - 本地缓存、过期清理、数据库压缩 +- **配置热更新** - 修改配置无需重启 +- **访问控制** - 支持 AccessKey 验证 + +## 项目结构 + +```tree +fanqie-go/ +├── cmd/fanqie/ # 程序入口 +│ └── main.go # 主函数,初始化服务 +├── config/ # 配置文件 +│ └── config.yaml # 配置文件 +├── emoji/ # 表情图片静态资源 +├── internal/ +│ ├── algos/ # 签名算法实现 +│ │ ├── gorgon.go # Gorgon 签名算法 +│ │ ├── algo8404.go # 8404 签名算法 +│ │ ├── argus.go # Argus 签名算法 +│ │ ├── ladon.go # Ladon 签名算法 +│ │ ├── signer.go # 签名管理器 +│ │ ├── switcher.go # 算法切换器 +│ │ ├── sm3.go # SM3 哈希算法 +│ │ ├── simon.go # Simon 对称加密 +│ │ ├── pkcs7.go # PKCS7 填充 +│ │ ├── protobuf.go # Protobuf 协议 +│ │ └── bytebuf.go # 字节缓冲区 +│ ├── common/ # 公共工具 +│ │ ├── logger.go # 日志工具 +│ │ ├── http.go # HTTP 工具 +│ │ └── utils.go # 通用工具 +│ ├── config/ # 配置管理 +│ │ ├── config.go # 配置加载 +│ │ └── watcher.go # 配置热更新 +│ ├── fqapi/ # API 封装层 +│ │ ├── cache_manager.go # SQLite 缓存管理 +│ │ ├── device_key_pool.go # 设备密钥池 +│ │ ├── device_register.go # 设备注册 +│ │ ├── register_key.go # 密钥注册 +│ │ ├── dhcry.go # DH 加密交换 +│ │ ├── ttencrypt.go # TTEncrypt 加密 +│ │ ├── search.go # 搜索接口 +│ │ ├── detail.go # 书籍详情 +│ │ ├── catalog.go # 章节目录 +│ │ ├── content.go # 章节内容 +│ │ ├── multi_content.go # 批量内容 +│ │ ├── multi_detail.go # 批量详情 +│ │ ├── item_summary.go # 章节摘要 +│ │ ├── category_front.go # 分类首页 +│ │ ├── category_landing.go # 分类详情 +│ │ ├── audio.go # 音频 +│ │ ├── video.go # 视频 +│ │ ├── manga.go # 漫画 +│ │ ├── manga_proxy.go # 漫画代理 +│ │ ├── review.go # 评论 +│ │ ├── content_parser.go # 内容解析 +│ │ ├── content_utils.go # 内容工具 +│ │ ├── emoji.go # 表情管理 +│ │ ├── device_config.go # 设备配置 +│ │ └── gorgon_utils.go # Gorgon 请求封装 +│ └── handlers/ # HTTP 处理器 +│ ├── common.go # 通用处理 +│ ├── book.go # 书籍接口 +│ ├── content.go # 内容接口 +│ ├── media.go # 媒体接口 +│ ├── review.go # 评论接口 +│ ├── category.go # 分类接口 +│ ├── manga_proxy.go # 漫画代理 +│ └── status.go # 状态页面 +└── go.mod # Go 模块定义 +``` + +## 快速开始 + +### 环境要求 + +- Go 1.24+ +- SQLite3 + +### 安装运行 + +```bash +# 克隆项目 +git clone https://github.com/cyqddx/fanqie-go.git +cd fanqie-go + +# 运行服务 +go run ./cmd/fanqie +``` + +服务默认启动在 `http://0.0.0.0:40180` + +## API 接口文档 + +### 基础接口 + +| 接口 | 方法 | 说明 | +| ------ | ------ | ------ | +| `GET /` | GET | 服务首页 | +| `GET /status` | GET | 服务状态(需 AccessKey) | +| `GET /self-info` | GET | 自我信息(需 AccessKey) | +| `GET /vacuum` | GET | 数据库压缩(需 AccessKey) | + +### 书籍相关 + +| 接口 | 方法 | 说明 | 参数 | +| ------ | ------ | ------ | ------ | +| `/search` | GET | 搜索书籍 | `keyword`, `tab_type`, `offset` | +| `/detail` | GET | 书籍详情 | `book_id` | +| `/info` | GET | 书籍详情(同 detail) | `book_id` | +| `/catalog` | GET | 章节目录 | `book_id` | +| `/directory/all_items` | GET | 章节目录(同 catalog) | `book_id` | +| `/multi-detail` | GET | 批量书籍详情 | `book_ids`(逗号分隔) | + +### 内容相关 + +| 接口 | 方法 | 说明 | 参数 | +| ------ | ------ | ------ | ------ | +| `/content` | GET | 章节内容 | `chapter_id`, `book_id` | +| `/multi-content` | GET | 批量章节内容 | `chapter_ids`(逗号分隔) | +| `/item-summary` | GET | 章节摘要 | `book_id`, `chapter_id` | + +### 评论相关 + +| 接口 | 方法 | 说明 | 参数 | +| ------ | ------ | ------ | ------ | +| `/chapter_review` | GET | 章节评论 | `book_id`, `chapter_id` | +| `/review` | GET | 书评列表 | `book_id`, `offset`, `limit` | +| `/review_html` | GET | 评论 HTML | `book_id`, `chapter_id` | + +### 分类相关 + +| 接口 | 方法 | 说明 | 参数 | +| ------ | ------ | ------ | ------ | +| `/category-front` | GET | 分类首页 | - | +| `/category-landing` | GET | 分类详情 | `category_id`, `word_number`, `book_status`, `sort_by`, `offset` | + +### 媒体相关 + +| 接口 | 方法 | 说明 | 参数 | +| ------ | ------ | ------ | ------ | +| `/audio` | GET | 音频内容 | `audio_id`, `tone_id` | +| `/video` | GET | 视频内容 | `video_id` | +| `/manga` | GET | 漫画内容 | `manga_id`, `chapter_id` | +| `/manga/image/{itemID}/{index}` | GET | 漫画图片 | 路径参数 | + +## 内容获取示例 + +### 搜索书籍 + +```bash +GET http://localhost:40180/search?keyword=斗罗大陆 +``` + +### 获取书籍详情 + +```bash +GET http://localhost:40180/detail?book_id=xxx +``` + +### 获取章节目录 + +```bash +GET http://localhost:40180/catalog?book_id=xxx +``` + +### 获取章节内容 + +```bash +GET http://localhost:40180/content?book_id=xxx&chapter_id=xxx +``` + +支持以下 URL 参数覆盖默认配置: + +| 参数 | 说明 | 可选值 | +| ------ | ------ | ------ | +| `text_mode` | 文本模式 | `plain`(纯文本), `html`(HTML) | +| `img_mode` | 图片模式 | `remove`(移除), `keep`(保留) | +| `review_mode` | 评论模式 | `remove`(移除), `keep`(保留) | + +### 批量获取 + +```bash +GET http://localhost:40180/multi-detail?book_ids=id1,id2,id3 + +GET http://localhost:40180/multi-content?chapter_ids=id1,id2,id3 +``` + +## 配置说明 + +### 完整配置项 + +配置文件位于 `config/config.yaml`: + +```yaml +# 服务器配置 +server: + host: "0.0.0.0" # 监听地址 + port: "40180" # 监听端口 + access_key: "" # 访问密钥(可选) + +# 番茄小说配置 +fanqie: + # 签名算法: "gorgon" / "8404" + algorithm: "gorgon" + + # 签名方法: + # - "sign2": X-Gorgon + X-Khronos + # - "sign4": X-Gorgon + X-Ladon + X-Argus + X-Khronos + signature: "sign4" + + # 内容处理 + content: + remove_images: false # 移除图片 + remove_image_desc: true # 移除图片描述 + remove_blank_lines: true # 移除空行 + remove_html_tags: true # 移除 HTML 标签 + remove_chapter_title: true # 移除章节标题 + add_paragraph_indent: false # 添加段落缩进 + double_newline: false # 段落间双换行 + enable_review: true # 启用段评 + + # 漫画处理 + manga: + # 输出模式: + # - "proxy": 代理模式 + # - "url": 直接返回图片 URL + # - "data_uri": Base64 编码 + output_mode: "url" + +# 缓存配置 +cache: + enabled: true # 启用缓存 + sqlite_path: "./cache.db" # SQLite 路径 + cleanup_enabled: true # 启用自动清理 + cleanup_seconds: 86400 # 清理间隔(秒) + +# 设备密钥池 +device_key_pool: + enabled: true # 启用设备池 + initial_size: 20 # 初始设备数 + min_size: 10 # 最小设备数 + max_size: 30 # 最大设备数 +``` + +### 配置说明表 + +| 配置项 | 说明 | 默认值 | +| ------ | ------ | ------ | +| `server.host` | HTTP 服务监听地址 | `0.0.0.0` | +| `server.port` | HTTP 服务监听端口 | `40180` | +| `server.access_key` | 访问密钥,设为空则不验证 | - | +| `fanqie.algorithm` | 签名算法 `gorgon` / `8404` | `gorgon` | +| `fanqie.signature` | 签名方法 `sign2` / `sign4` | `sign4` | +| `fanqie.content.*` | 正文内容处理选项 | 见上文 | +| `fanqie.manga.output_mode` | 漫画输出模式 | `url` | +| `cache.enabled` | 是否启用 SQLite 缓存 | `true` | +| `cache.sqlite_path` | SQLite 数据库路径 | `./cache.db` | +| `device_key_pool.enabled` | 是否启用设备密钥池 | `true` | +| `device_key_pool.initial_size` | 初始设备数量 | `20` | +| `device_key_pool.min_size` | 最小设备数量 | `10` | +| `device_key_pool.max_size` | 最大设备数量 | `30` | + +## 签名算法详解 + +### 算法实现 + +项目实现了番茄小说的核心签名算法: + +| 算法 | 文件 | 说明 | +| ------ | ------ | ------ | +| **Gorgon** | `algos/gorgon.go` | 核心签名算法,生成 X-Gorgon、X-Khronos、X-SS-REQ-Ticket | +| **8404** | `algos/algo8404.go` | 另一种签名算法实现 | +| **Argus** | `algos/argus.go` | 增强签名,包含 ProtoBuf、SM3、Simon、AES 加密 | +| **Ladon** | `algos/ladon.go` | 附加签名,基于 MD5 变换 | +| **Khronos** | 内嵌于 Gorgon | 时间戳签名 | + +### 签名方法 + +- **sign2**: X-Gorgon + X-Khronos(基础签名) +- **sign4**: X-Gorgon + X-Ladon + X-Argus + X-Khronos(完整签名) + +## 设备密钥池 + +项目实现了设备密钥池管理: + +- **自动注册** - 启动时自动注册多个设备 +- **密钥轮换** - 请求时自动切换设备 +- **自动补充** - 设备数量低于阈值时自动注册新设备 +- **持久化存储** - 设备信息保存到 SQLite + +## 缓存机制 + +- **SQLite 本地缓存** - 缓存书籍内容和章节数据 +- **自动清理** - 定期清理过期缓存 +- **数据库压缩** - 支持 VACUUM 操作优化数据库 + +## 技术栈 + +- **Go 1.24** - 编程语言 +- **Gin** - HTTP 框架(间接依赖) +- **SQLite3** - 本地数据库 +- **Viper** - 配置管理 +- **goquery** - HTML 解析 + +## 注意事项 + +- 本项目仅供学习和研究使用 +- 请遵守番茄小说的服务条款 +- 请合理使用,避免对官方服务造成影响 +- 签名算法和设备注册可能随时失效 + +## License + +MIT License diff --git a/cmd/fanqie/main.go b/cmd/fanqie/main.go new file mode 100644 index 0000000..0693796 --- /dev/null +++ b/cmd/fanqie/main.go @@ -0,0 +1,182 @@ +package main + +import ( + "fanqie/internal/algos" + "fanqie/internal/common" + "fanqie/internal/config" + "fanqie/internal/fqapi" + "fanqie/internal/handlers" + "fmt" + "log" + "net/http" + "os" +) + +var ( + logger = common.NewLogger("main", "INFO", "") + configWatcher *config.ConfigWatcher +) + +func main() { + logger.Info("番茄小说API服务 - v2.0.0") + + // 加载配置文件 + initConfig() + + // 初始化数据库 + if err := fqapi.InitDB(); err != nil { + logger.Error("数据库初始化失败: " + err.Error()) + os.Exit(1) + } + defer fqapi.CloseCache() // 程序退出时关闭数据库 + + // 加载统计数据 + handlers.LoadStatistics() + + // 启动统计数据定期保存 + handlers.StartStatisticsSaver() + defer handlers.StopStatisticsSaver() // 程序退出时保存统计数据 + + // 启动缓存自动清理任务 + fqapi.StartCacheCleanup() + defer fqapi.StopCacheCleanup() // 程序退出时停止清理任务 + + // 初始化算法切换器 + initAlgorithm() + + // 初始化设备密钥池 + if err := initDeviceKeyPool(); err != nil { + logger.Error("设备密钥池初始化失败: " + err.Error()) + os.Exit(1) + } + + // 设置路由 + setupRoutes() + + // 启动服务器 + startServer() +} + +// initConfig 初始化配置 +func initConfig() { + configPath := "./config/config.yaml" + if _, err := os.Stat(configPath); os.IsNotExist(err) { + os.MkdirAll("./config", 0755) + } + + if err := config.LoadConfig(configPath); err != nil { + logger.Warning(fmt.Sprintf("加载配置失败: %v", err)) + } + + // 启动配置文件热更新监控 + watcher, err := config.StartConfigWatcher(configPath) + if err != nil { + logger.Warning(fmt.Sprintf("配置监控失败: %v", err)) + } else { + configWatcher = watcher + } + + // 注册配置更新回调 + config.RegisterConfigCallback(func(cfg *config.Config) { + algos.GlobalAlgorithmSwitcher.SwitchAlgorithm(cfg.Fanqie.Algorithm) + algos.GlobalSignatureSwitcher.SwitchSignature(cfg.Fanqie.Signature) + }) +} + +// initAlgorithm 初始化算法 +func initAlgorithm() { + if config.AppConfig != nil { + algos.GlobalAlgorithmSwitcher.SwitchAlgorithm(config.AppConfig.Fanqie.Algorithm) + logger.Info(fmt.Sprintf("当前算法: %s", config.AppConfig.Fanqie.Algorithm)) + + algos.GlobalSignatureSwitcher.SwitchSignature(config.AppConfig.Fanqie.Signature) + + // 根据签名方法显示详细信息 + var signatureDetails string + if config.AppConfig.Fanqie.Signature == "sign4" { + signatureDetails = "X-Gorgon + X-Ladon + X-Argus + X-Khronos" + } else { + signatureDetails = "X-Gorgon + X-Khronos" + } + logger.Info(fmt.Sprintf("当前签名方法: %s", signatureDetails)) + } else { + algos.GlobalAlgorithmSwitcher.SwitchAlgorithm("gorgon") + algos.GlobalSignatureSwitcher.SwitchSignature("sign2") + } +} + +// initDeviceKeyPool 初始化设备密钥池 +func initDeviceKeyPool() error { + pool := fqapi.GetDeviceKeyPool() + if err := pool.Initialize(); err != nil { + return fmt.Errorf("初始化设备密钥池失败: %w", err) +} + + // 显示池状态 + stats := pool.GetStats() + logger.Info(fmt.Sprintf("设备密钥池状态 - 总数: %d, 有效: %d, 无效: %d", + stats["total"], stats["valid"], stats["invalid"])) + + return nil +} + +// setupRoutes 设置路由 +func setupRoutes() { + http.HandleFunc("/", handlers.HandleMain) + http.HandleFunc("/status", handlers.RequireAccessKey(handlers.HandleStatus)) + http.HandleFunc("/self-info", handlers.RequireAccessKey(handlers.HandleSelfInfo)) + http.HandleFunc("/vacuum", handlers.RequireAccessKey(handlers.HandleVacuum)) + + // API端点 + http.HandleFunc("/search", handlers.HandleSearch) + http.HandleFunc("/detail", handlers.HandleDetail) + http.HandleFunc("/info", handlers.HandleDetail) + http.HandleFunc("/catalog", handlers.HandleCatalog) + http.HandleFunc("/directory/all_items", handlers.HandleCatalog) + http.HandleFunc("/multi-detail", handlers.HandleMultiDetail) + http.HandleFunc("/multi-content", handlers.HandleMultiContent) + http.HandleFunc("/content", handlers.HandleContent) + http.HandleFunc("/item-summary", handlers.HandleItemSummary) + http.HandleFunc("/category-front", handlers.HandleCategoryFront) + http.HandleFunc("/category-landing", handlers.HandleCategoryLanding) + http.HandleFunc("/audio", handlers.HandleAudio) + http.HandleFunc("/audio/proxy/", handlers.HandleAudioProxy) + http.HandleFunc("/video", handlers.HandleVideo) + http.HandleFunc("/manga", handlers.HandleManga) + http.HandleFunc("/manga/image/", handlers.HandleMangaImageProxy) + + // 评论相关端点 + http.HandleFunc("/chapter_review", handlers.HandleChapterReview) + http.HandleFunc("/review", handlers.HandleReview) + http.HandleFunc("/review_html", handlers.HandleReviewHTML) + + // 表情图片静态文件服务 + http.Handle("/emoji/", http.StripPrefix("/emoji/", http.FileServer(http.Dir("emoji")))) +} + +// startServer 启动服务器 +func startServer() { + host := "0.0.0.0" + port := "40180" + + if config.AppConfig != nil { + if config.AppConfig.Server.Host != "" { + host = config.AppConfig.Server.Host + } + if config.AppConfig.Server.Port != "" { + port = config.AppConfig.Server.Port + } + } + + addr := host + ":" + port + logger.Info(fmt.Sprintf("服务器启动在 %s", addr)) + + if err := http.ListenAndServe(addr, nil); err != nil { + logger.Error(fmt.Sprintf("服务器启动失败: %v", err)) + os.Exit(1) + } +} + +func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) +} diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..9d3dcda --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,82 @@ +# 番茄小说API服务配置文件 + +# 服务器配置 +server: + port: "40180" # 监听端口 + host: "0.0.0.0" # 监听地址 + access_key: "" # 访问密钥,为空则不启用验证(用于 /status 和 /self-info) + +# 番茄小说配置 +fanqie: + # 算法类型: "gorgon" 或 "8404" + # gorgon: Gorgon算法 + # 8404: 备用算法 + algorithm: "gorgon" + + # 签名方法: "sign2" 或 "sign4" + # sign2: 仅Gorgon签名 + # sign4: X-Gorgon + X-Ladon + X-Argus 完整签名 + signature: "sign4" + + # 内容处理配置 + content: + # 是否移除图片(默认false,保留图片) + remove_images: false + + # 是否移除图片描述(默认false,保留描述) + remove_image_desc: true + + # 是否移除空行(默认true,移除空行) + remove_blank_lines: true + + # 是否移除HTML标签(默认false,保留HTML) + remove_html_tags: true + + # 是否移除章节名(默认false,保留章节名) + remove_chapter_title: true + + # 是否添加段落缩进(默认true,添加两个全角空格) + add_paragraph_indent: false + + # 段落间使用双换行(true=\n\n, false=\n) + double_newline: false + + # 是否启用段评功能(默认true,启用段评) + enable_review: true + + # 漫画处理配置 + manga: + # 输出模式:proxy / data_uri / url + # proxy: 返回本地代理URL /manga/image/{itemID}/{index} + # data_uri: 返回 data:image/jpeg;base64,/9j/4AAQ... + # url: 返回原始图片URL数组 + output_mode: "url" + +# 缓存配置 +cache: + # 是否启用正文内容缓存 + enabled: true + + # SQLite数据库文件路径 + sqlite_path: "./cache.db" + + # 是否启用正文内容缓存自动清理 + cleanup_enabled: true + + # 正文内容缓存清理时间(秒),0表示不自动清理 + cleanup_seconds: 86400 + +# 设备密钥池配置 +device_key_pool: + # 是否启用设备密钥池 + enabled: true + + # 初始化设备数量 + initial_size: 20 + + # 最小设备数量(低于此数量自动补充) + min_size: 10 + + # 最大设备数量 + max_size: 30 + diff --git a/emoji/data.json b/emoji/data.json new file mode 100644 index 0000000..0894438 --- /dev/null +++ b/emoji/data.json @@ -0,0 +1,55 @@ +{ + "version":1, + "size": 49, + "datas": { + "1": "[微笑]", + "2": "[偷笑]", + "3": "[笑]", + "4": "[什么]", + "5": "[害羞]", + "6": "[爱慕]", + "7": "[飞吻]", + "8": "[奸笑]", + "9": "[尬笑]", + "10": "[思考]", + "11": "[撇嘴]", + "12": "[做鬼脸]", + "13": "[酷]", + "14": "[翻白眼]", + "15": "[惊呆]", + "16": "[震惊]", + "17": "[送心]", + "18": "[委屈]", + "19": "[快哭了]", + "20": "[笑哭]", + "21": "[哭]", + "22": "[大笑]", + "23": "[舔屏]", + "24": "[怒]", + "25": "[捂脸]", + "26": "[吐]", + "27": "[恐惧]", + "28": "[抓狂]", + "29": "[敬礼]", + "30": "[石化]", + "31": "[OK]", + "32": "[赞]", + "33": "[爱心]", + "34": "[伤心]", + "35": "[KISS]", + "36": "[懂了]", + "37": "[探究]", + "38": "[重拳出击]", + "39": "[吃瓜]", + "40": "[盯]", + "41": "[你细品]", + "42": "[赶稿中]", + "43": "[注意]", + "44": "[饱了]", + "45": "[码住]", + "46": "[学会了]", + "47": "[顶帖]", + "48": "[求爆更]", + "49": "[求关注]" + } +} \ No newline at end of file diff --git a/emoji/images/emoji_dr_1.png b/emoji/images/emoji_dr_1.png new file mode 100644 index 0000000000000000000000000000000000000000..93dbd58e81b7c5a423d5055a41700091a6299e8a GIT binary patch literal 5009 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 b(RcL^^N{o`(E1l800000NkvXXu0mjf%*`O^ literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_10.png b/emoji/images/emoji_dr_10.png new file mode 100644 index 0000000000000000000000000000000000000000..b9ad4339816e1a617f14b79e354378d5ac53c379 GIT binary patch literal 5252 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 KMNUMnLSTXkFQ$V4 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_11.png b/emoji/images/emoji_dr_11.png new file mode 100644 index 0000000000000000000000000000000000000000..a236269539f75913eed785e2026832f47020fd8d GIT binary patch literal 4941 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 B`N#kO literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_14.png b/emoji/images/emoji_dr_14.png new file mode 100644 index 0000000000000000000000000000000000000000..79dc6be51c899045c86f44d6fca73c848bdce0e1 GIT binary patch literal 4919 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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_16.png b/emoji/images/emoji_dr_16.png new file mode 100644 index 0000000000000000000000000000000000000000..6ab75d2a64de1bc746e8c2c5c4c70800090f1f17 GIT binary patch literal 5156 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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_17.png b/emoji/images/emoji_dr_17.png new file mode 100644 index 0000000000000000000000000000000000000000..23b1c05dd2bc882279d75dc51acdebed28ab19ad GIT binary patch literal 5401 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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_21.png b/emoji/images/emoji_dr_21.png new file mode 100644 index 0000000000000000000000000000000000000000..8ed4c3c414edf2db9119868b17419b9a94502fb1 GIT binary patch literal 5504 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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_22.png b/emoji/images/emoji_dr_22.png new file mode 100644 index 0000000000000000000000000000000000000000..c9af036725a15464c42f8bd5d799d4abd6422b39 GIT binary patch literal 4822 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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_23.png b/emoji/images/emoji_dr_23.png new file mode 100644 index 0000000000000000000000000000000000000000..281b6127e744c0d58702e953e449c034df2df416 GIT binary patch literal 5095 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 JPDHLkV1ifC2POaj literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_24.png b/emoji/images/emoji_dr_24.png new file mode 100644 index 0000000000000000000000000000000000000000..0f1f79d32463f8b535a2cc551e38c7978f5aee00 GIT binary patch literal 5623 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 R>1+T1002ovPDHLkV1g^w8QuT@ literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_25.png b/emoji/images/emoji_dr_25.png new file mode 100644 index 0000000000000000000000000000000000000000..f067b2766a23b6c371194d9065a1e411babca35e GIT binary patch literal 4771 zcmV;U5?t+xP)xOZ($``N2>7=6wB&2l{Lv-*xuZ zhWolg`n*a0j0O4JbN%6U_}+H;jTQcy68pbO`ZOT>ggy{D}zWt~!^soBGRJ{j4kbvo89%M_;bPQMcB- zsAu`7C_=a6{ncn~u)FM;Ep)cHOSjxyw9k*QouI9ZnN%yR%cEJc%Vn>=Kf3Jy|Ns2l zas1kC{Mc&!?t=a6fc?>5{o{E3;B@|q1pVQ5{*4R#%2xf(TKwdD`{#cA)@J<2QT~t- z{_%wT>3}T)68zI-{pfrB-g5oLP5ze`{oQZ;)M@>`MgEc#{lZEAp&9+IFa5JL{hJ*9 zsVPms??%1t|MiAi#qs^TLDnKF+aM`+(DnW1c>w?c;U6gf|C0T>J!;JK{kAz@$nyTC zAydQf|ND&npdoP1^@-T`klXnD&SE(O5oO8pg4XsQ0T4z75SHHfeAD$$2oU_rUjLjC zC;$gD0TDk15H!8{{KZ)r00HS8D9;}$IlS`oA1eK%CnvuC{2(gG87f%}5@pK!oZ$Ia z#{K@!OcN0d|Cb8$?&tXOitpWY{J>LM*ZrvD`uh0u{Ml#!|F9r-9032@ zS?hu$t`;GPHVA(!21~>KS-92zyDykY760mR@6&4j)Kh$QLe4ZOjOG7o*Z$hmcf%7W z^pqZX+5Yw5rq0`(>f?m1r(I$a6nVI`$-Hq=MJhQVDd1irr(Yb0+x+zBz_Qo8KW~iN zyz<=e=-su zDzS1Ym=7Lr#^uz?&cU;{z0{*+M^M{hDdXGThs)2eouH$SkgBnMfn{%OU1ZyoFh-5C zrOB`{OlNdKDoYg-+VuTfldn5gZH!ngwyCPCcRl7nDjjy`jsO4vM08S4QveDRw=VX} zA>fyXAY``F!lO(6M*Q)5VLCDFjBZi*UqUtY?&7(AdhO7qo|OFf+S>f_w4eU*($bxh zfyBXNQT}#eN`u6(kN^M=5lKWrRA}Dp*Ud{5VH^kW_C;b*EGt~2kSL4l5_F3GfZ5q~ zmZ5f(mD!2e=(v|PM1o=6rG;WIdEYk4wNOE!7t|tyO``(E?9vNGRscx-W-}8IM-3|ZqKmYST|0W)fwC4){a@!C;f$>+x*(yQyyp1_Hju z#>Tp8+VG7p&>U>>ZundAv;>=dzQ$OjOVhd#4Z(jbb%Zz7uRn^eP0hYGXbi1^IUZ~r-O5O7SZYYS{$uhw415=>M$O#hrA zo?I5tEuQtL+vICoR++idjfmn*RumauZyFHQu*cmOi*nL!Mfi7#n*Rwiw*a3Wpf0>$#k~ zXY`BQfg=6={mI{zJ#blJjlsOHwK#EY?%kWEQDo`O98|D#qjfzN z=1k|yU7gA@7|H37e`~K)z}@(9!bN(lQKGK%&{x+c-i?-fdQuSt3r%}Pr<+SAv0hX? zR~Rbfj(UIN{sy9e;Q|f|7^5yP%}q>P;{p0+^hFn4V%9eXHxx?hxJS{bN?`-g`wOl6 zR(+qn0g1!nfH@q-A(vk)J{e_Md{GV$f{nwa16HHr_A2xkVXR@8h9u{5hZirl)o1*`W-2Eo{^h=AQgD!E!W4wzg6ZVgOZwtMGlZc_6e39gDs)5Ajq zk113XkFXUkh8s0ZIVr`omL4s7?(kM??gc9Yu7Dy(T3HcK2QOEj(0ioRVmT^h8E zwQ`3$*swkRh8+!l&yR1CWU@&EMnvD#!`rv--{o6OqU|O$_Nb)7nkx*qLvBC3)zfgO zwRQKVtv`Yr_Lw$|@V--z1_lP6!cLJ57_JTT%~HF-y2Hcz(B2(~jvqgEtn*;KyX!wF zhuEkUA=5#}t%n1rk%6a=gq@1u^u?=76ec(n(`sOo!$|*7Ydhf1PP)|Yg$1nI@ML7> z!57FZ%pN2=tZ4+}E=6&Ne-7jM4VU0!J6v7wLD>mYCTHH?xcT_?+p0Z4_AIwaqt(I@ z{{+?%0e2qsxE4M_uxZ${hCBQ5La9=D|GdVwu;yKmeg#8rZ?D3e{BH2Uq<{?-GK0G( zXU>*-)0I1S=F?;g8V61@w^H4z4Le*H7|V6S6>d4AcR;4bfFUE3>O!%YIiH`OzjNnK zp7pNMWNYCVdrZ5+%$3PS;BwgYq1C}*Au9w=0#4`qvIuCW4SF`tU_Dd|o2!Mrn{*B{ zndoi>@=T_f&t}h27+xvf`~+Lj4=q&+aoBbpNBnEpe+clf93okC#>Yr`0ezufym%3G zU%pbAo-XbGdi&AS0bDNu=j30&$K7EuX&o}bTt;amIx_n#KQCaC&u5Co8>NL*`Tp%k zkM3&`gTYDF0n_Usa#sQ462ldS#IFn67>TjO9WgSkw!gx2Qx zOkZWbug`|F=QE|!f;p+cQAv?Pfc1i|LbhE83|GKId%ZPqfWd6Pmg(W~+3Yl2$bEp} zV$!`-n7axT0b|xSELIwU`#rpEv&-}HIAH25WYELo%@}^ZC4x&(Thw?=8KY8KJ9Y!%4vXIn0{5e5c*vZ7%R`z$EJ= z4--8;wUC+4vxvqhjLY<_444Omc*E4+FUJ(-O5aaf2Q2MrEn+St=}H=_LvTf(v+uhDkf!4rgJ&2O4VOPNyul-@5zqN(L~6!CAPx zu*6U&JXDZk0yZ6kwkHjEs|ze-0mJ=Rnoc7^1`Ib-L}62MxT5h;GNu@YbETPUPx{ne zSGbeGqVuJxXQg5$ZOgb(dAOyIQ{y4l*hIKXFjY2`& zOfu;5E$QhW+uQ5v>kJt*T)gkyLxqd%0V+PF+_nSXJq7si`VHKd&oH|T&yd61IlblM zuJ$@gQEAPstJMmNq2lBApTYtvirZa|%OqgIWf(xoa2d?9J^i#wtwMB~ZF1|Y_MJWh z%4{bGfKM+~98Pyx@vSAp?7Cc&hDqT0E*Hu6OmBy}7cL^S#%tHNcduT^XiWXg85;Za zh0`~$*IcnM4ccXQNuNo~PyoZd(V@2Q-D}W{Q@U!upRo45eUcWm*rnD!f?gA{*l-ktod}Cu{L;l@;wxzk&n%!Qs!2F-3!HV=@+axr0Wt4fU zx(i{s81G7b4+N|M*w(&xub;g5U+r;;>}Jumv8$AfmcU~a#g)mtiNq=yW|`rtV_moN zfDyxfEG<5;Jt4XTYpz>*j>j)l9N1W~NM)r+b5g16R=Au-B>(LGtH&>rrrmbsV7aiq zw7>+r-R_wZc=Q}c%T3)SnQaQg9en`{ql)jmZTpyB-gnJhw-US&&Mm1>01Z@rFjEp&{P~9jmS# z^MNv(FlfVd%$FUg;qxNonrTWZ^&tq?oM14B;*@|j-*=VxSG{Ffu;Rv2$gNl)TjZfD zFlf09c-JR$;h+((7d-iR4dp-IJp1=9i~4e5xD}3)p%Qr6Y>*;tY_rq{FS!k3tOjg^ z`hR%&l_5u&B8@p!3FGDEdAG9vxH!YtsbBEBM{6D zwl&p+qet~b!{M6l!9p|aV2E61N&b8%%lk@noz5d;hsloeQq>|&tmE?Bi-XlsA-dkw zR`3zcEWVb|!(uq@M-n1&sp!uT@e{=0FKbzGQx z^k{g{@6}kqz6<>cn`vJWdBCRFjU<-MNsT+~&}iM~Sqi{_7s%T`e7U?BBvW zJx)$VCe#1CGaipOHHkLi(P&%F2Nv_b_&%K$U61ab95Ip|*nJ{ zT&^+fq{x4k^ZDh+s!JT_^YSJyiZ}>;-}i#(bj{81cib$~zyk~{Xgr&ZVdpofEK7FC zp+W%5U^k9VBB-2ea*RlR4!J&;eq^z@${xq4fQSPF*t7(@!H8hE4EFR+tFhNFs_!{{ zGsv=lBw<6ULc`DxD-}WB(+`tm$@kZ8}W{p3j-=RzC`%;RX$NVtLnsBtNjUp5E))Z4oO@e)VX2b?f?JRlr9Y zM-ofvG@&HRZCP%@c=_egufp}KI7UeFtkE-D8?=9{x*qcXQFvFZP)g~xTN~B3bi-rO z8!rt+&#vcsYOf=;HqGoSpeaTXEH+t&ZeiTblRACXaF=;vZlCE-yG{qQ x-fxm5Ve5|5)mUA}aqKp`c7NBdUAz8gzX4leJf*-tnYRD{002ovPDHLkV1gWcncDyW literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_26.png b/emoji/images/emoji_dr_26.png new file mode 100644 index 0000000000000000000000000000000000000000..c18adbd0c6e39a5985c043b8c8e4576929548b21 GIT binary patch literal 5057 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 XP1>_^5gm_H00000NkvXXu0mjf#Hbgj literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_27.png b/emoji/images/emoji_dr_27.png new file mode 100644 index 0000000000000000000000000000000000000000..069160031f59845dcae763a7d36959868531085a GIT binary patch literal 4927 zcmV-F6Ts|=P)$5Z^cJk-8(zO;SsRTllC9q>yg6{NZ-}w>mEY z6a3k0{K-{z$ngEgQ2XC={M~T7JAI`WxMeGyg~fiZeFzR`T6?&rz2#u<8#65MWOCO z5*YrR68z_X_V)Pl@bPlS@$Wh#Rwf&{#G7Ea?`gj9@Gm4s85!+DB2laD{;DVbp&I^{ z2wAc0>$y;Dw%=m3>g2aqOs4H_wA!G*uhGVWZNBVqyygAz?Dh2Z%*Tn@$bIs-NZq$z z{-qsNuI^~L>3Fx&aInv)!K$&tqFpK+^X%#T>xJOScK(qB+JDWs^x&IxVy7|)x>zswrnK<58B}1@bdezv4`sI`H;QA^GzUJtKQ$<(rBg0 zov@vPywBCjh{Lpa#=(UDwIaiRAb7pqbf>?YzqpFGy{fa2vdz?xud9@(c0It#j@?; zi-$QN%Sj*qqz$pr;ltF{+_O;Ak}LLj5sky-n!K^s&6eughR>fwp3CHazvPa?+MdJA zyR2=FX*U80000&bW%=J01f{B z7}rBjBK|h;*?(aDs-jmR4%~4r=<)E>tG>LUhlET!{`%e5k&AR+@6gc9xT9Xy)x*1v ziSF*Dn4EsHs%@M!wV(h153ET?c7DxmY_9#sVO&*xKZmFtqeXUb1z2DQc;3WuNQ1@HYdVS#k$5vfZNkUI}QG7^t z0NZrctFtz#Sf9bi-S`A3n%lnW9)r~+%4C*BZCREzx&SRSY`Jkgwuf0+&47+k*hI&` zrCtd^xTD)wp;f(4Dp*1zGJQ0k`%N+FGe#)S(u4CSbdKD0@Gijq9f zo(&?aKS=l^J^R1EQ6k)x~@we+PyoEHAX$KG%kIdB+KZ2Br|;=57hH6 zug30nTrB?=7H})4wA%}oi7rj`4A%@g*C+R2-fpv7ImOH*D+&)iv`=?Mus<&VFo!B4 zwt*L@C<+BheE8{9R|^EYKD!JbiX2!8v;4f3*GlP~B;R@3Q&nZz-u2yW=SG%j2a^l9 zAQmZWPd&jMD59>bC>~>8{yhKD1O9{`gj<`)gjfnLU<4O!){1xhT{Uv5x}HRM7Ugry zi-8YajE%AyyKKYtngQUS3B&aV(W5j*|8Tkd2ja~54&i>4hGd42t9eui&) z34iO|D(XteiJ}ⅆ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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_29.png b/emoji/images/emoji_dr_29.png new file mode 100644 index 0000000000000000000000000000000000000000..b02beb527444126c26975fc48670099e0879688a GIT binary patch literal 5366 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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_3.png b/emoji/images/emoji_dr_3.png new file mode 100644 index 0000000000000000000000000000000000000000..677a91d61ec0aa02d29cf3c21bb760a84d2a6fc5 GIT binary patch literal 4752 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 XQqWm@FmDH100000NkvXXu0mjfABM0- literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_31.png b/emoji/images/emoji_dr_31.png new file mode 100644 index 0000000000000000000000000000000000000000..6cb04d9a1937742f43332df9b9176c09df70e47c GIT binary patch literal 5062 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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_33.png b/emoji/images/emoji_dr_33.png new file mode 100644 index 0000000000000000000000000000000000000000..1b17978dfd075c63ff4750d1e49975230f3adf05 GIT binary patch literal 4003 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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_35.png b/emoji/images/emoji_dr_35.png new file mode 100644 index 0000000000000000000000000000000000000000..57f622267d5a75178488ebbd960926336f51fddf GIT binary patch literal 4750 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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_37.png b/emoji/images/emoji_dr_37.png new file mode 100644 index 0000000000000000000000000000000000000000..cdf0af453e4265f8353c23d32799f586d63bc912 GIT binary patch literal 4975 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 Hu0mjfyZFaa literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_40.png b/emoji/images/emoji_dr_40.png new file mode 100644 index 0000000000000000000000000000000000000000..6107e542bfff94bf32b1591eb9e6780297e39e30 GIT binary patch literal 5105 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 JPDHLkV1oC*tr!3R literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_42.png b/emoji/images/emoji_dr_42.png new file mode 100644 index 0000000000000000000000000000000000000000..df740dd0b41178a8af64e580f81fca60da5ad135 GIT binary patch literal 5306 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 M07*qoM6N<$f;s=q`~Uy| literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_43.png b/emoji/images/emoji_dr_43.png new file mode 100644 index 0000000000000000000000000000000000000000..34372747f7dbbd5cec55127ff4e4bd1658adb098 GIT binary patch literal 5220 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 Ztq+pCpMMa@?N`npiPy}kUYHMqLG^W1{E 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 P00000NkvXXu0mjfP=qOX literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_46.png b/emoji/images/emoji_dr_46.png new file mode 100644 index 0000000000000000000000000000000000000000..c2cbf4f1d58a53abc2dc7b8a3c4c04e1252848ec GIT binary patch literal 4569 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 P00000NkvXXu0mjf;jKdA literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_48.png b/emoji/images/emoji_dr_48.png new file mode 100644 index 0000000000000000000000000000000000000000..5936780b96b2ba19dec3d31a98c59a89ede7e32e GIT binary patch literal 6513 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 R8}I-C002ovPDHLkV1gAF9PR)B literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_6.png b/emoji/images/emoji_dr_6.png new file mode 100644 index 0000000000000000000000000000000000000000..69d6b0b5f65e4cbcffca5a1b032f64b4a5fa8720 GIT binary patch literal 5040 zcmV;h6Hn}kP)n;h{nKp!h!y%mLHu|`{6tCmLP`FfHu^zI|D`$p zs6_tJY5%N0{=QWFy;=W-3jI|5WngzFGd9JN{B%{?K0jyHfwQ zNB_85`$JRyJ1_nv4gSMmNe2=B#99BgPya$m`4}x50t@~$7yeCI|FT5?E-e54kpDkJ z{n{wfn80S*5uKqvzb|7Lmr zvrGP>J^#N;`xYTl2@?N5RR1e9{x~Q9sX_lXHvXb3|N4plMO^>YTK~aQ`XW93N+bP0 z8ve;v{;X5~qc#-)3jG%d{0a#FH9!7382{T}_#-9#s7?0+7yr;y|F|#r4<`FoKJ6VN z|Hx4MOfmg*F8=#}|L1J|Rx4gC9}WNq|Kel*#X6N`HTz{L^Hw9F6dV44H2;a1|8a!< zaY}znEdMnk|MGU#olE|YJx(MVK^YeRdW)%aIsIoh>76ZVJS5y2A~F#W{mgFuy=U^h zRokpk`F|yWJRZ3d9fS`TY6}(qfkgduJEK=5@G~R)DkA@-u6AIR8Rz@M2 zvK#WFh;A#-Xa3lMeCy}Fmxe;+-|OSSq|nJgI`qh%N$FcODE_vcd)Txt<-fS7D$8@N ze}Mo15GhGSK~!ko?9;z%;$RrS@yjnbXpqn?#4LhCLFjaaLLl!H2(K?H86trWt{MCf zZgYC4fE+k>e8++gN1%elPB#_Yy42A^p`+Y%&zrn$ZL_p$Z}N*0FfV@cyh*+tI&|pJ z|4WhOqq??H{YaLTZ(p5U*R1oJscvM}&MmDj?-41Zjmn9sdX9^{jgaejs(Dgrd=0J` z4a>9<@&GLdzq__sX&B1Bhcz@)=On;yd&LPMx~UoRKBui)Dwrbor8hX?d7fIS?`>+s zQgz`L9*FsIlh0|4G11jZbx*7bYyopj!D@M z&L33QVT@8n850w-0|M?$F|qO)UpOV<)Epe_y(;53KIb}l<`nn*$DW|
JHZ3J>9!m7zF)LuNgbIY(0G( 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 literal 0 HcmV?d00001 diff --git a/emoji/images/emoji_dr_9.png b/emoji/images/emoji_dr_9.png new file mode 100644 index 0000000000000000000000000000000000000000..38a8924ecd22690acd1e4cb44c05c4d446fad9d3 GIT binary patch literal 5046 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 M07*qoM6N<$g2@L*F8}}l literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..666fd7a --- /dev/null +++ b/go.mod @@ -0,0 +1,64 @@ +module fanqie + +go 1.24.0 + +require ( + github.com/PuerkitoBio/goquery v1.8.1 + github.com/fsnotify/fsnotify v1.7.0 + github.com/google/uuid v1.4.0 + github.com/mattn/go-sqlite3 v1.14.18 + github.com/spf13/viper v1.18.2 +) + +require ( + github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.11 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.11.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/mock v0.6.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bd04443 --- /dev/null +++ b/go.sum @@ -0,0 +1,246 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= +github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= +github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/algos/algo8404.go b/internal/algos/algo8404.go new file mode 100644 index 0000000..e0f6ec3 --- /dev/null +++ b/internal/algos/algo8404.go @@ -0,0 +1,174 @@ +package algos + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "math/rand" +) + +// Algo8404 8404加密器 +type Algo8404 struct { + unix int64 + params string + xSSStub string +} + +// NewAlgo8404 创建新的Algo8404实例 +func NewAlgo8404(params string, unix int64, xSSStub string) *Algo8404 { + if xSSStub == "" { + xSSStub = "00000000000000000000000000000000" + } + return &Algo8404{ + unix: unix, + params: params, + xSSStub: xSSStub, + } +} + +// getXGorgon 生成X-Gorgon签名 +func (a *Algo8404) getXGorgon() string { + timeHex := fmt.Sprintf("%x", a.unix) + timeBin, _ := hex.DecodeString(timeHex) + + m := md5.Sum([]byte(a.params)) + mBytes := m[:4] + + var aaa []byte + if a.xSSStub != "" { + stubBytes, _ := hex.DecodeString(a.xSSStub) + if len(stubBytes) >= 4 { + aaa = stubBytes[:4] + } else { + aaa = make([]byte, 4) + } + } else { + aaa = make([]byte, 4) + } + + // 生成随机数 + a1 := rand.Intn(256) + a2 := rand.Intn(256) + + // 构建数据 + data := make([]byte, 0) + data = append(data, mBytes...) + data = append(data, aaa...) + data = append(data, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x07, 0x04}...) + data = append(data, timeBin...) + + // 初始化表 + table := make([]int, 256) + for i := 0; i < 256; i++ { + table[i] = i + } + + key := []int{0x4A, 0x00, 0x16, a2, 0x47, 0x6C, 0x00, a1} + + // 第一轮处理 + w8 := 0 + for i := 0; i < 256; i++ { + keyI := i & 7 + w10 := key[keyI] + w9 := w8 + table[i] + w9 = (w9 + w10) & 0xFFFFFFFF + w10 = w9 + w10 &= 0xFFFFFF00 + w20 := (w9 - w10) & 0xFFFFFFFF + table[i] = table[w20] + w8 = w20 + } + + // 第二轮处理 + dataArr := make([]int, len(data)) + for i, b := range data { + dataArr[i] = int(b) + } + + w26 := 0 + for i := 0; i < len(dataArr); i++ { + w25 := i + w8_ := w25 ^ 1 + w9_ := ((w25 & 1) << 1) & 2 + w8_ = (w9_ + w8_) & 0xFFFFFFFF + x10 := w8_ & 0xFF + + w9b := table[x10] + w11 := (w26 ^ w9b) & 0xFFFFFFFF + w9c := ((w26 | w9b) << 1) & 0xFFFFFFFF + w9c = (w9c - w11) & 0xFFFFFFFF + w26 = w9c + x11 := w26 & 0xFF + + w12 := table[x11] + table[x10] = w12 + table[x11] = w12 + + w10_ := w12 + w11_ := dataArr[i] + w13_ := w10_ | w12 + w10b := w10_ & w12 + wX10 := (w13_ + w10b) & 0xFF + ks := table[wX10] + + dataArr[i] = ks ^ w11_ + } + + // 第三轮处理 + w2 := 0xFFFFFFAA + w3 := 0x55 + w4 := 0x33 + w8_2 := (^len(dataArr)) & 0xFFFFFFFF + + for i := 0; i < len(dataArr); i++ { + val := dataArr[i] + w16 := ((val >> 4) & 0x0F) | ((val & 0x0F) << 4) + dataArr[i] = w16 & 0xFF + + if i == len(dataArr)-1 { + dataArr[i] = dataArr[0] ^ w16 + } else { + w7 := dataArr[i] + w16n := dataArr[i+1] + w20 := w7 | w16n + wI2 := w7 & w16n + dataArr[i] = (w20 - wI2) & 0xFF + } + + w13_ := dataArr[i] + t1 := w2 & (w13_ << 1) + t2 := w3 & (w13_ >> 1) + w13_ = t1 | t2 + t3 := w13_ << 2 + t4 := w4 & (w13_ >> 2) + mix := (t3 & 0xFFFFFFCF) | t4 + high := (mix >> 4) & 0x0F + low := mix & 0x0FFFFFFF + mix = ((low << 4) | high) & 0xFFFFFFFF + w13_ = mix ^ w8_2 + + dataArr[i] = w13_ & 0xFF + } + + // 构建输出 + output := make([]byte, 0) + output = append(output, 0x84, 0x04) + output = append(output, byte(a1), byte(a2)) + output = append(output, 0x00, 0x00) + + for _, val := range dataArr { + output = append(output, byte(val)) + } + + return hex.EncodeToString(output) +} + +// GetValue 获取加密值 +func (a *Algo8404) GetValue() map[string]string { + return map[string]string{ + "X-Gorgon": a.getXGorgon(), + "X-Khronos": fmt.Sprintf("%d", a.unix), + "X-SS-REQ-Ticket": fmt.Sprintf("%d", a.unix*1000), + } +} + diff --git a/internal/algos/argus.go b/internal/algos/argus.go new file mode 100644 index 0000000..2acbd6e --- /dev/null +++ b/internal/algos/argus.go @@ -0,0 +1,177 @@ +package algos + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "fmt" + "math/rand" + "net/url" +) + +// Argus Argus加密器 +type Argus struct{} + +// encryptEncPb 加密ProtoBuf +func encryptEncPb(data []byte, l int) []byte { + dataCopy := make([]byte, len(data)) + copy(dataCopy, data) + + xorArray := dataCopy[:8] + + for i := 8; i < l; i++ { + dataCopy[i] ^= xorArray[i%8] + } + + // 反转数组 + for i := 0; i < l/2; i++ { + dataCopy[i], dataCopy[l-1-i] = dataCopy[l-1-i], dataCopy[i] + } + + return dataCopy[:l] +} + +// GetBodyHash 获取Body哈希 +func GetBodyHash(stub string) []byte { + sm3 := NewSM3() + if stub == "" || len(stub) == 0 { + return sm3.Hash(make([]byte, 16))[0:6] + } + stubBytes, _ := hex.DecodeString(stub) + return sm3.Hash(stubBytes)[0:6] +} + +// GetQueryHash 获取Query哈希 +func GetQueryHash(query string) []byte { + sm3 := NewSM3() + if query == "" || len(query) == 0 { + return sm3.Hash(make([]byte, 16))[0:6] + } + return sm3.Hash([]byte(query))[0:6] +} + +// Encrypt Argus加密 +func (a *Argus) Encrypt(xargusBean map[int]interface{}) string { + // 创建ProtoBuf + protobuf := NewProtoBuf(xargusBean).ToBuf() + + // PKCS7填充 + protobuf = AddPKCS7Padding(protobuf, 16) + newLen := len(protobuf) + + signKey := []byte{ + 0xAC, 0x1A, 0xDA, 0xAE, 0x95, 0xA7, 0xAF, 0x94, + 0xA5, 0x11, 0x4A, 0xB3, 0xB3, 0xA9, 0x7D, 0xD8, + 0x00, 0x50, 0xAA, 0x0A, 0x39, 0x31, 0x4C, 0x40, + 0x52, 0x8C, 0xAE, 0xC9, 0x52, 0x56, 0xC2, 0x8C, + } + + sm3Output := []byte{ + 0xFC, 0x78, 0xE0, 0xA9, 0x65, 0x7A, 0x0C, 0x74, + 0x8C, 0xE5, 0x15, 0x59, 0x90, 0x3C, 0xCF, 0x03, + 0x51, 0x0E, 0x51, 0xD3, 0xCF, 0xF2, 0x32, 0xD7, + 0x13, 0x43, 0xE8, 0x8A, 0x32, 0x1C, 0x53, 0x04, + } + + key := sm3Output[:32] + keyList := make([]uint64, 0) + encPb := make([]byte, newLen) + + for i := 0; i < 2; i++ { + val1 := binary.LittleEndian.Uint64(key[i*16 : i*16+8]) + val2 := binary.LittleEndian.Uint64(key[i*16+8 : i*16+16]) + keyList = append(keyList, val1, val2) + } + + for i := 0; i < newLen/16; i++ { + pt := []uint64{ + binary.LittleEndian.Uint64(protobuf[i*16 : i*16+8]), + binary.LittleEndian.Uint64(protobuf[i*16+8 : i*16+16]), + } + ct := SimonEnc(pt, keyList, 0) + binary.LittleEndian.PutUint64(encPb[i*16:i*16+8], ct[0]) + binary.LittleEndian.PutUint64(encPb[i*16+8:i*16+16], ct[1]) + } + + header := []byte{0xF2, 0xF7, 0xFC, 0xFF, 0xF2, 0xF7, 0xFC, 0xFF} + bBuffer := encryptEncPb(append(header, encPb...), newLen+8) + bBuffer = append([]byte{0xA6, 0x6E, 0xAD, 0x9F, 0x77, 0x01, 0xD0, 0x0C, 0x18}, bBuffer...) + bBuffer = append(bBuffer, []byte("ao")...) + + // AES加密 + keyHash := md5.Sum(signKey[:16]) + ivHash := md5.Sum(signKey[16:]) + + block, _ := aes.NewCipher(keyHash[:]) + paddedBuffer := AddPKCS7Padding(bBuffer, aes.BlockSize) + ciphertext := make([]byte, len(paddedBuffer)) + + mode := cipher.NewCBCEncrypter(block, ivHash[:]) + mode.CryptBlocks(ciphertext, paddedBuffer) + + result := append([]byte{0xF2, 0x81}, ciphertext...) + return base64.StdEncoding.EncodeToString(result) +} + +// GetSign 获取签名 +func (a *Argus) GetSign( + queryhash string, + data string, + timestamp int64, + aid int, + licenseID int, + platform int, + secDeviceID string, + sdkVersion string, + sdkVersionInt int, +) string { + paramsDict, _ := url.ParseQuery(queryhash) + + deviceID := "" + if val, ok := paramsDict["device_id"]; ok && len(val) > 0 { + deviceID = val[0] + } + + versionName := "" + if val, ok := paramsDict["version_name"]; ok && len(val) > 0 { + versionName = val[0] + } + + xargusBean := map[int]interface{}{ + 1: 0x20200929 << 1, + 2: 2, + 3: rand.Int63n(0x7FFFFFFF), + 4: fmt.Sprintf("%d", aid), + 5: deviceID, + 6: fmt.Sprintf("%d", licenseID), + 7: versionName, + 8: sdkVersion, + 9: sdkVersionInt, + 10: make([]byte, 8), + 11: platform, + 12: timestamp << 1, + 13: GetBodyHash(data), + 14: GetQueryHash(queryhash), + 15: map[int]interface{}{ + 1: 1, + 2: 1, + 3: 1, + 7: 3348294860, + }, + 16: secDeviceID, + 20: "none", + 21: 738, + 23: map[int]interface{}{ + 1: "NX551J", + 2: 8196, + 4: 2162219008, + }, + 25: 2, + } + + return a.Encrypt(xargusBean) +} + diff --git a/internal/algos/bytebuf.go b/internal/algos/bytebuf.go new file mode 100644 index 0000000..28452b8 --- /dev/null +++ b/internal/algos/bytebuf.go @@ -0,0 +1,53 @@ +package algos + +// ByteBuf 字节缓冲区 +type ByteBuf struct { + mem []byte + dataSize int + pos int +} + +// NewByteBuf 创建新的字节缓冲区 +func NewByteBuf(data []byte, size *int) *ByteBuf { + buf := &ByteBuf{ + pos: 0, + } + + if data != nil { + buf.mem = data + } + + if size != nil { + buf.dataSize = *size + } else if data != nil { + buf.dataSize = len(data) + } else { + panic("Either size or data must be provided") + } + + return buf +} + +// Data 获取数据 +func (b *ByteBuf) Data() []byte { + return b.mem +} + +// Size 获取大小 +func (b *ByteBuf) Size() int { + return b.dataSize +} + +// RemovePadding 移除填充 +func (b *ByteBuf) RemovePadding() []byte { + paddingSize := PKCS7DataLength(b.mem, b.dataSize, 16) + if paddingSize == 0 { + return b.mem[:b.dataSize] + } + b.dataSize = paddingSize + dst := make([]byte, b.dataSize) + copy(dst, b.mem[:b.dataSize]) + b.mem = dst + return b.mem +} + diff --git a/internal/algos/gorgon.go b/internal/algos/gorgon.go new file mode 100644 index 0000000..549d858 --- /dev/null +++ b/internal/algos/gorgon.go @@ -0,0 +1,137 @@ +package algos + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "strconv" +) + +// Gorgon Gorgon加密器 +type Gorgon struct { + unix int64 + params string + xSSStub string + cookies string +} + +// NewGorgon 创建新的Gorgon实例 +func NewGorgon(params string, unix int64, xSSStub string, cookies string) *Gorgon { + if xSSStub == "" { + xSSStub = "00000000000000000000000000000000" + } + return &Gorgon{ + unix: unix, + params: params, + xSSStub: xSSStub, + cookies: cookies, + } +} + +// hash MD5哈希 +func (g *Gorgon) hash(data string) string { + hasher := md5.New() + hasher.Write([]byte(data)) + return hex.EncodeToString(hasher.Sum(nil)) +} + +// getBaseString 获取基础字符串 +func (g *Gorgon) getBaseString() string { + baseStr := g.hash(g.params) + baseStr = baseStr + g.xSSStub + + if g.cookies != "" { + baseStr = baseStr + g.hash(g.cookies) + } else { + baseStr = baseStr + "00000000000000000000000000000000" + } + + return baseStr +} + +// rbitAlgorithm 位反转算法 +func (g *Gorgon) rbitAlgorithm(num int) int { + result := "" + tmpString := fmt.Sprintf("%08b", num) + + for i := 0; i < 8; i++ { + result = result + string(tmpString[7-i]) + } + + val, _ := strconv.ParseInt(result, 2, 64) + return int(val) +} + +// hexString 转十六进制字符串 +func (g *Gorgon) hexString(num int) string { + tmpString := fmt.Sprintf("%x", num) + if len(tmpString) < 2 { + tmpString = "0" + tmpString + } + return tmpString +} + +// reverse 反转 +func (g *Gorgon) reverse(num int) int { + tmpString := g.hexString(num) + val, _ := strconv.ParseInt(tmpString[1:]+tmpString[:1], 16, 64) + return int(val) +} + +// encrypt 加密 +func (g *Gorgon) encrypt(data string) map[string]string { + length := 0x14 + key := []int{ + 0xDF, 0x77, 0xB9, 0x40, 0xB9, 0x9B, 0x84, 0x83, + 0xD1, 0xB9, 0xCB, 0xD1, 0xF7, 0xC2, 0xB9, 0x85, + 0xC3, 0xD0, 0xFB, 0xC3, + } + + paramList := make([]int, 0) + for i := 0; i < 12; i += 4 { + temp := data[8*i : 8*(i+1)] + for j := 0; j < 4; j++ { + val, _ := strconv.ParseInt(temp[j*2:(j+1)*2], 16, 64) + paramList = append(paramList, int(val)) + } + } + + paramList = append(paramList, 0x0, 0x6, 0xB, 0x1C) + + H := int(g.unix) + paramList = append(paramList, (H&0xFF000000)>>24) + paramList = append(paramList, (H&0x00FF0000)>>16) + paramList = append(paramList, (H&0x0000FF00)>>8) + paramList = append(paramList, (H&0x000000FF)>>0) + + eorResultList := make([]int, len(paramList)) + for i, val := range paramList { + eorResultList[i] = val ^ key[i] + } + + for i := 0; i < length; i++ { + C := g.reverse(eorResultList[i]) + D := eorResultList[(i+1)%length] + E := C ^ D + F := g.rbitAlgorithm(E) + H := ((F ^ 0xFFFFFFFF) ^ length) & 0xFF + eorResultList[i] = H + } + + result := "" + for _, param := range eorResultList { + result += g.hexString(param) + } + + return map[string]string{ + "x-ss-req-ticket": strconv.FormatInt(g.unix*1000, 10), + "x-khronos": strconv.FormatInt(g.unix, 10), + "x-gorgon": fmt.Sprintf("0404b0d30000%s", result), + } +} + +// GetValue 获取加密值 +func (g *Gorgon) GetValue() map[string]string { + return g.encrypt(g.getBaseString()) +} + diff --git a/internal/algos/ladon.go b/internal/algos/ladon.go new file mode 100644 index 0000000..a60b631 --- /dev/null +++ b/internal/algos/ladon.go @@ -0,0 +1,121 @@ +package algos + +import ( + "crypto/md5" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "fmt" +) + +// validate 验证并限制在64位范围内 +func validate(num uint64) uint64 { + return num & 0xFFFFFFFFFFFFFFFF +} + +// ror 右循环移位 +func ror(value uint64, count int) uint64 { + nbits := 64 + count %= nbits + low := (value << (nbits - count)) + value = (value >> count) + value = value | low + return value +} + +// encryptLadonInput 加密Ladon输入 +func encryptLadonInput(hashTable []byte, inputData []byte) []byte { + data0 := binary.LittleEndian.Uint64(inputData[:8]) + data1 := binary.LittleEndian.Uint64(inputData[8:]) + + for i := 0; i < 0x22; i++ { + hash := binary.LittleEndian.Uint64(hashTable[i*8 : (i+1)*8]) + data1 = validate(hash ^ (data0 + ((data1 >> 8) | (data1 << (64 - 8))))) + data0 = validate(data1 ^ ((data0 >> 0x3D) | (data0 << (64 - 0x3D)))) + } + + outputData := make([]byte, 26) + binary.LittleEndian.PutUint64(outputData[:8], data0) + binary.LittleEndian.PutUint64(outputData[8:], data1) + + return outputData +} + +// encryptLadon 加密Ladon +func encryptLadon(md5Hex []byte, data []byte, size int) []byte { + hashTable := make([]byte, 272+16) + copy(hashTable[:32], md5Hex) + + temp := make([]uint64, 0) + for i := 0; i < 4; i++ { + temp = append(temp, binary.LittleEndian.Uint64(hashTable[i*8:(i+1)*8])) + } + + bufferB0 := temp[0] + bufferB8 := temp[1] + temp = temp[2:] + + for i := 0; i < 0x22; i++ { + x9 := bufferB0 + x8 := bufferB8 + x8 = validate(ror(x8, 8)) + x8 = validate(x8 + x9) + x8 = validate(x8 ^ uint64(i)) + temp = append(temp, x8) + x8 = validate(x8 ^ ror(x9, 61)) + binary.LittleEndian.PutUint64(hashTable[(i+1)*8:], x8) + bufferB0 = x8 + bufferB8 = temp[0] + temp = temp[1:] + } + + newSize := PaddingSize(size) + + input := make([]byte, newSize) + copy(input[:size], data) + PKCS7PadBuffer(input, size, newSize, 16) + + output := make([]byte, newSize) + for i := 0; i < newSize/16; i++ { + result := encryptLadonInput(hashTable, input[i*16:(i+1)*16]) + copy(output[i*16:(i+1)*16], result) + } + + return output +} + +// LadonEncrypt Ladon加密主函数 +func LadonEncrypt(khronos int64, lcID int, aid int, randomBytes []byte) string { + data := fmt.Sprintf("%d-%d-%d", khronos, lcID, aid) + + if randomBytes == nil { + randomBytes = make([]byte, 4) + rand.Read(randomBytes) + } + + keygen := append(randomBytes, []byte(fmt.Sprintf("%d", aid))...) + hasher := md5.New() + hasher.Write(keygen) + md5Hex := hex.EncodeToString(hasher.Sum(nil)) + + size := len(data) + newSize := PaddingSize(size) + + output := make([]byte, newSize+4) + copy(output[:4], randomBytes) + + encrypted := encryptLadon([]byte(md5Hex), []byte(data), size) + copy(output[4:], encrypted) + + return base64.StdEncoding.EncodeToString(output) +} + +// Ladon Ladon加密器 +type Ladon struct{} + +// Encrypt Ladon加密 +func (l *Ladon) Encrypt(xKhronos int64, lcID int, aid int) string { + return LadonEncrypt(xKhronos, lcID, aid, nil) +} + diff --git a/internal/algos/pkcs7.go b/internal/algos/pkcs7.go new file mode 100644 index 0000000..b32f1e3 --- /dev/null +++ b/internal/algos/pkcs7.go @@ -0,0 +1,78 @@ +package algos + +// PKCS7DataLength 获取PKCS7填充数据的实际长度 +func PKCS7DataLength(buffer []byte, bufferSize, modulus int) int { + if bufferSize%modulus != 0 || bufferSize < modulus { + return 0 + } + + paddingValue := buffer[bufferSize-1] + if paddingValue < 1 || int(paddingValue) > modulus { + return 0 + } + + if bufferSize < int(paddingValue)+1 { + return 0 + } + + count := 1 + bufferSize-- + + for i := count; i < int(paddingValue); i++ { + bufferSize-- + if buffer[bufferSize] != paddingValue { + return 0 + } + } + + return bufferSize +} + +// PKCS7PadBuffer 对缓冲区进行PKCS7填充 +func PKCS7PadBuffer(buffer []byte, dataLength, bufferSize, modulus int) int { + padByte := modulus - (dataLength % modulus) + if dataLength+padByte > bufferSize { + return -padByte + } + + for i := 0; i < padByte; i++ { + buffer[dataLength+i] = byte(padByte) + } + + return padByte +} + +// PaddingSize 计算填充后的大小 +func PaddingSize(size int) int { + mod := size % 16 + if mod > 0 { + return size + (16 - mod) + } + return size +} + +// RemovePKCS7Padding 移除PKCS7填充 +func RemovePKCS7Padding(data []byte) []byte { + length := len(data) + if length == 0 { + return data + } + + paddingLen := int(data[length-1]) + if paddingLen > length || paddingLen > 16 { + return data + } + + return data[:length-paddingLen] +} + +// AddPKCS7Padding 添加PKCS7填充 +func AddPKCS7Padding(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padText := make([]byte, padding) + for i := range padText { + padText[i] = byte(padding) + } + return append(data, padText...) +} + diff --git a/internal/algos/protobuf.go b/internal/algos/protobuf.go new file mode 100644 index 0000000..24d6540 --- /dev/null +++ b/internal/algos/protobuf.go @@ -0,0 +1,404 @@ +package algos + +import ( + "fmt" +) + +// ProtoFieldType 协议字段类型 +type ProtoFieldType int + +const ( + ProtoFieldTypeVARINT ProtoFieldType = 0 + ProtoFieldTypeINT64 ProtoFieldType = 1 + ProtoFieldTypeSTRING ProtoFieldType = 2 + ProtoFieldTypeGROUPSTART ProtoFieldType = 3 + ProtoFieldTypeGROUPEND ProtoFieldType = 4 + ProtoFieldTypeINT32 ProtoFieldType = 5 + ProtoFieldTypeERROR1 ProtoFieldType = 6 + ProtoFieldTypeERROR2 ProtoFieldType = 7 +) + +// ProtoField 协议字段 +type ProtoField struct { + Idx int + Type ProtoFieldType + Val interface{} +} + +// NewProtoField 创建新的协议字段 +func NewProtoField(idx int, fieldType ProtoFieldType, val interface{}) *ProtoField { + return &ProtoField{ + Idx: idx, + Type: fieldType, + Val: val, + } +} + +// IsAsciiStr 检查是否为ASCII字符串 +func (pf *ProtoField) IsAsciiStr() bool { + if data, ok := pf.Val.([]byte); ok { + for _, b := range data { + if b < 0x20 || b > 0x7E { + return false + } + } + return true + } + return false +} + +// ProtoReader 协议读取器 +type ProtoReader struct { + data []byte + pos int +} + +// NewProtoReader 创建新的协议读取器 +func NewProtoReader(data []byte) *ProtoReader { + return &ProtoReader{ + data: data, + pos: 0, + } +} + +// Seek 设置位置 +func (pr *ProtoReader) Seek(pos int) { + pr.pos = pos +} + +// IsRemain 检查是否还有剩余数据 +func (pr *ProtoReader) IsRemain(length int) bool { + return pr.pos+length <= len(pr.data) +} + +// Read0 读取一个字节 +func (pr *ProtoReader) Read0() byte { + if !pr.IsRemain(1) { + panic("buffer overflow") + } + ret := pr.data[pr.pos] + pr.pos++ + return ret & 0xFF +} + +// Read 读取指定长度的字节 +func (pr *ProtoReader) Read(length int) []byte { + if !pr.IsRemain(length) { + panic("buffer overflow") + } + ret := pr.data[pr.pos : pr.pos+length] + pr.pos += length + return ret +} + +// ReadInt32 读取32位整数 +func (pr *ProtoReader) ReadInt32() uint32 { + bytes := pr.Read(4) + return uint32(bytes[0]) | uint32(bytes[1])<<8 | uint32(bytes[2])<<16 | uint32(bytes[3])<<24 +} + +// ReadInt64 读取64位整数 +func (pr *ProtoReader) ReadInt64() uint64 { + bytes := pr.Read(8) + return uint64(bytes[0]) | uint64(bytes[1])<<8 | uint64(bytes[2])<<16 | uint64(bytes[3])<<24 | + uint64(bytes[4])<<32 | uint64(bytes[5])<<40 | uint64(bytes[6])<<48 | uint64(bytes[7])<<56 +} + +// ReadVarint 读取可变长度整数 +func (pr *ProtoReader) ReadVarint() uint64 { + vint := uint64(0) + n := 0 + for { + b := pr.Read0() + vint |= uint64(b&0x7F) << (7 * n) + if b < 0x80 { + break + } + n++ + } + return vint +} + +// ReadString 读取字符串 +func (pr *ProtoReader) ReadString() []byte { + length := pr.ReadVarint() + return pr.Read(int(length)) +} + +// ProtoWriter 协议写入器 +type ProtoWriter struct { + data []byte +} + +// NewProtoWriter 创建新的协议写入器 +func NewProtoWriter() *ProtoWriter { + return &ProtoWriter{ + data: make([]byte, 0), + } +} + +// Write0 写入一个字节 +func (pw *ProtoWriter) Write0(b byte) { + pw.data = append(pw.data, b&0xFF) +} + +// Write 写入字节数组 +func (pw *ProtoWriter) Write(bytes []byte) { + pw.data = append(pw.data, bytes...) +} + +// WriteInt32 写入32位整数 +func (pw *ProtoWriter) WriteInt32(val uint32) { + bs := []byte{ + byte(val & 0xFF), + byte((val >> 8) & 0xFF), + byte((val >> 16) & 0xFF), + byte((val >> 24) & 0xFF), + } + pw.Write(bs) +} + +// WriteInt64 写入64位整数 +func (pw *ProtoWriter) WriteInt64(val uint64) { + bs := []byte{ + byte(val & 0xFF), + byte((val >> 8) & 0xFF), + byte((val >> 16) & 0xFF), + byte((val >> 24) & 0xFF), + byte((val >> 32) & 0xFF), + byte((val >> 40) & 0xFF), + byte((val >> 48) & 0xFF), + byte((val >> 56) & 0xFF), + } + pw.Write(bs) +} + +// WriteVarint 写入可变长度整数 +func (pw *ProtoWriter) WriteVarint(vint uint64) { + vint = vint & 0xFFFFFFFF + for vint > 0x80 { + pw.Write0(byte((vint & 0x7F) | 0x80)) + vint >>= 7 + } + pw.Write0(byte(vint & 0x7F)) +} + +// WriteString 写入字符串 +func (pw *ProtoWriter) WriteString(bytes []byte) { + pw.WriteVarint(uint64(len(bytes))) + pw.Write(bytes) +} + +// ToBytes 转换为字节数组 +func (pw *ProtoWriter) ToBytes() []byte { + return pw.data +} + +// ProtoBuf 协议缓冲区 +type ProtoBuf struct { + fields []*ProtoField +} + +// NewProtoBuf 创建新的协议缓冲区 +func NewProtoBuf(data interface{}) *ProtoBuf { + pb := &ProtoBuf{ + fields: make([]*ProtoField, 0), + } + + if data != nil { + switch v := data.(type) { + case []byte: + if len(v) > 0 { + pb.parseBuf(v) + } + case map[int]interface{}: + if len(v) > 0 { + pb.parseDict(v) + } + default: + panic(fmt.Sprintf("unsupported type(%T) to protobuf", data)) + } + } + + return pb +} + +// parseBuf 解析字节数组 +func (pb *ProtoBuf) parseBuf(bytes []byte) { + reader := NewProtoReader(bytes) + for reader.IsRemain(1) { + key := reader.ReadVarint() + fieldType := ProtoFieldType(key & 0x7) + fieldIdx := int(key >> 3) + + if fieldIdx == 0 { + break + } + + switch fieldType { + case ProtoFieldTypeINT32: + pb.Put(NewProtoField(fieldIdx, fieldType, reader.ReadInt32())) + case ProtoFieldTypeINT64: + pb.Put(NewProtoField(fieldIdx, fieldType, reader.ReadInt64())) + case ProtoFieldTypeVARINT: + pb.Put(NewProtoField(fieldIdx, fieldType, reader.ReadVarint())) + case ProtoFieldTypeSTRING: + pb.Put(NewProtoField(fieldIdx, fieldType, reader.ReadString())) + default: + panic(fmt.Sprintf("parse protobuf error, unexpected field type: %d", fieldType)) + } + } +} + +// ToBuf 转换为字节数组 +func (pb *ProtoBuf) ToBuf() []byte { + writer := NewProtoWriter() + for _, field := range pb.fields { + key := uint64((field.Idx << 3) | int(field.Type&7)) + writer.WriteVarint(key) + + switch field.Type { + case ProtoFieldTypeINT32: + writer.WriteInt32(field.Val.(uint32)) + case ProtoFieldTypeINT64: + writer.WriteInt64(field.Val.(uint64)) + case ProtoFieldTypeVARINT: + writer.WriteVarint(field.Val.(uint64)) + case ProtoFieldTypeSTRING: + writer.WriteString(field.Val.([]byte)) + default: + panic(fmt.Sprintf("encode to protobuf error, unexpected field type: %d", field.Type)) + } + } + return writer.ToBytes() +} + +// Get 获取字段 +func (pb *ProtoBuf) Get(idx int) *ProtoField { + for _, field := range pb.fields { + if field.Idx == idx { + return field + } + } + return nil +} + +// GetList 获取字段列表 +func (pb *ProtoBuf) GetList(idx int) []*ProtoField { + result := make([]*ProtoField, 0) + for _, field := range pb.fields { + if field.Idx == idx { + result = append(result, field) + } + } + return result +} + +// GetInt 获取整数 +func (pb *ProtoBuf) GetInt(idx int) uint64 { + pf := pb.Get(idx) + if pf == nil { + return 0 + } + + switch pf.Type { + case ProtoFieldTypeINT32: + return uint64(pf.Val.(uint32)) + case ProtoFieldTypeINT64: + return pf.Val.(uint64) + case ProtoFieldTypeVARINT: + return pf.Val.(uint64) + default: + panic(fmt.Sprintf("getInt(%d) -> %d", idx, pf.Type)) + } +} + +// GetBytes 获取字节数组 +func (pb *ProtoBuf) GetBytes(idx int) []byte { + pf := pb.Get(idx) + if pf == nil { + return nil + } + + if pf.Type == ProtoFieldTypeSTRING { + return pf.Val.([]byte) + } + + panic(fmt.Sprintf("getBytes(%d) -> %d", idx, pf.Type)) +} + +// GetUTF8 获取UTF8字符串 +func (pb *ProtoBuf) GetUTF8(idx int) string { + bs := pb.GetBytes(idx) + if bs == nil { + return "" + } + return string(bs) +} + +// GetProtoBuf 获取嵌套的ProtoBuf +func (pb *ProtoBuf) GetProtoBuf(idx int) *ProtoBuf { + bs := pb.GetBytes(idx) + if bs == nil { + return nil + } + return NewProtoBuf(bs) +} + +// Put 添加字段 +func (pb *ProtoBuf) Put(field *ProtoField) { + pb.fields = append(pb.fields, field) +} + +// PutInt32 添加32位整数 +func (pb *ProtoBuf) PutInt32(idx int, val uint32) { + pb.Put(NewProtoField(idx, ProtoFieldTypeINT32, val)) +} + +// PutInt64 添加64位整数 +func (pb *ProtoBuf) PutInt64(idx int, val uint64) { + pb.Put(NewProtoField(idx, ProtoFieldTypeINT64, val)) +} + +// PutVarint 添加可变长度整数 +func (pb *ProtoBuf) PutVarint(idx int, val uint64) { + pb.Put(NewProtoField(idx, ProtoFieldTypeVARINT, val)) +} + +// PutBytes 添加字节数组 +func (pb *ProtoBuf) PutBytes(idx int, data []byte) { + pb.Put(NewProtoField(idx, ProtoFieldTypeSTRING, data)) +} + +// PutUTF8 添加UTF8字符串 +func (pb *ProtoBuf) PutUTF8(idx int, data string) { + pb.Put(NewProtoField(idx, ProtoFieldTypeSTRING, []byte(data))) +} + +// PutProtoBuf 添加嵌套的ProtoBuf +func (pb *ProtoBuf) PutProtoBuf(idx int, data *ProtoBuf) { + pb.Put(NewProtoField(idx, ProtoFieldTypeSTRING, data.ToBuf())) +} + +// parseDict 解析字典 +func (pb *ProtoBuf) parseDict(data map[int]interface{}) { + for k, v := range data { + switch val := v.(type) { + case int: + pb.PutVarint(k, uint64(val)) + case uint64: + pb.PutVarint(k, val) + case int64: + pb.PutVarint(k, uint64(val)) + case string: + pb.PutUTF8(k, val) + case []byte: + pb.PutBytes(k, val) + case map[int]interface{}: + pb.PutProtoBuf(k, NewProtoBuf(val)) + default: + panic(fmt.Sprintf("unsupported type(%T) to protobuf", v)) + } + } +} + diff --git a/internal/algos/signer.go b/internal/algos/signer.go new file mode 100644 index 0000000..53df54d --- /dev/null +++ b/internal/algos/signer.go @@ -0,0 +1,219 @@ +package algos + +import ( + "fmt" + "net/url" + "regexp" + "strconv" + "sync" + "time" +) + +// getTimestamp 获取时间戳(辅助函数) +func getTimestamp(unix *int64) int64 { + if unix == nil { + return time.Now().Unix() + } + return *unix +} + +// Sign2 签名方法2(Gorgon 或 8404) +func Sign2( + params string, + xSSStub string, + secDeviceID string, + cookie string, + aid int, + licenseID int, + sdkVersionStr string, + sdkVersion int, + platform int, + unix *int64, +) map[string]string { + timestamp := getTimestamp(unix) + + // 根据当前算法选择生成签名 + currentAlgo := GlobalAlgorithmSwitcher.GetCurrentAlgorithm() + if currentAlgo == "8404" { + algo := NewAlgo8404(params, timestamp, xSSStub) + return algo.GetValue() + } + + // 默认使用 Gorgon + gorgon := NewGorgon(params, timestamp, xSSStub, cookie) + return gorgon.GetValue() +} + +// Sign4 签名方法4(Gorgon + Ladon + Argus) +func Sign4( + params string, + xSSStub string, + secDeviceID string, + cookie string, + aid int, + licenseID int, + sdkVersionStr string, + sdkVersion int, + platform int, + unix *int64, +) map[string]string { + timestamp := getTimestamp(unix) + + // 根据当前算法选择生成基础签名 + var result map[string]string + currentAlgo := GlobalAlgorithmSwitcher.GetCurrentAlgorithm() + if currentAlgo == "8404" { + algo := NewAlgo8404(params, timestamp, xSSStub) + result = algo.GetValue() + } else { + // 默认使用 Gorgon + gorgon := NewGorgon(params, timestamp, xSSStub, cookie) + result = gorgon.GetValue() + } + + // 添加 Ladon 和 Argus 签名 + ladon := &Ladon{} + result["x-ladon"] = ladon.Encrypt(timestamp, licenseID, aid) + + argus := &Argus{} + result["x-argus"] = argus.GetSign( + params, + xSSStub, + timestamp, + aid, + licenseID, + platform, + secDeviceID, + sdkVersionStr, + sdkVersion, + ) + + return result +} + +// SignatureInterface 签名接口 +type SignatureInterface interface { + Sign(params, xSSStub, secDeviceID, cookie string, aid, licenseID int, sdkVersionStr string, sdkVersion, platform int, unix *int64) map[string]string +} + +// SignatureSwitcher 签名方法切换器 +type SignatureSwitcher struct { + mu sync.RWMutex + currentMethod string + methods map[string]SignatureInterface +} + +// NewSignatureSwitcher 创建签名切换器 +func NewSignatureSwitcher() *SignatureSwitcher { + switcher := &SignatureSwitcher{ + methods: make(map[string]SignatureInterface), + } + + // 注册所有签名方法 + switcher.methods["sign2"] = &Sign2Method{} + switcher.methods["sign4"] = &Sign4Method{} + + // 设置默认签名方法为sign2 + switcher.currentMethod = "sign2" + + return switcher +} + +// SwitchSignature 切换签名方法 +func (s *SignatureSwitcher) SwitchSignature(method string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, exists := s.methods[method]; !exists { + return fmt.Errorf("不支持的签名方法: %s", method) + } + + s.currentMethod = method + return nil +} + +// GetCurrentMethod 获取当前签名方法 +func (s *SignatureSwitcher) GetCurrentMethod() string { + s.mu.RLock() + defer s.mu.RUnlock() + return s.currentMethod +} + +// Sign 使用当前签名方法进行签名 +func (s *SignatureSwitcher) Sign(urlStr string, headers map[string]string) map[string]string { + return SignFromURL(s.GetCurrentMethod(), urlStr, headers) +} + +// SignFromParams 使用当前签名方法进行签名 +func (s *SignatureSwitcher) SignFromParams(params, xSSStub, secDeviceID, cookie string, aid, licenseID int, sdkVersionStr string, sdkVersion, platform int, unix *int64) map[string]string { + s.mu.RLock() + method := s.methods[s.currentMethod] + s.mu.RUnlock() + + return method.Sign(params, xSSStub, secDeviceID, cookie, aid, licenseID, sdkVersionStr, sdkVersion, platform, unix) +} + +// Sign2Method Sign2签名方法实现 +type Sign2Method struct{} + +func (s *Sign2Method) Sign(params, xSSStub, secDeviceID, cookie string, aid, licenseID int, sdkVersionStr string, sdkVersion, platform int, unix *int64) map[string]string { + return Sign2(params, xSSStub, secDeviceID, cookie, aid, licenseID, sdkVersionStr, sdkVersion, platform, unix) +} + +// Sign4Method Sign4签名方法实现 +type Sign4Method struct{} + +func (s *Sign4Method) Sign(params, xSSStub, secDeviceID, cookie string, aid, licenseID int, sdkVersionStr string, sdkVersion, platform int, unix *int64) map[string]string { + return Sign4(params, xSSStub, secDeviceID, cookie, aid, licenseID, sdkVersionStr, sdkVersion, platform, unix) +} + +// GlobalSignatureSwitcher 全局签名切换器实例 +var GlobalSignatureSwitcher = NewSignatureSwitcher() + +// SignFromURL 从 URL 和 headers 中提取参数并生成签名 +func SignFromURL(method, urlStr string, headers map[string]string) map[string]string { + // 解析 URL + u, err := url.Parse(urlStr) + if err != nil { + return make(map[string]string) + } + + // 提取查询参数 + params := u.RawQuery + xSSStub := headers["x-ss-stub"] + cookie := headers["Cookie"] + + // 从 URL 中提取 device_id 和 aid + deviceIDRegex := regexp.MustCompile(`device_id=(\d+)`) + aidRegex := regexp.MustCompile(`aid=(\d+)`) + + deviceIDMatch := deviceIDRegex.FindStringSubmatch(urlStr) + aidMatch := aidRegex.FindStringSubmatch(urlStr) + + secDeviceID := "" + aid := 1967 + if len(deviceIDMatch) > 1 { + secDeviceID = deviceIDMatch[1] + } + if len(aidMatch) > 1 { + aid, _ = strconv.Atoi(aidMatch[1]) + } + + // 使用固定参数 + licenseID := 1611921764 + sdkVersionStr := "v04.06.01-ml-android" + sdkVersion := 135004672 + platform := 0 + + // 根据方法调用对应的签名函数 + if method == "sign4" { + return Sign4(params, xSSStub, secDeviceID, cookie, aid, licenseID, sdkVersionStr, sdkVersion, platform, nil) + } + return Sign2(params, xSSStub, secDeviceID, cookie, aid, licenseID, sdkVersionStr, sdkVersion, platform, nil) +} + +// Sign2Wrapper 硬编码使用 Sign2(从 URL 和 headers 生成签名) +func Sign2Wrapper(urlStr string, headers map[string]string) map[string]string { + return SignFromURL("sign2", urlStr, headers) +} + diff --git a/internal/algos/simon.go b/internal/algos/simon.go new file mode 100644 index 0000000..432ecc4 --- /dev/null +++ b/internal/algos/simon.go @@ -0,0 +1,85 @@ +package algos + +// getBit 获取指定位的值 +func getBit(val uint64, pos int) uint64 { + if val&(1<> (64 - n)) + return r & 0xFFFFFFFFFFFFFFFF +} + +// rotateRight64 64位右循环移位 +func rotateRight64(v uint64, n int) uint64 { + r := (v << (64 - n)) | (v >> n) + return r & 0xFFFFFFFFFFFFFFFF +} + +// keyExpansion 密钥扩展 +func keyExpansion(key []uint64) []uint64 { + expandedKey := make([]uint64, 72) + copy(expandedKey, key[:4]) + + var tmp uint64 + for i := 4; i < 72; i++ { + tmp = rotateRight64(expandedKey[i-1], 3) + tmp = tmp ^ expandedKey[i-3] + tmp = tmp ^ rotateRight64(tmp, 1) + expandedKey[i] = (^expandedKey[i-4]) ^ tmp ^ getBit(0x3DC94C3A046D678B, (i-4)%62) ^ 3 + } + return expandedKey +} + +// SimonEnc Simon加密 +func SimonEnc(pt []uint64, k []uint64, c int) []uint64 { + key := make([]uint64, 72) + copy(key[:4], k[:4]) + key = keyExpansion(key) + + xi := pt[0] + xi1 := pt[1] + + var tmp, f uint64 + for i := 0; i < 72; i++ { + tmp = xi1 + if c == 1 { + f = rotateLeft64(xi1, 1) + } else { + f = rotateLeft64(xi1, 1) & rotateLeft64(xi1, 8) + } + xi1 = xi ^ f ^ rotateLeft64(xi1, 2) ^ key[i] + xi = tmp + } + + return []uint64{xi, xi1} +} + +// SimonDec Simon解密 +func SimonDec(ct []uint64, k []uint64, c int) []uint64 { + key := make([]uint64, 72) + copy(key[:4], k[:4]) + key = keyExpansion(key) + + xi := ct[0] + xi1 := ct[1] + + var tmp, f uint64 + for i := 71; i >= 0; i-- { + tmp = xi + if c == 1 { + f = rotateLeft64(xi, 1) + } else { + f = rotateLeft64(xi, 1) & rotateLeft64(xi, 8) + } + xi = xi1 ^ f ^ rotateLeft64(xi, 2) ^ key[i] + xi1 = tmp + } + + return []uint64{xi, xi1} +} + diff --git a/internal/algos/sm3.go b/internal/algos/sm3.go new file mode 100644 index 0000000..42581e7 --- /dev/null +++ b/internal/algos/sm3.go @@ -0,0 +1,154 @@ +package algos + +// SM3 SM3哈希算法实现 +type SM3 struct { + IV []uint32 + TJ []uint32 +} + +// NewSM3 创建SM3实例 +func NewSM3() *SM3 { + return &SM3{ + IV: []uint32{1937774191, 1226093241, 388252375, 3666478592, 2842636476, 372324522, 3817729613, 2969243214}, + TJ: []uint32{ + 2043430169, 2043430169, 2043430169, 2043430169, 2043430169, 2043430169, 2043430169, 2043430169, + 2043430169, 2043430169, 2043430169, 2043430169, 2043430169, 2043430169, 2043430169, 2043430169, + 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, + 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, + 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, + 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, + 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, + 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, 2055708042, + }, + } +} + +// rotateLeft 循环左移 +func (sm3 *SM3) rotateLeft(a uint32, k int) uint32 { + k = k % 32 + return ((a << uint(k)) & 0xFFFFFFFF) | ((a & 0xFFFFFFFF) >> uint(32-k)) +} + +// FFJ FF函数 +func (sm3 *SM3) FFJ(X, Y, Z uint32, j int) uint32 { + if j >= 0 && j < 16 { + return X ^ Y ^ Z + } else if j >= 16 && j < 64 { + return (X & Y) | (X & Z) | (Y & Z) + } + return 0 +} + +// GGJ GG函数 +func (sm3 *SM3) GGJ(X, Y, Z uint32, j int) uint32 { + if j >= 0 && j < 16 { + return X ^ Y ^ Z + } else if j >= 16 && j < 64 { + return (X & Y) | ((^X) & Z) + } + return 0 +} + +// P0 压缩函数P0 +func (sm3 *SM3) P0(X uint32) uint32 { + return X ^ sm3.rotateLeft(X, 9) ^ sm3.rotateLeft(X, 17) +} + +// P1 压缩函数P1 +func (sm3 *SM3) P1(X uint32) uint32 { + return X ^ sm3.rotateLeft(X, 15) ^ sm3.rotateLeft(X, 23) +} + +// CF 压缩函数 +func (sm3 *SM3) CF(Vi []uint32, Bi []byte) []uint32 { + W := make([]uint32, 68) + for i := 0; i < 16; i++ { + weight := uint32(0x1000000) + data := uint32(0) + for k := i * 4; k < (i+1)*4; k++ { + data = data + uint32(Bi[k])*weight + weight = weight / 0x100 + } + W[i] = data + } + + for j := 16; j < 68; j++ { + W[j] = sm3.P1(W[j-16]^W[j-9]^sm3.rotateLeft(W[j-3], 15)) ^ sm3.rotateLeft(W[j-13], 7) ^ W[j-6] + } + + W1 := make([]uint32, 64) + for j := 0; j < 64; j++ { + W1[j] = W[j] ^ W[j+4] + } + + A, B, C, D, E, F, G, H := Vi[0], Vi[1], Vi[2], Vi[3], Vi[4], Vi[5], Vi[6], Vi[7] + + for j := 0; j < 64; j++ { + SS1 := sm3.rotateLeft((sm3.rotateLeft(A, 12)+E+sm3.rotateLeft(sm3.TJ[j], j%32))&0xFFFFFFFF, 7) + SS2 := SS1 ^ sm3.rotateLeft(A, 12) + TT1 := (sm3.FFJ(A, B, C, j) + D + SS2 + W1[j]) & 0xFFFFFFFF + TT2 := (sm3.GGJ(E, F, G, j) + H + SS1 + W[j]) & 0xFFFFFFFF + D = C + C = sm3.rotateLeft(B, 9) + B = A + A = TT1 + H = G + G = sm3.rotateLeft(F, 19) + F = E + E = sm3.P0(TT2) + } + + V := make([]uint32, 8) + V[0] = A ^ Vi[0] + V[1] = B ^ Vi[1] + V[2] = C ^ Vi[2] + V[3] = D ^ Vi[3] + V[4] = E ^ Vi[4] + V[5] = F ^ Vi[5] + V[6] = G ^ Vi[6] + V[7] = H ^ Vi[7] + + return V +} + +// Hash 计算SM3哈希 +func (sm3 *SM3) Hash(msg []byte) []byte { + msgLen := len(msg) + reserve1 := msgLen % 64 + paddingLen := 0 + if reserve1 > 55 { + paddingLen = 64 - reserve1 + 64 + } else { + paddingLen = 64 - reserve1 + } + + msgPadding := make([]byte, msgLen+paddingLen) + copy(msgPadding, msg) + msgPadding[msgLen] = 0x80 + + lengthBits := uint64(msgLen) * 8 + for i := 0; i < 8; i++ { + msgPadding[len(msgPadding)-1-i] = byte(lengthBits >> uint(8*i)) + } + + groupCount := len(msgPadding) / 64 + + V := make([]uint32, len(sm3.IV)) + copy(V, sm3.IV) + + for i := 0; i < groupCount; i++ { + B := msgPadding[i*64 : (i+1)*64] + V = sm3.CF(V, B) + } + + result := make([]byte, 32) + for i := 0; i < 8; i++ { + result[i*4] = byte((V[i] >> 24) & 0xFF) + result[i*4+1] = byte((V[i] >> 16) & 0xFF) + result[i*4+2] = byte((V[i] >> 8) & 0xFF) + result[i*4+3] = byte(V[i] & 0xFF) + } + + return result +} + diff --git a/internal/algos/switcher.go b/internal/algos/switcher.go new file mode 100644 index 0000000..aed0c8f --- /dev/null +++ b/internal/algos/switcher.go @@ -0,0 +1,159 @@ +package algos + +import ( + "fmt" + "strings" + "sync" + "time" +) + +// AlgorithmInterface 算法接口 +type AlgorithmInterface interface { + GetAlgorithmName() string + GenerateHeaders(url string, headers map[string]string) (map[string]string, error) +} + +// AlgorithmSwitcher 算法切换器 +type AlgorithmSwitcher struct { + mu sync.RWMutex + currentAlgorithm string + algorithms map[string]AlgorithmInterface +} + +// NewAlgorithmSwitcher 创建算法切换器 +func NewAlgorithmSwitcher() *AlgorithmSwitcher { + switcher := &AlgorithmSwitcher{ + algorithms: make(map[string]AlgorithmInterface), + } + + // 注册所有算法 + switcher.algorithms["gorgon"] = &GorgonAlgorithmAdapter{} + switcher.algorithms["8404"] = &Algo8404Adapter{} + + // 设置默认算法为gorgon + switcher.currentAlgorithm = "gorgon" + + return switcher +} + +// SwitchAlgorithm 切换算法 +func (s *AlgorithmSwitcher) SwitchAlgorithm(algorithmType string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, exists := s.algorithms[algorithmType]; !exists { + return fmt.Errorf("不支持的算法类型: %s", algorithmType) + } + + s.currentAlgorithm = algorithmType + return nil +} + +// GetCurrentAlgorithm 获取当前算法名称 +func (s *AlgorithmSwitcher) GetCurrentAlgorithm() string { + s.mu.RLock() + defer s.mu.RUnlock() + return s.currentAlgorithm +} + +// GetAvailableAlgorithms 获取可用算法列表 +func (s *AlgorithmSwitcher) GetAvailableAlgorithms() []string { + s.mu.RLock() + defer s.mu.RUnlock() + + algorithms := make([]string, 0, len(s.algorithms)) + for name := range s.algorithms { + algorithms = append(algorithms, name) + } + return algorithms +} + +// GetCurrentAlgorithmInstance 获取当前算法实例 +func (s *AlgorithmSwitcher) GetCurrentAlgorithmInstance() AlgorithmInterface { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.algorithms[s.currentAlgorithm] +} + +// GetAlgorithmInstance 获取指定算法实例 +func (s *AlgorithmSwitcher) GetAlgorithmInstance(algorithmType string) (AlgorithmInterface, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + algorithm, exists := s.algorithms[algorithmType] + if !exists { + return nil, fmt.Errorf("不支持的算法类型: %s", algorithmType) + } + + return algorithm, nil +} + +// GorgonAlgorithmAdapter Gorgon算法适配器(兼容旧版) +type GorgonAlgorithmAdapter struct{} + +func (g *GorgonAlgorithmAdapter) GetAlgorithmName() string { + return "Gorgon算法" +} + +func (g *GorgonAlgorithmAdapter) GenerateHeaders(url string, headers map[string]string) (map[string]string, error) { + // 使用Gorgon算法 + parts := strings.Split(url, "?") + if len(parts) < 2 { + return headers, fmt.Errorf("URL格式错误") + } + + xSSStub := headers["x-ss-stub"] + cookie := headers["Cookie"] + if cookie == "" { + cookie = headers["cookie"] + } + + gorgon := NewGorgon(parts[1], time.Now().Unix(), xSSStub, cookie) + result := gorgon.GetValue() + + finalHeaders := make(map[string]string) + for k, v := range headers { + finalHeaders[k] = v + } + + for k, v := range result { + finalHeaders[k] = v + } + + return finalHeaders, nil +} + +// Algo8404Adapter 8404算法适配器 +type Algo8404Adapter struct{} + +func (a *Algo8404Adapter) GetAlgorithmName() string { + return "8404算法" +} + +func (a *Algo8404Adapter) GenerateHeaders(url string, headers map[string]string) (map[string]string, error) { + // 使用8404算法 + parts := strings.Split(url, "?") + if len(parts) < 2 { + return headers, fmt.Errorf("URL格式错误") + } + + xSSStub := headers["x-ss-stub"] + algo := NewAlgo8404(parts[1], time.Now().Unix(), xSSStub) + result := algo.GetValue() + + finalHeaders := make(map[string]string) + for k, v := range headers { + finalHeaders[k] = v + } + + for k, v := range result { + finalHeaders[k] = v + } + + return finalHeaders, nil +} + +// GlobalAlgorithmSwitcher 全局算法切换器实例 +var GlobalAlgorithmSwitcher = NewAlgorithmSwitcher() + diff --git a/internal/common/http.go b/internal/common/http.go new file mode 100644 index 0000000..ac35ebb --- /dev/null +++ b/internal/common/http.go @@ -0,0 +1,192 @@ +package common + +import ( + "bytes" + "compress/zlib" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" +) + +// HTTPClient HTTP客户端 +type HTTPClient struct { + client *http.Client + logger *Logger +} + +// NewHTTPClient 创建新的HTTP客户端 +func NewHTTPClient() *HTTPClient { + return &HTTPClient{ + client: &http.Client{ + Timeout: 30 * time.Second, + }, + logger: NewLogger("http_utils", "INFO", ""), + } +} + +// ClientResponse 客户端响应 +type ClientResponse struct { + Status int + Content []byte + Headers http.Header + Cookies []*http.Cookie + Text string +} + +// GetHeader 获取响应头(兼容方法) +func (r *ClientResponse) GetHeader(key string) string { + return r.Headers.Get(key) +} + +// JSON 解析JSON响应 +func (r *ClientResponse) JSON() (map[string]interface{}, error) { + var result map[string]interface{} + err := json.Unmarshal(r.Content, &result) + return result, err +} + +// Request HTTP请求 +func (h *HTTPClient) Request(url string, options map[string]interface{}) (*ClientResponse, error) { + // 获取请求方法 + method := "GET" + if m, ok := options["method"].(string); ok { + method = strings.ToUpper(m) + } + + // 获取请求头 + headers := make(map[string]string) + if h, ok := options["headers"].(map[string]string); ok { + headers = h + } + + // 设置默认User-Agent + if _, ok := headers["User-Agent"]; !ok { + headers["User-Agent"] = "okhttp/4.12.0" + } + + // 获取请求体 + var body io.Reader + if method == "POST" || method == "PUT" { + if b, ok := options["body"].(string); ok { + body = strings.NewReader(b) + } else if b, ok := options["body"].([]byte); ok { + body = bytes.NewReader(b) + } else if data, ok := options["data"].(string); ok { + body = strings.NewReader(data) + } else if data, ok := options["data"].([]byte); ok { + body = bytes.NewReader(data) + } + } + + // 创建请求 + req, err := http.NewRequest(method, url, body) + if err != nil { + h.logger.Error(fmt.Sprintf("Failed to create request: %v", err)) + return nil, err + } + + // 设置请求头 + for k, v := range headers { + req.Header.Set(k, v) + } + + // 打印请求信息 + // h.logger.Info(fmt.Sprintf("=== HTTP 请求 ===")) + // h.logger.Info(fmt.Sprintf("方法: %s", method)) + // h.logger.Info(fmt.Sprintf("URL: %s", url)) + // h.logger.Info(fmt.Sprintf("请求头:")) + // for k, v := range req.Header { + // h.logger.Info(fmt.Sprintf(" %s: %s", k, strings.Join(v, ", "))) + // } + + // 检查是否跳过SSL验证 + skipSSL := os.Getenv("FQWEB_SKIP_SSL_VERIFY") + if skipSSL != "" && h.client.Transport == nil { + // 配置跳过SSL验证 + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + h.client.Transport = transport + h.logger.Warning("已启用SSL验证跳过模式") + } + + // 发送请求 + resp, err := h.client.Do(req) + if err != nil { + h.logger.Error(fmt.Sprintf("HTTP Request error: %v", err)) + return nil, err + } + defer resp.Body.Close() + + // 读取响应体 + content, err := io.ReadAll(resp.Body) + if err != nil { + h.logger.Error(fmt.Sprintf("Failed to read response: %v", err)) + return nil, err + } + + // 处理zlib压缩 + if len(content) > 2 && (content[0] == 0x78 && (content[1] == 0x9C || content[1] == 0x01)) { + reader, err := zlib.NewReader(bytes.NewReader(content)) + if err == nil { + decompressed, err := io.ReadAll(reader) + reader.Close() + if err == nil { + content = decompressed + } + } + } + + + response := &ClientResponse{ + Status: resp.StatusCode, + Content: content, + Headers: resp.Header, + Cookies: resp.Cookies(), + Text: string(content), + } + + return response, nil +} + +// Get GET请求 +func (h *HTTPClient) Get(url string, headers map[string]string) (*ClientResponse, error) { + options := map[string]interface{}{ + "method": "GET", + "headers": headers, + } + return h.Request(url, options) +} + +// Post POST请求 +func (h *HTTPClient) Post(url string, data interface{}, headers map[string]string) (*ClientResponse, error) { + options := map[string]interface{}{ + "method": "POST", + "headers": headers, + "body": data, + } + return h.Request(url, options) +} + +// ConvertDictToFormString 将字典转换为表单字符串 +func ConvertDictToFormString(dict map[string]string) string { + parts := make([]string, 0, len(dict)) + for k, v := range dict { + parts = append(parts, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(parts, "&") +} + +// DefaultHTTPClient 默认HTTP客户端 +var DefaultHTTPClient = NewHTTPClient() + +// Request 全局请求函数 +func Request(url string, options map[string]interface{}) (*ClientResponse, error) { + return DefaultHTTPClient.Request(url, options) +} + diff --git a/internal/common/logger.go b/internal/common/logger.go new file mode 100644 index 0000000..b0a09ce --- /dev/null +++ b/internal/common/logger.go @@ -0,0 +1,123 @@ +package common + +import ( + "fmt" + "log" + "strings" + "time" +) + +// LogLevel 日志级别 +type LogLevel int + +const ( + DEBUG LogLevel = iota + INFO + WARNING + ERROR + CRITICAL +) + +// Logger 日志器 +type Logger struct { + moduleName string + outputLevel LogLevel +} + +// NewLogger 创建新的日志器 +func NewLogger(moduleName string, outputLevel string, filename string) *Logger { + logger := &Logger{ + moduleName: moduleName, + } + + // 设置日志级别 + switch strings.ToUpper(outputLevel) { + case "DEBUG": + logger.outputLevel = DEBUG + case "INFO": + logger.outputLevel = INFO + case "WARNING": + logger.outputLevel = WARNING + case "ERROR": + logger.outputLevel = ERROR + case "CRITICAL": + logger.outputLevel = CRITICAL + default: + logger.outputLevel = INFO + } + + return logger +} + +// Debug 调试日志 +func (l *Logger) Debug(message string) { + if l.outputLevel <= DEBUG { + l.log("DEBUG", message) + } +} + +// Info 信息日志 +func (l *Logger) Info(message string) { + if l.outputLevel <= INFO { + l.log("INFO", message) + } +} + +// Warning 警告日志 +func (l *Logger) Warning(message string) { + if l.outputLevel <= WARNING { + l.log("WARNING", message) + } +} + +// Error 错误日志 +func (l *Logger) Error(message string) { + if l.outputLevel <= ERROR { + l.log("ERROR", message) + } +} + +// Critical 严重错误日志 +func (l *Logger) Critical(message string) { + if l.outputLevel <= CRITICAL { + l.log("CRITICAL", message) + } +} + +// log 内部日志方法 +func (l *Logger) log(level string, message string) { + timestamp := time.Now().Format("2006-01-02 15:04:05") + + logMessage := fmt.Sprintf("%s|[%s/%s]|%s", + timestamp, l.moduleName, level, message) + + // 控制台输出 + fmt.Println(logMessage) +} + +// Log 全局日志函数 +func Log(moduleName string) *Logger { + return NewLogger(moduleName, "INFO", "") +} + +// HighlightError 高亮错误 +func HighlightError(err interface{}) string { + return fmt.Sprintf("%v", err) +} + +// 全局日志实例 +var ( + defaultLogger = NewLogger("fanqie", "INFO", "") +) + +// LogPrint 打印日志 +func LogPrint(args ...interface{}) { + message := fmt.Sprint(args...) + defaultLogger.Info(message) +} + +func init() { + // 初始化日志系统 + log.SetFlags(log.LstdFlags | log.Lshortfile) +} + diff --git a/internal/common/utils.go b/internal/common/utils.go new file mode 100644 index 0000000..b90abba --- /dev/null +++ b/internal/common/utils.go @@ -0,0 +1,32 @@ +package common + +import ( + "crypto/md5" + "encoding/base64" + "encoding/hex" + "fmt" +) + +// Bytes2DataURI 将字节转换为 Data URI +func Bytes2DataURI(dataBytes []byte, mimeType string) string { + encoded := base64.StdEncoding.EncodeToString(dataBytes) + return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded) +} + +// CreateMD5 创建MD5哈希 +func CreateMD5(s interface{}) string { + var data []byte + switch v := s.(type) { + case string: + data = []byte(v) + case []byte: + data = v + default: + data = []byte(fmt.Sprintf("%v", v)) + } + + hasher := md5.New() + hasher.Write(data) + return hex.EncodeToString(hasher.Sum(nil)) +} + diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..4a3ab31 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,234 @@ +package config + +import ( + "fmt" + "log" + "sync" + + "github.com/spf13/viper" +) + +// Config 应用配置结构 +type Config struct { + Server ServerConfig `mapstructure:"server"` + Fanqie FanqieConfig `mapstructure:"fanqie"` + Cache CacheConfig `mapstructure:"cache"` + DeviceKeyPool DeviceKeyPoolConfig `mapstructure:"device_key_pool"` +} + +// ServerConfig 服务器配置 +type ServerConfig struct { + Port string `mapstructure:"port"` + Host string `mapstructure:"host"` + AccessKey string `mapstructure:"access_key"` +} + +// FanqieConfig 番茄小说配置 +type FanqieConfig struct { + Algorithm string `mapstructure:"algorithm"` + Signature string `mapstructure:"signature"` // 签名方法: sign2 / sign4 + Content ContentConfig `mapstructure:"content"` + Manga MangaConfig `mapstructure:"manga"` +} + +// ContentConfig 内容处理配置 +type ContentConfig struct { + RemoveImages bool `mapstructure:"remove_images"` + RemoveImageDesc bool `mapstructure:"remove_image_desc"` + RemoveBlankLines bool `mapstructure:"remove_blank_lines"` + RemoveHTMLTags bool `mapstructure:"remove_html_tags"` + RemoveChapterTitle bool `mapstructure:"remove_chapter_title"` + AddParagraphIndent bool `mapstructure:"add_paragraph_indent"` + DoubleNewline bool `mapstructure:"double_newline"` // 段落间使用双换行 + EnableReview bool `mapstructure:"enable_review"` // 是否启用段评功能 +} + +// MangaConfig 漫画处理配置 +type MangaConfig struct { + OutputMode string `mapstructure:"output_mode"` // 输出模式: data_uri / base64 / url +} + +// CacheConfig 缓存配置 +type CacheConfig struct { + Enabled bool `mapstructure:"enabled"` + SQLitePath string `mapstructure:"sqlite_path"` + CleanupEnabled bool `mapstructure:"cleanup_enabled"` // 是否启用缓存清理 + CleanupSeconds int `mapstructure:"cleanup_seconds"` // 缓存清理时间(秒) +} + +// DeviceKeyPoolConfig 设备密钥池配置 +type DeviceKeyPoolConfig struct { + Enabled bool `mapstructure:"enabled"` // 是否启用设备密钥池 + InitialSize int `mapstructure:"initial_size"` // 初始化设备数量 + MinSize int `mapstructure:"min_size"` // 最小设备数量 + MaxSize int `mapstructure:"max_size"` // 最大设备数量 +} + +// ConfigManager 配置管理器 +type ConfigManager struct { + mu sync.RWMutex + config *Config + configPath string + callbacks []func(*Config) +} + +var ( + // AppConfig 全局配置实例 + AppConfig *Config + manager *ConfigManager +) + +// NewConfigManager 创建配置管理器 +func NewConfigManager() *ConfigManager { + return &ConfigManager{ + callbacks: make([]func(*Config), 0), + } +} + +// LoadConfig 加载配置文件 +func LoadConfig(configPath string) error { + // 初始化管理器 + if manager == nil { + manager = NewConfigManager() + manager.configPath = configPath + } + + viper.SetConfigFile(configPath) + viper.SetConfigType("yaml") + + // 设置默认值 + setDefaults() + + if err := viper.ReadInConfig(); err != nil { + return fmt.Errorf("读取配置文件失败: %w", err) + } + + newConfig := &Config{} + if err := viper.Unmarshal(newConfig); err != nil { + return fmt.Errorf("解析配置文件失败: %w", err) + } + + // 更新配置 + manager.mu.Lock() + manager.config = newConfig + AppConfig = newConfig + manager.mu.Unlock() + + return nil +} + +// setDefaults 设置默认值 +func setDefaults() { + // 服务器配置 + viper.SetDefault("server.port", "40180") + viper.SetDefault("server.host", "0.0.0.0") + + // 番茄小说配置 + viper.SetDefault("fanqie.algorithm", "gorgon") + viper.SetDefault("fanqie.signature", "sign2") + + // 内容处理配置 + viper.SetDefault("fanqie.content.remove_images", false) + viper.SetDefault("fanqie.content.remove_image_desc", false) + viper.SetDefault("fanqie.content.remove_blank_lines", true) + viper.SetDefault("fanqie.content.remove_html_tags", false) + viper.SetDefault("fanqie.content.remove_chapter_title", false) + viper.SetDefault("fanqie.content.add_paragraph_indent", false) + viper.SetDefault("fanqie.content.double_newline", true) + viper.SetDefault("fanqie.content.enable_review", true) + + // 漫画处理配置 + viper.SetDefault("fanqie.manga.output_mode", "url") + + // 缓存配置 + viper.SetDefault("cache.enabled", true) + viper.SetDefault("cache.sqlite_path", "./cache.db") + viper.SetDefault("cache.cleanup_enabled", false) + viper.SetDefault("cache.cleanup_seconds", 86400) + + // 设备密钥池配置 + viper.SetDefault("device_key_pool.enabled", true) + viper.SetDefault("device_key_pool.initial_size", 3) + viper.SetDefault("device_key_pool.min_size", 2) + viper.SetDefault("device_key_pool.max_size", 10) +} + +// GetConfig 获取配置实例 +func GetConfig() *Config { + if manager != nil { + manager.mu.RLock() + defer manager.mu.RUnlock() + if manager.config != nil { + return manager.config + } + } + + if AppConfig == nil { + log.Fatal("配置未初始化,请先调用 LoadConfig") + } + return AppConfig +} + +// ReloadConfig 重新加载配置文件 +func ReloadConfig() error { + if manager == nil { + return fmt.Errorf("配置管理器未初始化") + } + + manager.mu.RLock() + configPath := manager.configPath + manager.mu.RUnlock() + + if configPath == "" { + return fmt.Errorf("配置文件路径为空") + } + + // 重新读取配置文件 + viper.SetConfigFile(configPath) + if err := viper.ReadInConfig(); err != nil { + return fmt.Errorf("重新读取配置文件失败: %w", err) + } + + newConfig := &Config{} + if err := viper.Unmarshal(newConfig); err != nil { + return fmt.Errorf("重新解析配置文件失败: %w", err) + } + + // 更新配置 + manager.mu.Lock() + oldAlgorithm := manager.config.Fanqie.Algorithm + manager.config = newConfig + AppConfig = newConfig + callbacks := make([]func(*Config), len(manager.callbacks)) + copy(callbacks, manager.callbacks) + manager.mu.Unlock() + + // 通知配置更新 + for _, callback := range callbacks { + callback(newConfig) + } + + log.Printf("配置重新加载成功: %s", configPath) + if oldAlgorithm != newConfig.Fanqie.Algorithm { + log.Printf("算法已切换: %s -> %s", oldAlgorithm, newConfig.Fanqie.Algorithm) + } + return nil +} + +// RegisterConfigCallback 注册配置更新回调 +func RegisterConfigCallback(callback func(*Config)) { + if manager == nil { + log.Printf("警告: 配置管理器未初始化,无法注册回调") + return + } + + manager.mu.Lock() + defer manager.mu.Unlock() + manager.callbacks = append(manager.callbacks, callback) +} + +// GetConfigManager 获取配置管理器实例 +func GetConfigManager() *ConfigManager { + return manager +} + diff --git a/internal/config/watcher.go b/internal/config/watcher.go new file mode 100644 index 0000000..072ded4 --- /dev/null +++ b/internal/config/watcher.go @@ -0,0 +1,120 @@ +package config + +import ( + "log" + "path/filepath" + "time" + + "github.com/fsnotify/fsnotify" +) + +// ConfigWatcher 配置文件监控器 +type ConfigWatcher struct { + watcher *fsnotify.Watcher + configPath string + stopCh chan struct{} +} + +// NewConfigWatcher 创建配置文件监控器 +func NewConfigWatcher(configPath string) (*ConfigWatcher, error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + + // 转换为绝对路径 + absPath, err := filepath.Abs(configPath) + if err != nil { + return nil, err + } + + return &ConfigWatcher{ + watcher: watcher, + configPath: absPath, + stopCh: make(chan struct{}), + }, nil +} + +// Start 启动配置文件监控 +func (w *ConfigWatcher) Start() error { + // 监控配置文件所在目录 + configDir := filepath.Dir(w.configPath) + if err := w.watcher.Add(configDir); err != nil { + return err + } + + go w.watchLoop() + return nil +} + +// Stop 停止配置文件监控 +func (w *ConfigWatcher) Stop() { + close(w.stopCh) + if w.watcher != nil { + w.watcher.Close() + } + log.Println("配置文件监控已停止") +} + +// watchLoop 监控循环 +func (w *ConfigWatcher) watchLoop() { + // 防抖动:在短时间内多次文件变化只处理一次 + var debounceTimer *time.Timer + const debounceDelay = 500 * time.Millisecond + + for { + select { + case event, ok := <-w.watcher.Events: + if !ok { + return + } + + // 只处理配置文件的写入或创建事件 + if filepath.Base(event.Name) == filepath.Base(w.configPath) { + if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create { + // log.Printf("检测到配置文件变化: %s", event.Name) + + // 重置防抖动定时器 + if debounceTimer != nil { + debounceTimer.Stop() + } + + debounceTimer = time.AfterFunc(debounceDelay, func() { + if err := ReloadConfig(); err != nil { + log.Printf("重新加载配置失败: %v", err) + } else { + log.Println("✅ 配置热更新成功!") + } + }) + } + } + + case err, ok := <-w.watcher.Errors: + if !ok { + return + } + log.Printf("配置文件监控错误: %v", err) + + case <-w.stopCh: + if debounceTimer != nil { + debounceTimer.Stop() + } + return + } + } +} + +// StartConfigWatcher 启动配置文件监控(全局函数) +func StartConfigWatcher(configPath string) (*ConfigWatcher, error) { + watcher, err := NewConfigWatcher(configPath) + if err != nil { + return nil, err + } + + if err := watcher.Start(); err != nil { + return nil, err + } + + return watcher, nil +} + diff --git a/internal/fqapi/audio.go b/internal/fqapi/audio.go new file mode 100644 index 0000000..6089466 --- /dev/null +++ b/internal/fqapi/audio.go @@ -0,0 +1,135 @@ +package fqapi + +import ( + "encoding/json" + "fanqie/internal/common" + "fmt" +) + +var audioLogger = common.NewLogger("audio", "INFO", "") + +// HandleAudio 处理获取音频 +func HandleAudio(itemIDs interface{}, toneID interface{}, output interface{}) (string, error) { + if itemIDs == nil || itemIDs == "" { + return BuildSuccessResponse([]interface{}{}), nil + } + + // 处理itemIDs - 使用ParseStringParam + itemIDStr := ParseStringParam(itemIDs, "") + + // 处理toneID - 使用ParseIntParam + toneIDInt := ParseIntParam(toneID, 1) + + // 处理output参数 + outputStr := ParseStringParam(output, "url") + + // 使用 BuildURL 添加设备参数 + url := BuildURL("https://reading.snssdk.com/reading/reader/audio/playinfo/", + fmt.Sprintf("item_ids=%s", itemIDStr), + fmt.Sprintf("tone_id=%d", toneIDInt), + "pv_player=-1", + ) + + // 使用 GorgonRequest 添加签名和设备信息 + resp, err := GorgonRequest(url, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + audioLogger.Error(fmt.Sprintf("获取音频失败: %v", err)) + return BuildErrorResponse("获取音频失败"), err + } + + respJSON, err := resp.JSON() + if err != nil { + audioLogger.Error(fmt.Sprintf("解析响应失败: %v", err)) + return BuildErrorResponse("解析响应失败"), err + } + + // 检查响应码 + var respCode float64 + var okCode bool + + if code, ok := respJSON["code"].(float64); ok { + respCode, okCode = code, code == 0 + } else if code, ok := respJSON["Code"].(float64); ok { + respCode, okCode = code, code == 0 + } + + if okCode { + // 转换响应格式:只修改 code 和 message,保留原始数据结构 + respJSON["code"] = 200 + respJSON["message"] = "SUCCESS" + + // 处理output参数,替换音频URL + if outputStr != "url" { + // 获取当前使用的设备信息 + currentDevice := GetCurrentDevice() + deviceID := "" + if currentDevice != nil { + deviceID = currentDevice.DeviceHeader + } + + // 准备请求头 + headers := map[string]string{ + "User-Agent": "com.dragon.read", + } + + // 处理音频URL,支持多种可能的结构 + if dataArray, ok := respJSON["data"].([]interface{}); ok { + // 情况1:data是数组格式(实际返回的格式) + for _, item := range dataArray { + if audioItem, ok := item.(map[string]interface{}); ok { + // 处理main_url + if mainURL, ok := audioItem["main_url"].(string); ok && mainURL != "" { + processedURL := processMediaURL(mainURL, deviceID, headers, outputStr, "audio") + audioItem["main_url"] = processedURL + } + // 处理backup_url + if backupURL, ok := audioItem["backup_url"].(string); ok && backupURL != "" { + processedURL := processMediaURL(backupURL, deviceID, headers, outputStr, "audio") + audioItem["backup_url"] = processedURL + } + } + } + } else if data, ok := respJSON["data"].(map[string]interface{}); ok { + // 情况2:单个音频在audio_info下 + if audioInfo, ok := data["audio_info"].(map[string]interface{}); ok { + if playURL, ok := audioInfo["play_url"].(string); ok && playURL != "" { + processedURL := processMediaURL(playURL, deviceID, headers, outputStr, "audio") + audioInfo["play_url"] = processedURL + } + } + + // 情况3:音频列表在audio_list下 + if audioList, ok := data["audio_list"].([]interface{}); ok { + for _, item := range audioList { + if audioItem, ok := item.(map[string]interface{}); ok { + if playURL, ok := audioItem["play_url"].(string); ok && playURL != "" { + processedURL := processMediaURL(playURL, deviceID, headers, outputStr, "audio") + audioItem["play_url"] = processedURL + } + } + } + } + } + } + + resultBytes, _ := json.Marshal(respJSON) + return string(resultBytes), nil + } + + // 返回错误信息 + errMsg := "获取音频失败" + if msg, ok := respJSON["message"].(string); ok { + errMsg = msg + } else if msg, ok := respJSON["Message"].(string); ok { + errMsg = msg + } + audioLogger.Error(fmt.Sprintf("获取音频失败: code=%v, msg=%s", respCode, errMsg)) + return BuildErrorResponse(errMsg), nil +} + diff --git a/internal/fqapi/audio_proxy.go b/internal/fqapi/audio_proxy.go new file mode 100644 index 0000000..4645914 --- /dev/null +++ b/internal/fqapi/audio_proxy.go @@ -0,0 +1,113 @@ +package fqapi + +import ( + "database/sql" + "encoding/json" + "fmt" + "time" +) + +// AudioCacheItem 音频缓存项 +type AudioCacheItem struct { + OriginalURL string `json:"original_url"` // 原始CDN URL + DeviceID string `json:"device_id"` // 关联的设备ID + Headers map[string]string `json:"headers"` // 请求头信息 + CachedAt time.Time `json:"cached_at"` // 缓存时间 +} + +// GenerateAudioProxyURL 生成音频代理URL +func GenerateAudioProxyURL(originalURL, deviceID string, headers map[string]string) string { + // 生成缓存key + key := generateRandomKey(16) + + // 将headers序列化为JSON字符串 + headersJSON, _ := json.Marshal(headers) + + // 存储到数据库 + if cacheDB != nil { + cacheMutex.Lock() + defer cacheMutex.Unlock() + + _, err := cacheDB.Exec(` + INSERT INTO audio_cache (key, original_url, device_id, headers, created_at) + VALUES (?, ?, ?, ?, ?) + `, key, originalURL, deviceID, string(headersJSON), time.Now().Unix()) + if err != nil { + audioLogger.Error("保存音频缓存失败: " + err.Error()) + } + } + + // 返回代理URL + return fmt.Sprintf("/audio/proxy/%s", key) +} + +// HandleAudioProxy 处理音频代理请求 +func HandleAudioProxy(key string) ([]byte, string, error) { + if cacheDB == nil { + return nil, "", fmt.Errorf("数据库未初始化") + } + + // 从数据库获取音频信息 + var originalURL, deviceID, headersJSON string + var createdAt int64 + + cacheMutex.Lock() + err := cacheDB.QueryRow(` + SELECT original_url, device_id, headers, created_at + FROM audio_cache WHERE key = ? + `, key).Scan(&originalURL, &deviceID, &headersJSON, &createdAt) + cacheMutex.Unlock() + + if err == sql.ErrNoRows { + return nil, "", fmt.Errorf("音频信息不存在,请先调用 /audio 接口") + } + if err != nil { + return nil, "", fmt.Errorf("查询音频缓存失败: %w", err) + } + + // 检查缓存是否过期(2小时,匹配CDN链接有效期) + if time.Now().Unix()-createdAt > 7200 { // 2小时 = 7200秒 + // 清理过期缓存 + cacheMutex.Lock() + cacheDB.Exec("DELETE FROM audio_cache WHERE key = ?", key) + cacheMutex.Unlock() + return nil, "", fmt.Errorf("音频信息已过期,请重新调用 /audio 接口") + } + + // 解析headers + var headers map[string]string + json.Unmarshal([]byte(headersJSON), &headers) + if headers == nil { + headers = make(map[string]string) + } + + // 下载音频内容,使用关联的设备信息 + data, contentType, err := downloadMediaWithRetry(originalURL, deviceID, headers, 3) + if err != nil { + return nil, "", fmt.Errorf("下载音频失败: %w", err) + } + + return data, contentType, nil +} + +// CleanExpiredAudioCache 清理过期的音频缓存 +func CleanExpiredAudioCache() { + if cacheDB == nil { + return + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + // 删除2小时前的缓存 + result, err := cacheDB.Exec("DELETE FROM audio_cache WHERE created_at < ?", time.Now().Unix()-7200) + if err != nil { + audioLogger.Error("清理过期音频缓存失败: " + err.Error()) + return + } + + rowsAffected, _ := result.RowsAffected() + if rowsAffected > 0 { + audioLogger.Info(fmt.Sprintf("已清理 %d 条过期音频缓存", rowsAffected)) + } +} diff --git a/internal/fqapi/cache_manager.go b/internal/fqapi/cache_manager.go new file mode 100644 index 0000000..765a55e --- /dev/null +++ b/internal/fqapi/cache_manager.go @@ -0,0 +1,528 @@ +package fqapi + +import ( + "database/sql" + "encoding/json" + "fanqie/internal/common" + "fanqie/internal/config" + "fmt" + "sync" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +var ( + cacheDB *sql.DB + cacheMutex sync.Mutex + cacheLogger = common.NewLogger("cache_manager", "INFO", "") + contentRequestCount int // content接口访问计数 + countMutex sync.Mutex + cacheCleanupTicker *time.Ticker // 缓存清理定时器 +) + +// CacheData 缓存数据结构 +type CacheData struct { + Data string `json:"data"` + Expi float64 `json:"expi"` +} + +// InitDB 初始化数据库 +func InitDB() error { + if cacheDB != nil { + return nil // 已初始化 + } + + // 使用配置的路径 + dbPath := "./cache.db" + cfg := config.GetConfig() + if cfg != nil && cfg.Cache.SQLitePath != "" { + dbPath = cfg.Cache.SQLitePath + } + + var err error + cacheDB, err = sql.Open("sqlite3", dbPath) + if err != nil { + cacheLogger.Error("打开数据库失败: " + err.Error()) + return err + } + + // 创建缓存表(添加created_at字段用于清理) + _, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS cache ( + key TEXT PRIMARY KEY, + value TEXT, + created_at INTEGER DEFAULT (strftime('%s', 'now')) + )`) + if err != nil { + cacheLogger.Error("创建缓存表失败: " + err.Error()) + return err + } + + + // 创建statistics表(保存请求统计,始终使用) + _, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS statistics ( + id INTEGER PRIMARY KEY CHECK (id = 1), + total_requests INTEGER DEFAULT 0, + search_requests INTEGER DEFAULT 0, + detail_requests INTEGER DEFAULT 0, + catalog_requests INTEGER DEFAULT 0, + content_requests INTEGER DEFAULT 0, + manga_requests INTEGER DEFAULT 0, + audio_requests INTEGER DEFAULT 0, + video_requests INTEGER DEFAULT 0, + category_requests INTEGER DEFAULT 0, + updated_at INTEGER DEFAULT (strftime('%s', 'now')) + )`) + if err != nil { + cacheLogger.Error("创建statistics表失败: " + err.Error()) + return err + } + + // 初始化statistics表(如果不存在记录) + _, err = cacheDB.Exec(`INSERT OR IGNORE INTO statistics (id) VALUES (1)`) + if err != nil { + cacheLogger.Error("初始化statistics表失败: " + err.Error()) + } + + // 创建索引以提高清理效率 + _, err = cacheDB.Exec(`CREATE INDEX IF NOT EXISTS idx_created_at ON cache(created_at)`) + if err != nil { + cacheLogger.Error("创建索引失败: " + err.Error()) + } + + // 创建device_pool表(设备密钥池) + _, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS device_pool ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_header TEXT NOT NULL, + key TEXT NOT NULL, + key_ver INTEGER NOT NULL, + is_valid INTEGER DEFAULT 1, + created_at INTEGER NOT NULL, + last_used_at INTEGER NOT NULL + )`) + if err != nil { + cacheLogger.Error("创建device_pool表失败: " + err.Error()) + return err + } + + // 创建device_pool索引 + _, err = cacheDB.Exec(`CREATE INDEX IF NOT EXISTS idx_device_pool_valid ON device_pool(is_valid)`) + if err != nil { + cacheLogger.Warning("创建device_pool索引失败: " + err.Error()) + } + + // 创建audio_cache表(音频代理缓存) + _, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS audio_cache ( + key TEXT PRIMARY KEY, + original_url TEXT NOT NULL, + device_id TEXT NOT NULL, + headers TEXT, + created_at INTEGER DEFAULT (strftime('%s', 'now')) + )`) + if err != nil { + cacheLogger.Error("创建audio_cache表失败: " + err.Error()) + return err + } + + return nil +} + +// isCacheEnabled 检查正文内容缓存是否启用 +func isCacheEnabled() bool { + cfg := config.GetConfig() + if cfg == nil { + return false + } + return cfg.Cache.Enabled +} + +// GetCache 获取正文内容缓存 +func GetCache(key string) string { + if !isCacheEnabled() { + return "" + } + + if cacheDB == nil { + return "" + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + var value string + err := cacheDB.QueryRow("SELECT value FROM cache WHERE key = ?", key).Scan(&value) + if err != nil { + if err != sql.ErrNoRows { + cacheLogger.Error("查询缓存失败: " + err.Error()) + } + return "" + } + + var cacheData CacheData + err = json.Unmarshal([]byte(value), &cacheData) + if err != nil { + cacheLogger.Error("解析缓存数据失败: " + err.Error()) + return "" + } + + cacheLogger.Debug("缓存命中: " + key) + + // 检查是否过期 + if time.Now().Unix() < int64(cacheData.Expi) { + cacheLogger.Debug("缓存有效!") + return cacheData.Data + } + + cacheLogger.Debug("缓存已过期!") + return "" +} + +// UpdateCache 更新缓存 +// UpdateCache 更新或插入正文内容缓存 +func UpdateCache(key string, value string, expi int) error { + if !isCacheEnabled() { + return nil + } + + if cacheDB == nil { + return nil + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + cacheData := CacheData{ + Data: value, + Expi: float64(time.Now().Unix() + int64(expi)), + } + + jsonData, err := json.Marshal(cacheData) + if err != nil { + return err + } + + // 检查是否存在 + var exists bool + err = cacheDB.QueryRow("SELECT 1 FROM cache WHERE key = ?", key).Scan(&exists) + + if err == sql.ErrNoRows { + // 不存在,插入 + _, err = cacheDB.Exec("INSERT INTO cache (key, value) VALUES (?, ?)", key, string(jsonData)) + } else { + // 存在,更新 + _, err = cacheDB.Exec("UPDATE cache SET value = ? WHERE key = ?", string(jsonData), key) + } + + if err != nil { + cacheLogger.Error("更新缓存失败: " + err.Error()) + return err + } + + cacheLogger.Debug("缓存已更新: " + key) + return nil +} + + +// CloseCache 关闭缓存数据库 +func CloseCache() { + if cacheDB != nil { + cacheDB.Close() + } +} + + + + +// CleanupAllCache 立即清理所有正文缓存 +func CleanupAllCache() error { + if cacheDB == nil { + return fmt.Errorf("数据库未初始化") + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + // 删除所有缓存数据 + _, err := cacheDB.Exec("DELETE FROM cache") + if err != nil { + return err + } + + // 智能压缩:仅在空闲空间超过阈值时执行 + var freelistCount int64 + err = cacheDB.QueryRow("PRAGMA freelist_count").Scan(&freelistCount) + if err == nil && freelistCount > 0 { + var pageSize int64 + err = cacheDB.QueryRow("PRAGMA page_size").Scan(&pageSize) + if err == nil { + freeBytes := freelistCount * pageSize + // 阈值:空闲空间超过 5MB 时才执行压缩 + const vacuumThreshold = 5 * 1024 * 1024 // 5MB + + if freeBytes >= vacuumThreshold { + cacheLogger.Info(fmt.Sprintf("空闲空间 %.2f MB,开始执行压缩...", float64(freeBytes)/(1024*1024))) + _, err = cacheDB.Exec("VACUUM") + if err != nil { + cacheLogger.Warning("数据库压缩失败: " + err.Error()) + } else { + cacheLogger.Info(fmt.Sprintf("数据库压缩完成,已释放 %.2f MB 磁盘空间", float64(freeBytes)/(1024*1024))) + } + } else { + cacheLogger.Debug(fmt.Sprintf("空闲空间仅 %.2f MB,跳过压缩", float64(freeBytes)/(1024*1024))) + } + } + } + + return nil +} + +// CleanupExpiredCache 清理过期的正文缓存 +func CleanupExpiredCache() error { + cfg := config.GetConfig() + if cfg == nil { + return fmt.Errorf("配置未加载") + } + + // 检查是否启用清理 + if !cfg.Cache.CleanupEnabled { + return nil + } + + // 检查清理时间配置 + if cfg.Cache.CleanupSeconds <= 0 { + return nil + } + + if cacheDB == nil { + return fmt.Errorf("数据库未初始化") + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + // 计算过期时间点(当前时间 - cleanup_seconds) + expiredTime := time.Now().Unix() - int64(cfg.Cache.CleanupSeconds) + + // 删除过期的缓存(created_at < expiredTime) + result, err := cacheDB.Exec("DELETE FROM cache WHERE created_at < ?", expiredTime) + if err != nil { + cacheLogger.Error("清理过期缓存失败: " + err.Error()) + return err + } + + // 获取删除的行数 + rowsAffected, err := result.RowsAffected() + if err != nil { + cacheLogger.Warning("获取删除行数失败: " + err.Error()) + } else if rowsAffected > 0 { + cacheLogger.Info(fmt.Sprintf("已清理 %d 条过期缓存(创建时间早于 %d 秒前)", rowsAffected, cfg.Cache.CleanupSeconds)) + } + + return nil +} + +// StartCacheCleanup 启动缓存自动清理任务 +func StartCacheCleanup() { + cfg := config.GetConfig() + if cfg == nil || !cfg.Cache.CleanupEnabled || cfg.Cache.CleanupSeconds <= 0 { + cacheLogger.Info("缓存自动清理未启用或配置无效,跳过启动") + return + } + + // 每小时执行一次清理(可以根据需要调整间隔) + cleanupInterval := 1 * time.Hour + cacheCleanupTicker = time.NewTicker(cleanupInterval) + + cacheLogger.Info(fmt.Sprintf("缓存自动清理已启动,清理间隔: %v,过期时间: %d 秒", cleanupInterval, cfg.Cache.CleanupSeconds)) + + // 立即执行一次清理 + go func() { + if err := CleanupExpiredCache(); err != nil { + cacheLogger.Error("初始清理失败: " + err.Error()) + } + }() + + // 定时执行清理 + go func() { + for range cacheCleanupTicker.C { + if err := CleanupExpiredCache(); err != nil { + cacheLogger.Error("定时清理失败: " + err.Error()) + } + } + }() +} + +// StopCacheCleanup 停止缓存自动清理任务 +func StopCacheCleanup() { + if cacheCleanupTicker != nil { + cacheCleanupTicker.Stop() + cacheLogger.Info("缓存自动清理已停止") + // 最后执行一次清理 + if err := CleanupExpiredCache(); err != nil { + cacheLogger.Error("最后清理失败: " + err.Error()) + } + } +} + +// IncrementContentRequestCount 增加content接口访问计数 +func IncrementContentRequestCount() int { + countMutex.Lock() + defer countMutex.Unlock() + contentRequestCount++ + return contentRequestCount +} + +// ResetContentRequestCount 重置访问计数 +func ResetContentRequestCount() { + countMutex.Lock() + defer countMutex.Unlock() + contentRequestCount = 0 +} + +// GetContentRequestCount 获取当前访问计数 +func GetContentRequestCount() int { + countMutex.Lock() + defer countMutex.Unlock() + return contentRequestCount +} + +// VacuumDatabase 压缩数据库,释放磁盘空间 +func VacuumDatabase() error { + if cacheDB == nil { + return fmt.Errorf("数据库未初始化") + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + cacheLogger.Info("开始执行数据库压缩...") + _, err := cacheDB.Exec("VACUUM") + if err != nil { + cacheLogger.Error("数据库压缩失败: " + err.Error()) + return err + } + + cacheLogger.Info("数据库压缩完成,磁盘空间已释放") + return nil +} + +// GetDatabaseStats 获取数据库统计信息 +func GetDatabaseStats() (map[string]interface{}, error) { + if cacheDB == nil { + return nil, fmt.Errorf("数据库未初始化") + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + stats := make(map[string]interface{}) + + // 获取cache表记录数 + var cacheCount int + err := cacheDB.QueryRow("SELECT COUNT(*) FROM cache").Scan(&cacheCount) + if err != nil { + cacheLogger.Warning("获取cache表记录数失败: " + err.Error()) + cacheCount = 0 + } + stats["cache_count"] = cacheCount + + // 获取数据库页面统计 + var pageCount, pageSize, freelistCount int64 + err = cacheDB.QueryRow("PRAGMA page_count").Scan(&pageCount) + if err == nil { + stats["page_count"] = pageCount + } + + err = cacheDB.QueryRow("PRAGMA page_size").Scan(&pageSize) + if err == nil { + stats["page_size"] = pageSize + } + + err = cacheDB.QueryRow("PRAGMA freelist_count").Scan(&freelistCount) + if err == nil { + stats["freelist_count"] = freelistCount + stats["total_size_bytes"] = pageCount * pageSize + stats["free_size_bytes"] = freelistCount * pageSize + stats["used_size_bytes"] = (pageCount - freelistCount) * pageSize + } + + return stats, nil +} + +// SaveStatistics 保存请求统计到数据库 +func SaveStatistics(stats map[string]int64) error { + if cacheDB == nil { + return fmt.Errorf("数据库未初始化") + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + _, err := cacheDB.Exec(` + UPDATE statistics + SET total_requests = ?, + search_requests = ?, + detail_requests = ?, + catalog_requests = ?, + content_requests = ?, + manga_requests = ?, + audio_requests = ?, + video_requests = ?, + category_requests = ?, + updated_at = strftime('%s', 'now') + WHERE id = 1 + `, stats["total"], stats["search"], stats["detail"], stats["catalog"], + stats["content"], stats["manga"], stats["audio"], stats["video"], stats["category"]) + + if err != nil { + cacheLogger.Error("保存统计数据失败: " + err.Error()) + return err + } + + return nil +} + +// LoadStatistics 从数据库加载请求统计 +func LoadStatistics() (map[string]int64, error) { + if cacheDB == nil { + return nil, fmt.Errorf("数据库未初始化") + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + var total, search, detail, catalog, content, manga, audio, video, category int64 + + err := cacheDB.QueryRow(` + SELECT total_requests, search_requests, detail_requests, catalog_requests, + content_requests, manga_requests, audio_requests, video_requests, category_requests + FROM statistics WHERE id = 1 + `).Scan(&total, &search, &detail, &catalog, &content, &manga, &audio, &video, &category) + + if err == sql.ErrNoRows { + // 没有数据,返回空统计 + return map[string]int64{ + "total": 0, "search": 0, "detail": 0, "catalog": 0, + "content": 0, "manga": 0, "audio": 0, "video": 0, "category": 0, + }, nil + } + + if err != nil { + cacheLogger.Error("加载统计数据失败: " + err.Error()) + return nil, err + } + + stats := map[string]int64{ + "total": total, + "search": search, + "detail": detail, + "catalog": catalog, + "content": content, + "manga": manga, + "audio": audio, + "video": video, + "category": category, + } + + return stats, nil +} \ No newline at end of file diff --git a/internal/fqapi/catalog.go b/internal/fqapi/catalog.go new file mode 100644 index 0000000..09bb93e --- /dev/null +++ b/internal/fqapi/catalog.go @@ -0,0 +1,207 @@ +package fqapi + +import ( + "encoding/json" + "fanqie/internal/common" + "fmt" + "strconv" + "strings" +) + +var catalogLogger = common.NewLogger("catalog", "INFO", "") + +// HandleGetCatalog 处理获取书籍目录 +func HandleGetCatalog(bookID string) (string, error) { + if bookID == "" { + return BuildErrorResponse("需要参数 book_id"), nil + } + + // 构建URL + reqURL := BuildURL("https://reading.snssdk.com/reading/bookapi/directory/all_items/v/", "book_id="+bookID) + + resp, err := GorgonRequest(reqURL, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + catalogLogger.Error(fmt.Sprintf("获取目录请求失败: %v", err)) + // 尝试备用接口 + return offShelfCatalog(bookID) + } + + respJSON, err := resp.JSON() + if err != nil { + catalogLogger.Error(fmt.Sprintf("解析响应失败: %v", err)) + return offShelfCatalog(bookID) + } + + if code, ok := respJSON["code"].(float64); ok && code == 0 { + // 删除不需要的字段 + if data, ok := respJSON["data"].(map[string]interface{}); ok { + delete(data, "item_list") + delete(data, "book_info") + delete(data, "catalog_data") + } + + // 重新序列化 + resultBytes, err := json.Marshal(respJSON) + if err != nil { + return BuildErrorResponse("序列化失败"), err + } + + return string(resultBytes), nil + } + + // 如果主接口失败,尝试备用接口 + return offShelfCatalog(bookID) +} + +// offShelfCatalog 下架书籍目录获取(备用接口) +func offShelfCatalog(bookID string) (string, error) { + // 第一步:获取所有item_id列表 + url := "https://fanqienovel.com/api/reader/directory/detail?bookId=" + bookID + + client := common.NewHTTPClient() + resp, err := client.Get(url, map[string]string{ + "Referer": "https://fanqienovel.com/page/" + bookID, + "Origin": "https://fanqienovel.com", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36", + }) + + if err != nil { + catalogLogger.Error(fmt.Sprintf("备用接口请求失败: %v", err)) + return BuildErrorResponse("FAILED"), err + } + + var itemListResponse map[string]interface{} + err = json.Unmarshal(resp.Content, &itemListResponse) + if err != nil { + return BuildErrorResponse("FAILED"), err + } + + data, ok := itemListResponse["data"].(map[string]interface{}) + if !ok { + return BuildErrorResponse("响应格式错误"), fmt.Errorf("响应格式错误") + } + + allItemIDs, ok := data["allItemIds"].([]interface{}) + if !ok { + return BuildErrorResponse("无法获取item列表"), fmt.Errorf("无法获取item列表") + } + + // 转换为字符串数组 + itemList := make([]string, len(allItemIDs)) + for i, id := range allItemIDs { + itemList[i] = fmt.Sprintf("%v", id) + } + + // 分批处理(每批100个) + chunkSize := 100 + var result []map[string]interface{} + + for i := 0; i < len(itemList); i += chunkSize { + end := i + chunkSize + if end > len(itemList) { + end = len(itemList) + } + chunk := itemList[i:end] + + details, err := itemMultiDetail(chunk) + if err == nil { + result = append(result, details...) + } + } + + // 构建响应 + response := map[string]interface{}{ + "code": 0, + "message": "SUCCESS", + "data": map[string]interface{}{ + "item_data_list": result, + }, + } + + resultBytes, _ := json.Marshal(response) + return string(resultBytes), nil +} + +// itemMultiDetail 批量获取章节详情 +func itemMultiDetail(itemList []string) ([]map[string]interface{}, error) { + url := "https://novel.snssdk.com/api/novel/book/directory/detail/v/?aid=1319&item_ids=" + strings.Join(itemList, ",") + + client := common.NewHTTPClient() + resp, err := client.Get(url, map[string]string{ + "User-Agent": "okhttp/4.10.0", + }) + + if err != nil { + return nil, err + } + + var dataResp map[string]interface{} + err = json.Unmarshal(resp.Content, &dataResp) + if err != nil { + return nil, err + } + + if code, ok := dataResp["code"].(float64); ok && code == 0 { + chapters, ok := dataResp["data"].([]interface{}) + if !ok { + return nil, fmt.Errorf("响应格式错误") + } + + var result []map[string]interface{} + for _, ch := range chapters { + chapter, ok := ch.(map[string]interface{}) + if !ok { + continue + } + + versionStr := "" + if version, ok := chapter["version"].(string); ok && len(version) >= 32 { + versionStr = version[:32] + } + + // 安全解析 first_pass_time + firstPassTime := 0 + if fpt, ok := chapter["first_pass_time"].(float64); ok { + firstPassTime = int(fpt) + } else if fptStr, ok := chapter["first_pass_time"].(string); ok && fptStr != "" { + // 尝试解析字符串形式的数字 + if fptVal, err := strconv.ParseFloat(fptStr, 64); err == nil { + firstPassTime = int(fptVal) + } + } + + // 安全解析 chapter_word_number + chapterWordNumber := 0 + if cwn, ok := chapter["chapter_word_number"].(float64); ok { + chapterWordNumber = int(cwn) + } else if cwnStr, ok := chapter["chapter_word_number"].(string); ok && cwnStr != "" { + if cwnVal, err := strconv.ParseFloat(cwnStr, 64); err == nil { + chapterWordNumber = int(cwnVal) + } + } + + result = append(result, map[string]interface{}{ + "item_id": chapter["item_id"], + "version": nil, + "content_md5": versionStr, + "first_pass_time": firstPassTime, + "title": chapter["title"], + "volume_name": chapter["volume_name"], + "chapter_type": chapter["chapter_type"], + "chapter_word_number": chapterWordNumber, + "is_review": nil, + }) + } + + return result, nil + } + + return nil, fmt.Errorf("请求失败") +} + diff --git a/internal/fqapi/category_front.go b/internal/fqapi/category_front.go new file mode 100644 index 0000000..0138795 --- /dev/null +++ b/internal/fqapi/category_front.go @@ -0,0 +1,44 @@ +package fqapi + +import ( + "fanqie/internal/common" + "fmt" +) + +var categoryFrontLogger = common.NewLogger("category_front", "INFO", "") + +// HandleCategoryFront 处理获取分类列表 +func HandleCategoryFront(tab interface{}) (string, error) { + tabInt := ParseIntParam(tab, 0) + + reqURL := BuildURL( + "https://reading.snssdk.com/reading/bookapi/new_category/front/v/", + fmt.Sprintf("new_category_tab=%d", tabInt), + fmt.Sprintf("update_version_code=%s", "58932"), + "distinct_style=1", + ) + + resp, err := GorgonRequest(reqURL, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + categoryFrontLogger.Error(fmt.Sprintf("获取分类列表失败: %v", err)) + return BuildErrorResponse("请求失败"), err + } + + respJSON, err := resp.JSON() + if err != nil { + return BuildErrorResponse("解析响应失败"), err + } + + if code, ok := respJSON["code"].(float64); ok && code == 0 { + return resp.Text, nil + } + + return BuildErrorResponse("获取失败"), nil +} + diff --git a/internal/fqapi/category_landing.go b/internal/fqapi/category_landing.go new file mode 100644 index 0000000..dee2fb0 --- /dev/null +++ b/internal/fqapi/category_landing.go @@ -0,0 +1,56 @@ +package fqapi + +import ( + "fanqie/internal/common" + "fmt" +) + +var categoryLandingLogger = common.NewLogger("category_landing", "INFO", "") + +// HandleCategoryLanding 处理获取分类详情 +func HandleCategoryLanding(categoryID, offset, wordNumber, bookStatus, sortBy interface{}) (string, error) { + // 参数转换 + categoryIDInt := ParseIntParam(categoryID, 0) + offsetInt := ParseIntParam(offset, 0) + wordNumberInt := ParseIntParam(wordNumber, 0) + bookStatusInt := ParseIntParam(bookStatus, 0) + sortByInt := ParseIntParam(sortBy, 0) + + if categoryIDInt == 0 { + return BuildErrorResponseWithCode(100144, "参数错误"), nil + } + + reqURL := BuildURL( + "https://reading.snssdk.com/reading/bookapi/new_category/landing/v/", + fmt.Sprintf("category_id=%d", categoryIDInt), + fmt.Sprintf("limit=%d", 20), + fmt.Sprintf("offset=%d", offsetInt), + fmt.Sprintf("word_number=%d", wordNumberInt), + fmt.Sprintf("book_status=%d", bookStatusInt), + fmt.Sprintf("sort_by=%d", sortByInt), + ) + + resp, err := GorgonRequest(reqURL, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + categoryLandingLogger.Error(fmt.Sprintf("获取分类详情失败: %v", err)) + return BuildErrorResponse("请求失败"), err + } + + respJSON, err := resp.JSON() + if err != nil { + return BuildErrorResponse("解析响应失败"), err + } + + if code, ok := respJSON["code"].(float64); ok && code == 0 { + return resp.Text, nil + } + + return BuildErrorResponse("获取失败"), nil +} + diff --git a/internal/fqapi/content.go b/internal/fqapi/content.go new file mode 100644 index 0000000..c75f004 --- /dev/null +++ b/internal/fqapi/content.go @@ -0,0 +1,279 @@ +package fqapi + +import ( + "encoding/base64" + "encoding/json" + "fanqie/internal/common" + "fmt" + "sort" + "strings" +) + +var ( + contentLogger = common.NewLogger("content", "INFO", "") +) + +// ContentFetcher 内容获取器 +type ContentFetcher struct{} + +// HandleGetContent 处理获取内容 +func HandleGetContent(itemID string, textMode, imageMode interface{}) (string, error) { + if itemID == "" { + return `{"code": -1, "msg": "需要参数 item_id"}`, nil + } + + // 参数转换 + textModeBool := parseBoolParam(textMode) + imageModeBool := parseBoolParamDefault(imageMode, true) + + // contentLogger.Info(fmt.Sprintf("获取内容 item_id=%s text_mode=%v image_mode=%v", itemID, textModeBool, imageModeBool)) + + // 检查缓存 + cache := GetCache("content_" + itemID) + if cache != "" { + return processCachedContent(cache, textModeBool, imageModeBool) + } + + // 缓存未命中,从API获取 + return fetchContentFromAPI(itemID, textModeBool, imageModeBool) +} + +// processCachedContent 处理缓存内容 +func processCachedContent(cache string, textMode, imageMode bool) (string, error) { + var d map[string]interface{} + json.Unmarshal([]byte(cache), &d) + + if data, ok := d["data"].(map[string]interface{}); ok { + if content, ok := data["content"].(string); ok { + parser := NewContentParser() + data["content"] = parser.Parse(content, textMode, imageMode) + } + } + + return buildJSONResponse(d), nil +} + +// fetchContentFromAPI 从API获取内容(带设备切换重试) +func fetchContentFromAPI(itemID string, textMode, imageMode bool) (string, error) { + return fetchContentWithRetry(itemID, textMode, imageMode, 0) +} + +// fetchContentWithRetry 从API获取内容,支持设备切换重试 +func fetchContentWithRetry(itemID string, textMode, imageMode bool, retryCount int) (string, error) { + // 防止无限递归 + const maxRetries = 5 + if retryCount >= maxRetries { + contentLogger.Warning(fmt.Sprintf("已重试 %d 次,尝试备用接口", maxRetries)) + return tryBackupAPI(itemID, textMode, imageMode) + } + + // 从设备密钥池获取设备 + pool := GetDeviceKeyPool() + item, err := pool.GetNext() + if err != nil { + contentLogger.Error("从设备池获取设备失败: " + err.Error()) + return `{"code": -1, "msg": "设备池暂无可用设备"}`, err + } + + header := item.DeviceHeader + currentKeyInfo := &KeyInfo{ + Key: item.Key, + Ver: item.KeyVer, + } + contentLogger.Debug(fmt.Sprintf("使用池中设备 ID=%d (重试次数: %d)", item.ID, retryCount)) + + // 移除version_name参数 + headerWithoutVersion := removeVersionFromHeader(header) + + params := []string{ + "aid=1967", + "app_name=novelapp", + "channel=0", + "device_platform=android", + "version_code=66.9", + "item_id=" + itemID, + "os_version=0", + headerWithoutVersion, + } + + allParams := strings.Join(params, "&") + paramList := strings.Split(allParams, "&") + sort.Strings(paramList) + sortedParams := strings.Join(paramList, "&") + + url := "https://reading.snssdk.com/reading/reader/full/v/?" + sortedParams + + resp, err := ContentRequest(url, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + contentLogger.Warning(fmt.Sprintf("主接口请求出错,标记设备无效并切换设备: %v", err)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) + } + + var body map[string]interface{} + json.Unmarshal(resp.Content, &body) + + if code, ok := body["code"].(float64); ok && code == 0 { + data, ok := body["data"].(map[string]interface{}) + if !ok { + contentLogger.Warning("响应data字段无效,切换设备重试") + pool.MarkInvalid(header) + pool.RemoveInvalid() + return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) + } + + contentEncoded, ok := data["content"].(string) + if !ok || contentEncoded == "Invalid" { + // 内容无效,标记当前设备为无效并切换设备 + contentLogger.Warning(fmt.Sprintf("内容返回 Invalid,标记设备无效并切换设备 (重试: %d)", retryCount)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) + } + + // 检查密钥版本 + keyVersion, _ := data["key_version"].(float64) + if currentKeyInfo != nil && int(keyVersion) != currentKeyInfo.Ver { + contentLogger.Warning("密钥版本不匹配,重新获取密钥") + + // 更新池中的密钥 + newKeyInfo, err := GetKey(header) + if err == nil { + UpdateKeyInPool(header, newKeyInfo.Key, newKeyInfo.Ver) + currentKeyInfo = newKeyInfo + } else { + // 获取密钥失败,切换设备 + contentLogger.Warning("获取密钥失败,切换设备重试") + pool.MarkInvalid(header) + pool.RemoveInvalid() + return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) + } + } + + // 解密内容 + contentBytes, _ := base64.StdEncoding.DecodeString(contentEncoded) + if len(contentBytes) < 16 { + contentLogger.Warning("内容长度不足,切换设备重试") + pool.MarkInvalid(header) + pool.RemoveInvalid() + return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) + } + + iv := contentBytes[0:16] + encryptedData := contentBytes[16:] + + keyBytes, _ := base64.StdEncoding.DecodeString(currentKeyInfo.Key) + decrypted, err := De(encryptedData, keyBytes, iv) + if err != nil { + contentLogger.Warning(fmt.Sprintf("解密失败,切换设备重试: %v", err)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) + } + + // 处理内容 + contentStr := string(decrypted) + contentStr = strings.Replace(contentStr, ``, "", -1) + contentStr = strings.TrimSpace(contentStr) + + data["content"] = contentStr + + // 更新缓存 + cacheData := buildCacheData(data) + saveCacheJSON("content_"+itemID, cacheData, 86400*2) + + // 解析内容 + parser := NewContentParser() + data["content"] = parser.Parse(contentStr, textMode, imageMode) + + return buildJSONResponse(body), nil + } + + // 响应code不为0,切换设备重试 + contentLogger.Warning(fmt.Sprintf("响应code异常: %.0f,切换设备重试", body["code"])) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) +} + +// tryBackupAPI 尝试备用API(所有设备都失败后的最后手段) +func tryBackupAPI(itemID string, textMode, imageMode bool) (string, error) { + contentLogger.Warning("尝试备用接口1(所有设备均失败)") + + // 从设备池获取任意一个有效设备用于备用接口 + pool := GetDeviceKeyPool() + item, err := pool.GetNext() + if err != nil { + contentLogger.Error("备用接口:无法获取设备") + return `{"code": -1, "msg": "获取失败"}`, err + } + header := item.DeviceHeader + + // 备用接口1:使用DH加密 + backupAPI := fmt.Sprintf("https://novel.snssdk.com/api/novel/book/reader/content/v1?support_image=1&item_id=%s&aid=1319&app_name=novelapp&channel=0&device_platform=android&os_version=2&version_code=58932&%s", + itemID, header) + + cm := NewCM() + resp, err := GorgonRequest(backupAPI, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + "y": cm.ClientHandshake(), + }, + }) + + if err != nil { + contentLogger.Warning("备用接口1获取正文出错!") + return `{"code": -1, "msg": "获取失败"}`, err + } + + var body map[string]interface{} + json.Unmarshal(resp.Content, &body) + + if code, ok := body["code"].(float64); ok && code == 0 { + delete(body, "log_id") + + data, ok := body["data"].(map[string]interface{}) + if !ok { + return `{"code": -1, "msg": "获取失败"}`, err + } + + contentEncrypted, ok := data["content"].(string) + if !ok || contentEncrypted == "Bad Content" { + return `{"code": -1, "msg": "获取失败"}`, err + } + + // 解密内容 + cHeader := resp.GetHeader("c") + yHeader := resp.GetHeader("y") + + decryptedContent, err := cm.Decrypt(cHeader, yHeader, contentEncrypted) + if err != nil { + contentLogger.Error("解密失败: " + err.Error()) + return `{"code": -1, "msg": "解密失败"}`, err + } + + data["content"] = decryptedContent + + // 更新缓存 + bodyJSONStr := buildJSONResponse(body) + UpdateCache("content_"+itemID, bodyJSONStr, 86400) + + // 解析内容 + parser := NewContentParser() + data["content"] = parser.Parse(decryptedContent, textMode, imageMode) + + return buildJSONResponse(body), nil + } + + contentLogger.Warning("备用接口1获取正文出错!") + return `{"code": -1, "msg": "获取失败"}`, fmt.Errorf("所有接口都失败") +} + diff --git a/internal/fqapi/content_parser.go b/internal/fqapi/content_parser.go new file mode 100644 index 0000000..da82eac --- /dev/null +++ b/internal/fqapi/content_parser.go @@ -0,0 +1,187 @@ +package fqapi + +import ( + "fanqie/internal/common" + "fanqie/internal/config" + "regexp" + "strings" + + "github.com/PuerkitoBio/goquery" +) + +var ( + parserLogger = common.NewLogger("content_parser", "INFO", "") +) + + +// ContentParser 内容解析器 +type ContentParser struct { + cfg config.ContentConfig +} + +// NewContentParser 创建内容解析器 +func NewContentParser() *ContentParser { + return &ContentParser{ + cfg: getContentConfig(), + } +} + +// Parse 解析内容(URL参数优先于配置) +func (p *ContentParser) Parse(content string, textMode bool, imageMode bool) string { + // 1. 清理XML声明和DOCTYPE + content = p.cleanXMLDeclaration(content) + + // 2. 根据配置移除章节标题 + if p.cfg.RemoveChapterTitle { + content = p.removeChapterTitle(content) + } + + // 3. 根据配置处理图片描述 + if p.cfg.RemoveImageDesc { + content = p.removeImageDesc(content) + } + + // 4. 根据参数或配置移除图片 + // URL参数 image_mode=0 或配置 remove_images=true 时移除图片 + if !imageMode || p.cfg.RemoveImages { + content = p.removeImages(content) + } + + // 5. 根据参数或配置处理内容模式 + // URL参数 text_mode=1 或配置 remove_html_tags=true 时使用文本模式 + if textMode || p.cfg.RemoveHTMLTags { + return p.extractText(content) + } + + // 6. HTML模式处理 + return p.processHTMLMode(content) +} + +// cleanXMLDeclaration 清理XML声明 +func (p *ContentParser) cleanXMLDeclaration(content string) string { + content = regexp.MustCompile(`<\?xml[^>]*\?>`).ReplaceAllString(content, "") + content = regexp.MustCompile(`]*>`).ReplaceAllString(content, "") + return strings.TrimSpace(content) +} + +// removeChapterTitle 移除章节标题 +func (p *ContentParser) removeChapterTitle(content string) string { + // 移除 h1 和 h2 标签中的章节标题 + content = regexp.MustCompile(`(?si)]*class="[^"]*chapter[Tt]itle[^"]*"[^>]*>.*?`).ReplaceAllString(content, "") + // 同时移除 标签 + content = regexp.MustCompile(`(?si)<title>.*?`).ReplaceAllString(content, "") + return content +} + +// removeImageDesc 移除图片描述 +func (p *ContentParser) removeImageDesc(content string) string { + return regexp.MustCompile(`(?s)]*class="pictureDesc"[^>]*>.*?

`).ReplaceAllString(content, "") +} + +// removeImages 移除图片 +func (p *ContentParser) removeImages(content string) string { + return regexp.MustCompile(`(?s)]*data-fanqie-type="image"[^>]*>.*?`).ReplaceAllString(content, "") +} + +// extractText 提取文本 +func (p *ContentParser) extractText(content string) string { + doc, err := goquery.NewDocumentFromReader(strings.NewReader(content)) + if err != nil { + parserLogger.Error("解析HTML失败: " + err.Error()) + return content + } + + finalResult := make([]string, 0) + + // 查找body或article + body := doc.Find("body") + article := body.Find("article") + + var elements *goquery.Selection + if article.Length() > 0 { + elements = article + } else { + elements = body + } + + // 遍历所有直接子元素 + elements.Children().Each(func(i int, item *goquery.Selection) { + // 检查是否是图片div + dataType, _ := item.Attr("data-fanqie-type") + isImageDiv := dataType == "image" + + if isImageDiv { + // 这是图片div + if !p.cfg.RemoveImages { + // 保留完整的图片div结构 + divHTML, _ := goquery.OuterHtml(item) + // 解码HTML实体 + divHTML = decodeHTMLEntities(divHTML) + // 标准化img属性 + divHTML = normalizeImgAttributes(divHTML) + finalResult = append(finalResult, divHTML) + } + return + } + + // 普通文本元素 + if item.Children().Length() == 0 { + text := strings.TrimSpace(item.Text()) + if text != "" { + finalResult = append(finalResult, text) + } + } else { + // 递归提取文本 + item.Find("p").Each(func(j int, p *goquery.Selection) { + text := strings.TrimSpace(p.Text()) + if text != "" { + finalResult = append(finalResult, text) + } + }) + } + }) + + // 拼接结果 + var result string + + // 根据配置决定段落间的换行符 + separator := "\n" + if p.cfg.DoubleNewline { + separator = "\n\n" + } + + if p.cfg.AddParagraphIndent && len(finalResult) > 0 { + // 添加段落缩进(两个全角空格) + result = "\u3000\u3000" + strings.Join(finalResult, separator+"\u3000\u3000") + } else { + // 不添加缩进 + result = strings.Join(finalResult, separator) + } + + // 根据配置决定是否移除空行 + if p.cfg.RemoveBlankLines { + result = removeBlankLines(result) + } + + return result +} + +// processHTMLMode 处理HTML模式 +func (p *ContentParser) processHTMLMode(content string) string { + // 标准化img属性 + content = normalizeImgAttributes(content) + + // 解码HTML实体 + content = decodeHTMLEntities(content) + + // 根据配置决定是否移除空行 + if p.cfg.RemoveBlankLines { + // 对HTML内容移除空的

标签 + content = regexp.MustCompile(`(?s)]*>\s*

`).ReplaceAllString(content, "") + // 移除多余的空白 + content = regexp.MustCompile(`\n\s*\n\s*\n`).ReplaceAllString(content, "\n\n") + } + + return content +} + diff --git a/internal/fqapi/content_utils.go b/internal/fqapi/content_utils.go new file mode 100644 index 0000000..be5bc55 --- /dev/null +++ b/internal/fqapi/content_utils.go @@ -0,0 +1,265 @@ +package fqapi + +import ( + "encoding/json" + "fanqie/internal/config" + "sort" + "strings" +) + +// getContentConfig 获取内容配置(带默认值) +func getContentConfig() config.ContentConfig { + if config.AppConfig != nil { + return config.AppConfig.Fanqie.Content + } + // 返回默认配置 + return config.ContentConfig{ + RemoveImages: false, + RemoveImageDesc: false, + RemoveBlankLines: true, + RemoveHTMLTags: false, + RemoveChapterTitle: false, + AddParagraphIndent: false, + } +} + +// decodeHTMLEntities 解码HTML实体 +func decodeHTMLEntities(content string) string { + // 解码常见HTML实体 + content = strings.ReplaceAll(content, "&", "&") + content = strings.ReplaceAll(content, "<", "<") + content = strings.ReplaceAll(content, ">", ">") + content = strings.ReplaceAll(content, """, "\"") + content = strings.ReplaceAll(content, """, "\"") + content = strings.ReplaceAll(content, "'", "'") + content = strings.ReplaceAll(content, "'", "'") + content = strings.ReplaceAll(content, "'", "'") + content = strings.ReplaceAll(content, "/", "/") + + // 解码转义的反斜杠 + content = strings.ReplaceAll(content, "\\\"", "\"") + + return content +} + +// normalizeImgAttributes 将img标签中的img-width和img-height属性替换为标准的width和height属性 +func normalizeImgAttributes(content string) string { + // 替换 img-width 为 width + content = strings.ReplaceAll(content, "img-width=", "width=") + // 替换 img-height 为 height + content = strings.ReplaceAll(content, "img-height=", "height=") + return content +} + +// removeBlankLines 移除多余空行,但保留段落间的双换行 +func removeBlankLines(content string) string { + // 先将多个连续换行(3个及以上)规范化为双换行 + for strings.Contains(content, "\n\n\n") { + content = strings.ReplaceAll(content, "\n\n\n", "\n\n") + } + + // 移除首尾的空白 + content = strings.TrimSpace(content) + + return content +} + +// ============ 参数解析工具函数 ============ + +// parseBoolParam 解析布尔参数 +func parseBoolParam(param interface{}) bool { + switch v := param.(type) { + case string: + return v == "true" || v == "1" || strings.ToLower(v) == "true" + case bool: + return v + } + return false +} + +// parseBoolParamDefault 解析布尔参数(带默认值) +func parseBoolParamDefault(param interface{}, defaultVal bool) bool { + if param == nil || param == "" { + return defaultVal + } + return parseBoolParam(param) +} + +// ============ JSON 处理工具函数 ============ + +// escapeJSON 转义JSON中的特殊字符 +func escapeJSON(s string) string { + s = strings.ReplaceAll(s, "<", "\\u003c") + s = strings.ReplaceAll(s, ">", "\\u003e") + return s +} + +// buildJSONResponse 构建JSON响应并转义 +func buildJSONResponse(data interface{}) string { + resultBytes, _ := json.Marshal(data) + return escapeJSON(string(resultBytes)) +} + +// ============ 设备和密钥管理 ============ + +// getDeviceHeader 获取设备header(从设备池) +func getDeviceHeader() string { + // 从设备池获取任意有效设备header + return GetAnyValidDeviceHeader() +} + +// removeVersionFromHeader 移除header中的version_name参数 +func removeVersionFromHeader(header string) string { + return strings.Replace(header, "&version_name=5.8.9.32", "", -1) +} + +// ============ API参数构建工具 ============ + +// BuildURL 构建API请求URL +// 用法: BuildURL("https://reading.snssdk.com/reading/bookapi/detail/v/", "book_id=123", "tab_type=3") +func BuildURL(fullURL string, customParams ...string) string { + // 基础参数 + allParams := []string{ + "aid=1967", + "app_name=novelapp", + "channel=0", + "device_platform=android", + "os_version=0", + "version_code=58932", + } + + // 添加设备header + header := getDeviceHeader() + if header != "" { + allParams = append(allParams, header) + } + + // 添加自定义参数 + allParams = append(allParams, customParams...) + + // 合并、拆分、排序 + joined := strings.Join(allParams, "&") + paramList := strings.Split(joined, "&") + sort.Strings(paramList) + params := strings.Join(paramList, "&") + + return fullURL + "?" + params +} + +// ============ 缓存处理工具函数 ============ + +// buildCacheData 构建缓存数据结构 +func buildCacheData(data interface{}) map[string]interface{} { + return map[string]interface{}{ + "code": 0, + "message": "SUCCESS", + "data": data, + } +} + +// saveCacheJSON 保存JSON到缓存 +func saveCacheJSON(key string, data interface{}, ttl int) { + cacheJSON, _ := json.Marshal(data) + cacheStr := escapeJSON(string(cacheJSON)) + UpdateCache(key, cacheStr, ttl) +} + +// ============ 错误响应工具函数 ============ + +// BuildErrorResponse 构建统一的错误响应 +func BuildErrorResponse(msg string) string { + return buildJSONResponse(map[string]interface{}{ + "code": -1, + "msg": msg, + }) +} + +// BuildErrorResponseWithCode 构建带自定义错误码的响应 +func BuildErrorResponseWithCode(code int, msg string) string { + return buildJSONResponse(map[string]interface{}{ + "code": code, + "msg": msg, + }) +} + +// BuildSuccessResponse 构建成功响应 +func BuildSuccessResponse(data interface{}) string { + return buildJSONResponse(map[string]interface{}{ + "code": 0, + "message": "SUCCESS", + "data": data, + }) +} + +// ============ 参数转换工具函数 ============ + +// ParseIntParam 解析整数参数(支持多种类型) +func ParseIntParam(param interface{}, defaultVal int) int { + switch v := param.(type) { + case string: + if v != "" { + // 使用strconv.Atoi + if result, err := parseStringToInt(v); err == nil { + return result + } + } + case int: + return v + case float64: + return int(v) + case int64: + return int(v) + } + return defaultVal +} + +// parseStringToInt 辅助函数:将字符串转换为整数 +func parseStringToInt(s string) (int, error) { + // 简单实现,避免导入strconv + result := 0 + negative := false + + if len(s) == 0 { + return 0, nil + } + + start := 0 + if s[0] == '-' { + negative = true + start = 1 + } else if s[0] == '+' { + start = 1 + } + + for i := start; i < len(s); i++ { + if s[i] < '0' || s[i] > '9' { + break + } + result = result*10 + int(s[i]-'0') + } + + if negative { + result = -result + } + + return result, nil +} + +// ParseStringParam 解析字符串参数(支持数组类型自动Join) +func ParseStringParam(param interface{}, defaultVal string) string { + if param == nil || param == "" { + return defaultVal + } + + switch v := param.(type) { + case string: + if v != "" { + return v + } + case []string: + // 数组类型,用逗号连接 + return strings.Join(v, ",") + } + return defaultVal +} + diff --git a/internal/fqapi/detail.go b/internal/fqapi/detail.go new file mode 100644 index 0000000..0732e70 --- /dev/null +++ b/internal/fqapi/detail.go @@ -0,0 +1,55 @@ +package fqapi + +import ( + "encoding/json" + "fanqie/internal/common" + "fmt" +) + +var detailLogger = common.NewLogger("detail", "INFO", "") + +// HandleGetDetail 处理获取书籍详情 +func HandleGetDetail(bookID string) (string, error) { + if bookID == "" { + return BuildErrorResponse("需要参数 book_id"), nil + } + + // 构建URL + reqURL := BuildURL("https://reading.snssdk.com/reading/bookapi/detail/v/", "book_id="+bookID) + + resp, err := GorgonRequest(reqURL, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + detailLogger.Error(fmt.Sprintf("获取详情请求失败: %v", err)) + return BuildErrorResponse("获取详情请求失败"), err + } + + respJSON, err := resp.JSON() + if err != nil { + detailLogger.Error(fmt.Sprintf("解析响应失败: %v", err)) + return BuildErrorResponse("解析响应失败"), err + } + + if code, ok := respJSON["code"].(float64); ok && code == 0 { + // 删除content字段 + if data, ok := respJSON["data"].(map[string]interface{}); ok { + delete(data, "content") + } + + // 重新序列化 + resultBytes, err := json.Marshal(respJSON) + if err != nil { + return BuildErrorResponse("序列化失败"), err + } + + return string(resultBytes), nil + } + + return BuildErrorResponse("获取失败"), nil +} + diff --git a/internal/fqapi/device_config.go b/internal/fqapi/device_config.go new file mode 100644 index 0000000..c4a0304 --- /dev/null +++ b/internal/fqapi/device_config.go @@ -0,0 +1,102 @@ +package fqapi + +// DeviceConfig 设备配置 +var DeviceConfig = struct { + AndroidVersions [][2]interface{} + BrandModels map[string][]string + DeviceBrands []string +}{ + // Android 版本列表 [版本号, API Level] + AndroidVersions: [][2]interface{}{ + {"9", 29}, {"10", 30}, {"11", 31}, + {"12", 32}, {"13", 33}, {"14", 34}, + {"15", 35}, {"16", 36}, + }, + + // 品牌和型号对应关系 + BrandModels: map[string][]string{ + "Xiaomi": { + "MI8", "MI9", "MI10", "MI11", "MI12", "MI13", "MI14", "MI15", + "MIX2S", "MIX3", "MIX4", "MIXFold3", "MIXFold4", + "2201122C", "2206123SC", "2211133C", "2304FPN6DC", "23013RK75C", + "24031PN0DC", "24116PN88C", "2409ARN8BC", "25013PN75C", + }, + "Redmi": { + "RedmiK20", "RedmiK30", "RedmiK40", "RedmiK50", "RedmiK60", "RedmiK70", "RedmiK80", + "RedmiNote8", "RedmiNote9", "RedmiNote10", "RedmiNote11", "RedmiNote12", "RedmiNote13", "RedmiNote14", + "RedmiTurbo3", "RedmiTurbo4", + }, + "OPPO": { + "PCCM00", "PDEM10", "PDEM30", "PDYM20", "PDPM00", "PDSM00", "PDST00", + "CPH2015", "CPH2023", "CPH2145", "CPH2173", "CPH2179", "CPH2207", "CPH2211", + "CPH2247", "CPH2271", "CPH2333", "CPH2365", "CPH2437", "CPH2451", "CPH2473", + "CPH2477", "CPH2551", "CPH2591", "CPH2613", "CPH2647", "CPH2719", "CPH2759", + "PGBM10", "PGJM10", "PGKM10", "PCLM50", "PECM00", "PEXM00", + "PJCM10", "PJEM00", "PKBM00", "PKCM00", + }, + "vivo": { + "V1921A", "V1923A", "V1924A", "V1928A", "V1930A", "V1932A", "V1936A", + "V2001A", "V2011A", "V2012A", "V2020A", "V2021A", "V2022A", "V2023A", + "V2024A", "V2025A", "V2031A", "V2032A", "V2046A", "V2047A", "V2048A", + "V2049A", "V2050A", "V2056A", "V2057A", "V2059A", "V2061A", "V2065A", + "V2070A", "V2072A", "V2073A", "V2080A", "V2099A", "V2111A", "V2115A", + "V2121A", "V2134A", "V2141A", "V2145A", "V2156A", "V2162A", "V2171A", + "V2199A", "V2207A", "V2218A", "V2227A", "V2231A", "V2247A", "V2266A", + "V2280A", "V2296A", "V2318A", "V2327A", "V2338A", "V2352A", "V2366A", + "V2401A", "V2428A", "V2451A", "V2473A", + }, + "iQOO": { + "I2007", "I2012", "I2018", "I2019", "V2130A", "V2141A", "V2156A", + "I2207", "I2214", "I2301", "I2302", "V2307A", "V2352A", + "I2401", "I2409", + }, + "Huawei": { + "P30", "P40", "P50", "P60", "P70", + "Mate20", "Mate30", "Mate40", "Mate50", "Mate60", "MateX5", "MateXT", + "Nova5", "Nova6", "Nova7", "Nova8", "Nova9", "Nova10", "Nova11", "Nova12", "Nova13", + "ELE-AL00", "ANA-AN00", "TAS-AN00", "VOG-AL10", "LYA-AL00", "MAR-AL00", + "NOH-AN00", "ABR-AL00", "BRQ-AN00", "ELS-AN00", "FRL-AN00A", "JAD-AL50", + "ALN-AL00", "DBY-AN00", "CET-AN00", "OXP-AN00", "PGT-AN00", + }, + "Honor": { + "Honor10", "Honor20", "Honor30", "Honor50", "Honor60", "Honor70", "Honor80", "Honor90", "Honor100", "Honor200", + "HonorMagic5", "HonorMagic6", "HonorMagic7", + "HonorX40", "HonorX50", "HonorX60", + }, + "realme": { + "RMX1931", "RMX2001", "RMX2021", "RMX2061", "RMX2071", "RMX2085", "RMX2111", + "RMX2121", "RMX2141", "RMX2155", "RMX2161", "RMX2170", "RMX2173", "RMX2176", + "RMX2202", "RMX2205", "RMX3031", "RMX3033", "RMX3061", "RMX3063", "RMX3081", + "RMX3085", "RMX3092", "RMX3141", "RMX3142", "RMX3151", "RMX3161", "RMX3171", + "RMX3195", "RMX3197", "RMX3201", "RMX3231", "RMX3241", "RMX3261", "RMX3263", + "RMX3300", "RMX3310", "RMX3350", "RMX3357", "RMX3363", "RMX3371", "RMX3381", + "RMX3461", "RMX3478", "RMX3506", "RMX3511", "RMX3516", "RMX3551", "RMX3562", + "RMX3630", "RMX3660", "RMX3686", "RMX3687", "RMX3700", "RMX3710", "RMX3785", + "RMX3800", "RMX3890", "RMX3933", "RMX3999", "RMX4001", "RMX4100", "RMX4200", + "RMX4311", "RMX4400", "RMX4506", "RMX4600", "RMX4710", + }, + "Samsung": { + "SM-G9600", "SM-G9650", "SM-G9730", "SM-G9750", "SM-G9810", "SM-G9880", + "SM-G9900", "SM-G9910", "SM-G9960", "SM-G9980", "SM-N9600", "SM-N9700", + "SM-N9750", "SM-N9760", "SM-N9810", "SM-N9860", + "SM-A5160", "SM-A6060", "SM-A7070", "SM-A8050", "SM-A9080", "SM-A9200", + "SM-F7070", "SM-F9000", "SM-F9160", "SM-W2021", "SM-W2022", "SM-W2023", + "SM-S9180", "SM-S9280", "SM-S9360", "SM-S9480", + "SM-F9360", "SM-F9460", "SM-F9560", "SM-F9660", + "SM-A5560", "SM-A7260", "SM-A8860", + }, + "OnePlus": { + "GM1900", "GM1910", "HD1900", "HD1910", "IN2010", "IN2020", "LE2100", "LE2110", + "MT2110", "MT2111", "NE2210", "NE2211", "CPH2413", "CPH2423", "CPH2449", + "PGP110", "PHB110", "PHK110", "CPH2525", "CPH2573", "CPH2609", "PHT110", + "CPH2617", "CPH2667", "CPH2729", "CPH2747", "CPH2805", + "PJA110", "PJB110", "PJC110", "PJD110", + }, + }, + + // 所有支持的品牌列表 + DeviceBrands: []string{ + "Xiaomi", "Redmi", "OPPO", "vivo", "iQOO", + "Huawei", "Honor", "realme", "Samsung", "OnePlus", + }, +} diff --git a/internal/fqapi/device_key_pool.go b/internal/fqapi/device_key_pool.go new file mode 100644 index 0000000..b1beb73 --- /dev/null +++ b/internal/fqapi/device_key_pool.go @@ -0,0 +1,582 @@ +package fqapi + +import ( + "fanqie/internal/common" + "fanqie/internal/config" + "fmt" + "sync" + "time" +) + +var ( + poolLogger = common.NewLogger("device_key_pool", "INFO", "") + globalPool *DeviceKeyPool + poolOnce sync.Once +) + +// DeviceKeyItem 设备密钥项 +type DeviceKeyItem struct { + ID int `json:"id"` + DeviceHeader string `json:"device_header"` + Key string `json:"key"` + KeyVer int `json:"key_ver"` + IsValid bool `json:"is_valid"` + CreatedAt time.Time `json:"created_at"` + LastUsedAt time.Time `json:"last_used_at"` +} + +// DeviceKeyPool 设备密钥池 +type DeviceKeyPool struct { + mu sync.RWMutex + currentIndex int + items []*DeviceKeyItem + enabled bool +} + +// GetDeviceKeyPool 获取全局设备密钥池实例 +func GetDeviceKeyPool() *DeviceKeyPool { + poolOnce.Do(func() { + globalPool = &DeviceKeyPool{ + items: make([]*DeviceKeyItem, 0), + enabled: false, + } + }) + return globalPool +} + +// Initialize 初始化设备密钥池 +func (p *DeviceKeyPool) Initialize() error { + cfg := config.GetConfig() + if cfg == nil || !cfg.DeviceKeyPool.Enabled { + poolLogger.Info("设备密钥池未启用") + p.enabled = false + return nil + } + + p.enabled = true + poolLogger.Info("开始初始化设备密钥池...") + + // 从数据库加载已有的设备 + if err := p.loadFromDB(); err != nil { + poolLogger.Warning("从数据库加载设备失败: " + err.Error()) + } + + // 检查并补充设备到初始化数量 + validCount := p.getValidCount() + initialSize := cfg.DeviceKeyPool.InitialSize + + if validCount < initialSize { + needCount := initialSize - validCount + poolLogger.Info(fmt.Sprintf("当前有效设备数: %d, 需要补充: %d", validCount, needCount)) + + for i := 0; i < needCount; i++ { + if err := p.addNewDevice(); err != nil { + poolLogger.Error(fmt.Sprintf("添加设备失败: %v", err)) + } + } + } + + poolLogger.Info(fmt.Sprintf("设备密钥池初始化完成,当前有效设备数: %d", p.getValidCount())) + return nil +} + +// IsEnabled 检查设备密钥池是否启用 +func (p *DeviceKeyPool) IsEnabled() bool { + return p.enabled +} + +// GetNext 获取下一个可用的设备密钥(轮询方式) +func (p *DeviceKeyPool) GetNext() (*DeviceKeyItem, error) { + if !p.enabled { + return nil, fmt.Errorf("设备密钥池未启用") + } + + p.mu.Lock() + defer p.mu.Unlock() + + // 过滤有效设备 + validItems := make([]*DeviceKeyItem, 0) + for _, item := range p.items { + if item.IsValid { + validItems = append(validItems, item) + } + } + + if len(validItems) == 0 { + return nil, fmt.Errorf("没有可用的设备") + } + + // 轮询获取下一个设备 + item := validItems[p.currentIndex%len(validItems)] + p.currentIndex++ + + // 更新最后使用时间 + item.LastUsedAt = time.Now() + p.updateDeviceInDB(item) + + // poolLogger.Debug(fmt.Sprintf("使用设备 ID=%d", item.ID)) + + // 异步检查是否需要补充 + go p.checkAndRefill() + + return item, nil +} + +// MarkInvalid 标记设备为无效 +func (p *DeviceKeyPool) MarkInvalid(deviceHeader string) error { + if !p.enabled { + return nil + } + + p.mu.Lock() + defer p.mu.Unlock() + + for _, item := range p.items { + if item.DeviceHeader == deviceHeader { + item.IsValid = false + // poolLogger.Warning(fmt.Sprintf("设备 ID=%d 已标记为无效", item.ID)) + + // 更新数据库 + if err := p.updateDeviceInDB(item); err != nil { + return err + } + + // 异步补充新设备 + go p.checkAndRefill() + + return nil + } + } + + return fmt.Errorf("未找到设备") +} + +// RemoveInvalid 移除所有无效设备并自动补充 +func (p *DeviceKeyPool) RemoveInvalid() error { + if !p.enabled { + return nil + } + + p.mu.Lock() + + // 从数据库中删除无效设备 + if err := p.removeInvalidFromDB(); err != nil { + p.mu.Unlock() + return err + } + + // 从内存中移除无效设备 + validItems := make([]*DeviceKeyItem, 0) + for _, item := range p.items { + if item.IsValid { + validItems = append(validItems, item) + } + } + + removedCount := len(p.items) - len(validItems) + p.items = validItems + + if removedCount > 0 { + poolLogger.Info(fmt.Sprintf("已移除 %d 个无效设备", removedCount)) + + // 立即检查并补充设备(使用已持有锁的版本) + p.checkAndRefillLocked() + } + + p.mu.Unlock() + return nil +} + +// GetStats 获取池统计信息 +func (p *DeviceKeyPool) GetStats() map[string]interface{} { + p.mu.RLock() + defer p.mu.RUnlock() + + validCount := 0 + invalidCount := 0 + + for _, item := range p.items { + if item.IsValid { + validCount++ + } else { + invalidCount++ + } + } + + return map[string]interface{}{ + "enabled": p.enabled, + "total": len(p.items), + "valid": validCount, + "invalid": invalidCount, + "current_index": p.currentIndex, + } +} + +// checkAndRefill 检查并补充设备(私有方法,会自动加锁) +func (p *DeviceKeyPool) checkAndRefill() { + if !p.enabled { + return + } + + cfg := config.GetConfig() + if cfg == nil { + return + } + + p.mu.Lock() + validCount := p.getValidCount() + p.mu.Unlock() + + minSize := cfg.DeviceKeyPool.MinSize + maxSize := cfg.DeviceKeyPool.MaxSize + + if validCount < minSize { + needCount := minSize - validCount + // 限制不超过最大值 + if validCount+needCount > maxSize { + needCount = maxSize - validCount + } + + if needCount > 0 { + poolLogger.Info(fmt.Sprintf("有效设备数 %d 低于最小值 %d,开始补充 %d 个设备", validCount, minSize, needCount)) + + for i := 0; i < needCount; i++ { + if err := p.addNewDevice(); err != nil { + poolLogger.Error(fmt.Sprintf("补充设备失败: %v", err)) + } + } + } + } +} + +// checkAndRefillLocked 检查并补充设备(已持有锁的内部版本) +func (p *DeviceKeyPool) checkAndRefillLocked() { + if !p.enabled { + return + } + + cfg := config.GetConfig() + if cfg == nil { + return + } + + validCount := p.getValidCount() + minSize := cfg.DeviceKeyPool.MinSize + maxSize := cfg.DeviceKeyPool.MaxSize + + if validCount < minSize { + needCount := minSize - validCount + // 限制不超过最大值 + if validCount+needCount > maxSize { + needCount = maxSize - validCount + } + + if needCount > 0 { + poolLogger.Info(fmt.Sprintf("有效设备数 %d 低于最小值 %d,开始补充 %d 个设备", validCount, minSize, needCount)) + + // 临时释放锁,因为addNewDevice需要加锁 + p.mu.Unlock() + for i := 0; i < needCount; i++ { + if err := p.addNewDevice(); err != nil { + poolLogger.Error(fmt.Sprintf("补充设备失败: %v", err)) + } + } + p.mu.Lock() + } + } +} + +// getValidCount 获取有效设备数量(调用前需已加锁或在RLock内) +func (p *DeviceKeyPool) getValidCount() int { + count := 0 + for _, item := range p.items { + if item.IsValid { + count++ + } + } + return count +} + +// addNewDevice 添加新设备(会自动加锁) +func (p *DeviceKeyPool) addNewDevice() error { + // 注册新设备 + deviceHeader, err := Register() + if err != nil { + return fmt.Errorf("注册设备失败: %w", err) + } + + // 获取密钥 + keyInfo, err := GetKey(deviceHeader) + if err != nil { + return fmt.Errorf("获取密钥失败: %w", err) + } + + // 创建设备项 + item := &DeviceKeyItem{ + DeviceHeader: deviceHeader, + Key: keyInfo.Key, + KeyVer: keyInfo.Ver, + IsValid: true, + CreatedAt: time.Now(), + LastUsedAt: time.Now(), + } + + // 保存到数据库 + if err := p.saveDeviceToDB(item); err != nil { + return fmt.Errorf("保存设备到数据库失败: %w", err) + } + + // 添加到内存 + p.mu.Lock() + p.items = append(p.items, item) + p.mu.Unlock() + + // poolLogger.Info(fmt.Sprintf("已添加新设备 ID=%d", item.ID)) + return nil +} + +// ===== 数据库操作方法 ===== + +// loadFromDB 从数据库加载设备 +func (p *DeviceKeyPool) loadFromDB() error { + if cacheDB == nil { + return fmt.Errorf("数据库未初始化") + } + + rows, err := cacheDB.Query(` + SELECT id, device_header, key, key_ver, is_valid, created_at, last_used_at + FROM device_pool + ORDER BY id + `) + if err != nil { + return err + } + defer rows.Close() + + items := make([]*DeviceKeyItem, 0) + for rows.Next() { + var item DeviceKeyItem + var createdAt, lastUsedAt int64 + + err := rows.Scan( + &item.ID, + &item.DeviceHeader, + &item.Key, + &item.KeyVer, + &item.IsValid, + &createdAt, + &lastUsedAt, + ) + if err != nil { + poolLogger.Error("扫描行失败: " + err.Error()) + continue + } + + item.CreatedAt = time.Unix(createdAt, 0) + item.LastUsedAt = time.Unix(lastUsedAt, 0) + items = append(items, &item) + } + + p.mu.Lock() + p.items = items + p.mu.Unlock() + + poolLogger.Info(fmt.Sprintf("从数据库加载了 %d 个设备", len(items))) + return nil +} + +// saveDeviceToDB 保存设备到数据库 +func (p *DeviceKeyPool) saveDeviceToDB(item *DeviceKeyItem) error { + if cacheDB == nil { + return fmt.Errorf("数据库未初始化") + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + result, err := cacheDB.Exec(` + INSERT INTO device_pool (device_header, key, key_ver, is_valid, created_at, last_used_at) + VALUES (?, ?, ?, ?, ?, ?) + `, item.DeviceHeader, item.Key, item.KeyVer, item.IsValid, + item.CreatedAt.Unix(), item.LastUsedAt.Unix()) + + if err != nil { + return err + } + + // 获取插入的ID + id, err := result.LastInsertId() + if err == nil { + item.ID = int(id) + } + + return nil +} + +// updateDeviceInDB 更新设备信息 +func (p *DeviceKeyPool) updateDeviceInDB(item *DeviceKeyItem) error { + if cacheDB == nil { + return fmt.Errorf("数据库未初始化") + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + _, err := cacheDB.Exec(` + UPDATE device_pool + SET device_header = ?, key = ?, key_ver = ?, is_valid = ?, last_used_at = ? + WHERE id = ? + `, item.DeviceHeader, item.Key, item.KeyVer, item.IsValid, + item.LastUsedAt.Unix(), item.ID) + + return err +} + +// removeInvalidFromDB 从数据库移除无效设备 +func (p *DeviceKeyPool) removeInvalidFromDB() error { + if cacheDB == nil { + return fmt.Errorf("数据库未初始化") + } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + + result, err := cacheDB.Exec(`DELETE FROM device_pool WHERE is_valid = 0`) + if err != nil { + return err + } + + affected, _ := result.RowsAffected() + if affected > 0 { + poolLogger.Info(fmt.Sprintf("从数据库删除了 %d 个无效设备", affected)) + } + + return nil +} + +// InitDevicePoolTable 初始化设备池表 +func InitDevicePoolTable() error { + if cacheDB == nil { + return fmt.Errorf("数据库未初始化") + } + + _, err := cacheDB.Exec(`CREATE TABLE IF NOT EXISTS device_pool ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_header TEXT NOT NULL, + key TEXT NOT NULL, + key_ver INTEGER NOT NULL, + is_valid INTEGER DEFAULT 1, + created_at INTEGER NOT NULL, + last_used_at INTEGER NOT NULL + )`) + + if err != nil { + return err + } + + // 创建索引 + _, err = cacheDB.Exec(`CREATE INDEX IF NOT EXISTS idx_device_pool_valid ON device_pool(is_valid)`) + if err != nil { + poolLogger.Warning("创建索引失败: " + err.Error()) + } + + return nil +} + +// GetDeviceHeaderFromPool 从池中获取设备header(兼容旧接口) +func GetDeviceHeaderFromPool() string { + pool := GetDeviceKeyPool() + if !pool.IsEnabled() { + return "" + } + + item, err := pool.GetNext() + if err != nil { + poolLogger.Error("从池中获取设备失败: " + err.Error()) + return "" + } + + return item.DeviceHeader +} + +// GetKeyInfoFromPool 从池中获取密钥信息(兼容旧接口) +func GetKeyInfoFromPool(deviceHeader string) *KeyInfo { + pool := GetDeviceKeyPool() + if !pool.IsEnabled() { + return nil + } + + pool.mu.RLock() + defer pool.mu.RUnlock() + + for _, item := range pool.items { + if item.DeviceHeader == deviceHeader && item.IsValid { + return &KeyInfo{ + Key: item.Key, + Ver: item.KeyVer, + } + } + } + + return nil +} + +// UpdateKeyInPool 更新池中设备的密钥 +func UpdateKeyInPool(deviceHeader string, key string, ver int) error { + pool := GetDeviceKeyPool() + if !pool.IsEnabled() { + return nil + } + + pool.mu.Lock() + defer pool.mu.Unlock() + + for _, item := range pool.items { + if item.DeviceHeader == deviceHeader { + item.Key = key + item.KeyVer = ver + return pool.updateDeviceInDB(item) + } + } + + return fmt.Errorf("未找到设备") +} + +// GetAnyValidDeviceHeader 获取任意一个有效设备的header(用于辅助功能) +func GetAnyValidDeviceHeader() string { + pool := GetDeviceKeyPool() + if !pool.IsEnabled() { + return "" + } + + pool.mu.RLock() + defer pool.mu.RUnlock() + + for _, item := range pool.items { + if item.IsValid { + return item.DeviceHeader + } + } + + return "" +} + +// GetValidDevice 获取一个有效的设备 +func GetValidDevice() (*DeviceKeyItem, error) { + pool := GetDeviceKeyPool() + if !pool.IsEnabled() { + return nil, fmt.Errorf("设备池未启用") + } + return pool.GetNext() +} + +// GetCurrentDevice 获取当前使用的设备(和GetValidDevice一样,返回下一个可用设备) +func GetCurrentDevice() *DeviceKeyItem { + pool := GetDeviceKeyPool() + if !pool.IsEnabled() { + return nil + } + item, _ := pool.GetNext() + return item +} + diff --git a/internal/fqapi/device_register.go b/internal/fqapi/device_register.go new file mode 100644 index 0000000..d127a20 --- /dev/null +++ b/internal/fqapi/device_register.go @@ -0,0 +1,384 @@ +package fqapi + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fanqie/internal/common" + "fmt" + "math/big" + "net" + "os" + "runtime" + "sort" + "strconv" + "strings" + "time" + + "github.com/google/uuid" +) + +// 使用共享的设备配置 +var ( + androidVersions = DeviceConfig.AndroidVersions + brandModels = DeviceConfig.BrandModels + deviceBrands = DeviceConfig.DeviceBrands +) + +// generateUUID 生成UUID4字符串 +func generateUUID() string { + return uuid.New().String() +} + +// currentMillis 返回当前毫秒级时间戳 +func currentMillis() int64 { + return time.Now().UnixNano() / int64(time.Millisecond) +} + +// getCustomBT 获取设备开机时间戳 +// 模拟 Android 中的 m25453o() 方法,计算设备开机时间戳: +// custom_bt = current_millis - elapsed_ms_since_boot +func getCustomBT() string { + var uptimeSeconds float64 + + if runtime.GOOS == "linux" { + // 在Linux上读取/proc/uptime + data, err := os.ReadFile("/proc/uptime") + if err == nil { + // /proc/uptime格式: "系统运行时间(秒) 空闲时间(秒)" + parts := strings.Split(string(data), " ") + if len(parts) > 0 { + uptime, err := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64) + if err == nil { + uptimeSeconds = uptime + } + } + } + + // 如果读取失败,使用time.monotonic作为备选 + if uptimeSeconds == 0 { + // Go没有直接的monotonic API,使用启动时间估算 + uptimeSeconds = float64(time.Now().Unix() % 86400) + } + } else { + // 非Linux系统,使用估算值 + // 这里可以使用进程启动时间作为近似 + uptimeSeconds = float64(time.Now().Unix() % 86400) + } + + return fmt.Sprintf("%d", currentMillis()-int64(uptimeSeconds*1000)) +} + +// generateDeviceInfo 随机生成设备及操作系统信息 +func generateDeviceInfo() map[string]interface{} { + osVersion := androidVersions[randInt(len(androidVersions))] + + // 随机选择品牌 + deviceBrand := deviceBrands[randInt(len(deviceBrands))] + + // 根据品牌选择对应的型号 + models := brandModels[deviceBrand] + deviceModel := models[randInt(len(models))] + + releaseBytes := make([]byte, 7) + rand.Read(releaseBytes) + releaseBuild := hex.EncodeToString(releaseBytes) + + openUDIDBytes := make([]byte, 10) + rand.Read(openUDIDBytes) + + return map[string]interface{}{ + "os": "Android", + "os_version": osVersion[0], + "os_api": osVersion[1], + "device_model": deviceModel, + "device_brand": deviceBrand, + "device_manufacturer": deviceBrand, + "cpu_abi": "arm64-v8a", + "release_build": releaseBuild, + "cdid": generateUUID(), + "sig_hash": "a4a27c2633195374c15651ffc3c4a497", + "openudid": hex.EncodeToString(openUDIDBytes), + "clientudid": generateUUID(), + "req_id": generateUUID(), + } +} + +// randInt 生成随机整数 +func randInt(max int) int { + n, _ := rand.Int(rand.Reader, big.NewInt(int64(max))) + return int(n.Int64()) +} + +// randomString 生成随机字符串 +func randomString(length int) string { + const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + b := make([]byte, length) + for i := range b { + n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + b[i] = charset[n.Int64()] + } + return string(b) +} + +// generateOptionalFields 生成ROM信息等 +func generateOptionalFields() (map[string]interface{}, map[string]int64) { + rom := randomString(14) + part1 := randomString(4) + part2 := randomString(6) + part3 := randomString(8) + + genTime := currentMillis() + return map[string]interface{}{ + "rom": rom, + "rom_version": fmt.Sprintf("coloros__%s.%s.%s release-keys", part1, part2, part3), + "apk_first_install_time": genTime - int64(randInt(365))*86400000, + }, map[string]int64{ + "_gen_time": genTime, + } +} + +// generateIPv6AddressLinkLocal 生成链路本地IPv6地址 +func generateIPv6AddressLinkLocal() string { + ifaces, err := net.Interfaces() + if err != nil { + return "::1" + } + + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok { + continue + } + ip := ipNet.IP + if ip.To16() != nil && strings.HasPrefix(ip.String(), "fe80::") { + return strings.Split(ip.String(), "%")[0] + } + } + } + return "::1" +} + +// generateIPv6AddressUniqueLocal 生成唯一本地IPv6地址 +func generateIPv6AddressUniqueLocal() string { + b := make([]byte, 1) + rand.Read(b) + parts := []string{fmt.Sprintf("fd%02x", b[0])} + for i := 0; i < 7; i++ { + b := make([]byte, 2) + rand.Read(b) + parts = append(parts, fmt.Sprintf("%04x", uint16(b[0])<<8|uint16(b[1]))) + } + return strings.Join(parts, ":") +} + +// generateIPv6List 生成IPv6列表 +func generateIPv6List() []map[string]string { + return []map[string]string{ + {"type": "client_anpi", "value": generateIPv6AddressLinkLocal()}, + {"type": "client_anpi", "value": generateIPv6AddressUniqueLocal()}, + {"type": "client_anpi", "value": generateIPv6AddressUniqueLocal()}, + } +} + +// GenerateFullRequestBody 生成完整的请求体数据 +func GenerateFullRequestBody() map[string]interface{} { + rawData := map[string]interface{}{ + "magic_tag": "ss_app_log", + "header": map[string]interface{}{ + "display_name": "番茄免费小说", + "aid": 1967, + "channel": "43536163a", + "package": "com.dragon.read", + "sdk_version": "3.7.0-rc.25-fanqie-xiaoshuo", + "sdk_target_version": 29, + "git_hash": "711d1a7", + "density_dpi": 240, + "display_density": "hdpi", + "resolution": "720x1280", + "language": "zh", + "timezone": 8, + "access": "wifi", + "not_request_sender": 0, + "carrier": "CHINA MOBILE", + "mcc_mnc": "46000", + "region": "CN", + "tz_name": "Asia/Shanghai", + "tz_offset": 28800, + "sim_region": "cn", + "sim_serial_number": []interface{}{}, + "oaid_may_support": false, + "device_platform": "android", + "custom": map[string]interface{}{"host_bit": 32, "dragon_device_type": "pad"}, + "pre_installed_channel": "", + "is_system_app": 0, + "sdk_flavor": "china", + "guest_mode": 0, + }, + } + + deviceInfo := generateDeviceInfo() + optionalFields, genTime := generateOptionalFields() + + header := rawData["header"].(map[string]interface{}) + for k, v := range deviceInfo { + header[k] = v + } + for k, v := range optionalFields { + header[k] = v + } + header["ipv6_list"] = generateIPv6List() + + for k, v := range genTime { + rawData[k] = v + } + + return rawData +} + +// tryActivatePremium 尝试激活设备特权 +func tryActivatePremium(header string) { + logger := common.NewLogger("device_register", "INFO", "") + + // 构建查询参数 + baseParams := []string{ + "aid=1967", + "app_name=novelapp", + "channel=0", + "device_platform=android", + "os_version=0", + "version_code=58932", + "manifest_version_code=58932", + "update_version_code=58932", + header, + } + + // 合并并排序参数 + allParams := strings.Join(baseParams, "&") + paramList := strings.Split(allParams, "&") + sort.Strings(paramList) + sortedParams := strings.Join(paramList, "&") + + requestBody := map[string]interface{}{ + "add_count_daily": 0, + "amount": 2592000, + "privilege_id": 7210376203117531962, + "from": 8, + "unique_key": fmt.Sprintf("%d", time.Now().UnixNano()/1e6), + } + + bodyJSON, _ := json.Marshal(requestBody) + + url := "https://reading.snssdk.com/reading/user/privilege/add/v/?" + sortedParams + + resp, err := GorgonRequestSync(url, map[string]interface{}{ + "method": "post", + "headers": map[string]string{ + "content-type": "application/json", + }, + "body": bodyJSON, + }) + + if err != nil { + logger.Error("激活特权失败: " + err.Error()) + return + } + + var result map[string]interface{} + json.Unmarshal(resp.Content, &result) + + if code, ok := result["code"].(float64); ok && code == 0 { + logger.Info("已成功激活设备特权") + } else { + logger.Warning("设备特权激活失败...") + } +} + +// Register 设备注册 +func Register() (string, error) { + logger := common.NewLogger("device_register", "INFO", "") + + logger.Info("正在注册设备...") + + // 生成完整请求体数据 + data := GenerateFullRequestBody() + + // 只记录关键设备信息 + header := data["header"].(map[string]interface{}) + logger.Info(fmt.Sprintf("生成设备信息 - 品牌: %s, 型号: %s, 系统: Android %v", + header["device_brand"], + header["device_model"], + header["os_version"])) + + jsonData, err := json.Marshal(data) + if err != nil { + logger.Error("JSON序列化失败: " + err.Error()) + return "", err + } + + // 使用TTEncrypt加密 + encrypted, err := TTEncrypt(jsonData) + if err != nil { + logger.Error("加密失败: " + err.Error()) + return "", err + } + + url := "https://i.snssdk.com/service/2/device_register/?tt_data=a" + headers := map[string]string{ + "user-agent": "okhttp/4.10.0", + "content-type": "application/octet-stream; tt-data=a", + } + + client := common.NewHTTPClient() + resp, err := client.Post(url, encrypted, headers) + if err != nil { + logger.Warning("设备注册失败!") + logger.Warning(err.Error()) + return "", fmt.Errorf("设备注册错误") + } + + var respData map[string]interface{} + err = json.Unmarshal(resp.Content, &respData) + if err != nil { + logger.Error("响应解析失败: " + err.Error()) + return "", err + } + + if resp.Status == 200 { + // 检查device_id + deviceID, ok := respData["device_id"] + if !ok { + logger.Warning("设备注册失败!响应中没有device_id") + return "", fmt.Errorf("设备注册失败") + } + + // 转换device_id为float64并检查 + deviceIDFloat, ok := deviceID.(float64) + if !ok || deviceIDFloat == 0 { + logger.Warning("设备注册失败!device_id无效") + return "", fmt.Errorf("设备注册失败") + } + + deviceModel := data["header"].(map[string]interface{})["device_model"].(string) + header := fmt.Sprintf("device_id=%v&device_type=%s&iid=%v&version_name=5.8.9.32", + respData["device_id_str"], + deviceModel, + respData["install_id_str"]) + + logger.Info("设备注册成功!" ) + + // 尝试激活特权 + tryActivatePremium(header) + + return header, nil + } + + logger.Warning("设备注册失败!") + return "", fmt.Errorf("设备注册失败") +} + diff --git a/internal/fqapi/dhcry.go b/internal/fqapi/dhcry.go new file mode 100644 index 0000000..10589a8 --- /dev/null +++ b/internal/fqapi/dhcry.go @@ -0,0 +1,185 @@ +package fqapi + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "math/big" +) + +// CM 加密管理器 +type CM struct { + bigC *big.Int // 大素数C + bigB int64 // 底数B + arryD []byte // 固定密钥 + thisD [2]*big.Int // 密钥对(私钥, 公钥) + thisC []byte // 随机IV +} + +// NewCM 创建新的CM实例 +func NewCM() *CM { + // 大素数C + bigCStr := "2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919" + bigC := new(big.Int) + bigC.SetString(bigCStr, 10) + + // 固定密钥(base64解码) + arryD, _ := base64.StdEncoding.DecodeString("rCXGfd2POMGzeiNIgo4iLg==") + + return &CM{ + bigC: bigC, + bigB: 2, + arryD: arryD, + } +} + +// generateKeyPair 生成密钥对 +func (cm *CM) generateKeyPair() [2]*big.Int { + // 生成随机私钥 + randomBytes := make([]byte, 32) + rand.Read(randomBytes) + + x := new(big.Int).SetBytes(randomBytes) + x = x.Mod(x, new(big.Int).Sub(cm.bigC, big.NewInt(1))) + + // 计算公钥: y = b^x mod c + y := new(big.Int).Exp(big.NewInt(cm.bigB), x, cm.bigC) + + cm.thisD = [2]*big.Int{x, y} + return cm.thisD +} + +// pairToBytes 将公钥转换为字节数组 +func (cm *CM) pairToBytes(keyPair [2]*big.Int) []byte { + return keyPair[1].Bytes() +} + +// randIV 生成随机IV +func (cm *CM) randIV() []byte { + iv := make([]byte, 16) + rand.Read(iv) + return iv +} + +// encodeAES AES-CBC加密 +func (cm *CM) encodeAES(key, iv, data []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // PKCS7填充 + padding := aes.BlockSize - len(data)%aes.BlockSize + padText := make([]byte, padding) + for i := range padText { + padText[i] = byte(padding) + } + paddedData := append(data, padText...) + + ciphertext := make([]byte, len(paddedData)) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext, paddedData) + + return ciphertext, nil +} + +// decodeAES AES-CBC解密 +func (cm *CM) decodeAES(key, iv, data []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + mode := cipher.NewCBCDecrypter(block, iv) + decrypted := make([]byte, len(data)) + mode.CryptBlocks(decrypted, data) + + // 移除PKCS7填充 + padding := int(decrypted[len(decrypted)-1]) + if padding > len(decrypted) || padding > aes.BlockSize { + return nil, fmt.Errorf("无效的填充") + } + + return decrypted[:len(decrypted)-padding], nil +} + +// ClientHandshake 客户端握手,生成请求头y +func (cm *CM) ClientHandshake() string { + keyPair := cm.generateKeyPair() + cm.thisC = cm.randIV() + + publicKeyBytes := cm.pairToBytes(keyPair) + encrypted, _ := cm.encodeAES(cm.arryD, cm.thisC, publicKeyBytes) + + combined := append(cm.thisC, encrypted...) + return base64.StdEncoding.EncodeToString(combined) +} + +// decryptF 解密函数 +func (cm *CM) decryptF(peerPublicKey []byte, keyPair [2]*big.Int, iv, data []byte) ([]byte, error) { + x := keyPair[0] + + // 计算共享密钥: shared_secret = peer_public_key^x mod c + peerPubInt := new(big.Int).SetBytes(peerPublicKey) + sharedSecret := new(big.Int).Exp(peerPubInt, x, cm.bigC) + + key := sharedSecret.Bytes() + // 确保key长度为32字节 + if len(key) < 32 { + // 前面补0 + paddedKey := make([]byte, 32) + copy(paddedKey[32-len(key):], key) + key = paddedKey + } else if len(key) > 32 { + key = key[:32] + } + + return cm.decodeAES(key, iv, data) +} + +// Decrypt 解密响应内容 +func (cm *CM) Decrypt(c, y, content string) (string, error) { + // c 应该为 "1" 表示启用加密 + // y 是服务器返回的公钥 + // content 是加密的内容 + + if c != "1" || y == "" { + return content, nil + } + + // 解码content + contentDecoded, err := base64.StdEncoding.DecodeString(content) + if err != nil { + return "", err + } + + if len(contentDecoded) < 16 { + return "", fmt.Errorf("内容太短") + } + + // 提取IV和数据 + iv := contentDecoded[:16] + data := contentDecoded[16:] + + // 解码服务器公钥 + peerPublicKey, err := base64.StdEncoding.DecodeString(y) + if err != nil { + return "", err + } + + // 检查密钥对是否已初始化 + if cm.thisD[0] == nil { + return "", fmt.Errorf("密钥对未初始化") + } + + // 解密 + decrypted, err := cm.decryptF(peerPublicKey, cm.thisD, iv, data) + if err != nil { + return "", err + } + + return string(decrypted), nil +} + diff --git a/internal/fqapi/emoji.go b/internal/fqapi/emoji.go new file mode 100644 index 0000000..3f5c756 --- /dev/null +++ b/internal/fqapi/emoji.go @@ -0,0 +1,230 @@ +package fqapi + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +// EmojiManager 表情管理器 +type EmojiManager struct { + emojiMap map[string]string // 表情文本 -> 图片URL的映射 + mutex sync.RWMutex + lastUpdate time.Time + cacheExpiry time.Duration +} + +// DouyinEmojiResponse 抖音表情API响应结构 +type DouyinEmojiResponse struct { + EmojiList []DouyinEmoji `json:"emoji_list"` +} + +// DouyinEmoji 抖音单个表情结构 +type DouyinEmoji struct { + OriginURI string `json:"origin_uri"` + DisplayName string `json:"display_name"` + Hide int `json:"hide"` + EmojiURL struct { + URI string `json:"uri"` + URLList []string `json:"url_list"` + } `json:"emoji_url"` +} + +// LocalEmojiData 本地表情数据结构 +type LocalEmojiData struct { + Version int `json:"version"` + Size int `json:"size"` + Datas map[string]string `json:"datas"` +} + +var ( + globalEmojiManager *EmojiManager + emojiOnce sync.Once +) + +// GetEmojiManager 获取全局表情管理器实例 +func GetEmojiManager() *EmojiManager { + emojiOnce.Do(func() { + globalEmojiManager = &EmojiManager{ + emojiMap: make(map[string]string), + cacheExpiry: 24 * time.Hour, // 缓存24小时 + } + // 初始化时加载表情 + globalEmojiManager.LoadEmojis() + }) + return globalEmojiManager +} + +// LoadEmojis 加载表情映射(优先API,其次本地) +func (em *EmojiManager) LoadEmojis() error { + em.mutex.Lock() + defer em.mutex.Unlock() + + // 检查缓存是否过期 + if time.Since(em.lastUpdate) < em.cacheExpiry && len(em.emojiMap) > 0 { + return nil + } + + newMap := make(map[string]string) + + // 1. 先尝试从抖音API获取表情 + apiEmojis, err := em.fetchDouyinEmojis() + if err == nil && len(apiEmojis) > 0 { + for displayName, url := range apiEmojis { + newMap[displayName] = url + } + } + + // 2. 加载本地表情作为补充 + localEmojis, err := em.loadLocalEmojis() + if err == nil { + for displayName, url := range localEmojis { + // 如果API中没有,则使用本地的 + if _, exists := newMap[displayName]; !exists { + newMap[displayName] = url + } + } + } + + // 3. 更新缓存 + if len(newMap) > 0 { + em.emojiMap = newMap + em.lastUpdate = time.Now() + } + + return nil +} + +// fetchDouyinEmojis 从抖音API获取表情映射 +func (em *EmojiManager) fetchDouyinEmojis() (map[string]string, error) { + client := &http.Client{ + Timeout: 10 * time.Second, + } + + resp, err := client.Get("https://www.douyin.com/aweme/v1/web/emoji/list") + if err != nil { + return nil, fmt.Errorf("获取抖音表情API失败: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("抖音表情API返回状态码: %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取响应失败: %w", err) + } + + var apiResp DouyinEmojiResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + return nil, fmt.Errorf("解析JSON失败: %w", err) + } + + emojiMap := make(map[string]string) + for _, emoji := range apiResp.EmojiList { + if len(emoji.EmojiURL.URLList) > 0 { + // 使用第一个URL + emojiMap[emoji.DisplayName] = emoji.EmojiURL.URLList[0] + } + } + + return emojiMap, nil +} + +// loadLocalEmojis 从本地文件加载表情映射 +func (em *EmojiManager) loadLocalEmojis() (map[string]string, error) { + // 读取 data.json + dataPath := filepath.Join("emoji", "data.json") + data, err := ioutil.ReadFile(dataPath) + if err != nil { + return nil, fmt.Errorf("读取emoji/data.json失败: %w", err) + } + + var localData LocalEmojiData + if err := json.Unmarshal(data, &localData); err != nil { + return nil, fmt.Errorf("解析emoji/data.json失败: %w", err) + } + + emojiMap := make(map[string]string) + for id, displayName := range localData.Datas { + // 生成本地图片路径:emoji/images/emoji_dr_{id}.png + // 转换为相对URL路径 + localPath := fmt.Sprintf("/emoji/images/emoji_dr_%s.png", id) + emojiMap[displayName] = localPath + } + + return emojiMap, nil +} + +// GetEmojiURL 获取表情图片URL +func (em *EmojiManager) GetEmojiURL(displayName string) (string, bool) { + em.mutex.RLock() + defer em.mutex.RUnlock() + + url, exists := em.emojiMap[displayName] + return url, exists +} + +// GetAllEmojis 获取所有表情映射(用于生成JavaScript代码) +func (em *EmojiManager) GetAllEmojis() map[string]string { + em.mutex.RLock() + defer em.mutex.RUnlock() + + // 返回副本 + result := make(map[string]string, len(em.emojiMap)) + for k, v := range em.emojiMap { + result[k] = v + } + return result +} + +// RefreshEmojis 手动刷新表情缓存 +func (em *EmojiManager) RefreshEmojis() error { + em.mutex.Lock() + em.lastUpdate = time.Time{} // 重置更新时间 + em.mutex.Unlock() + + return em.LoadEmojis() +} + +// CheckLocalEmojiFiles 检查本地表情文件是否存在 +func CheckLocalEmojiFiles() error { + dataPath := filepath.Join("emoji", "data.json") + if _, err := os.Stat(dataPath); os.IsNotExist(err) { + return fmt.Errorf("emoji/data.json 文件不存在") + } + + imagesDir := filepath.Join("emoji", "images") + if _, err := os.Stat(imagesDir); os.IsNotExist(err) { + return fmt.Errorf("emoji/images 目录不存在") + } + + return nil +} + +// ReplaceEmojisInText 替换文本中的表情为HTML img标签 +func ReplaceEmojisInText(text string) string { + em := GetEmojiManager() + + // 匹配 [表情] 格式 + result := text + for displayName, url := range em.GetAllEmojis() { + // 转义特殊字符 + escapedName := strings.ReplaceAll(displayName, "[", "\\[") + escapedName = strings.ReplaceAll(escapedName, "]", "\\]") + + // 替换为 img 标签 + imgTag := fmt.Sprintf(`%s`, url, displayName) + result = strings.ReplaceAll(result, displayName, imgTag) + } + + return result +} + diff --git a/internal/fqapi/gorgon_utils.go b/internal/fqapi/gorgon_utils.go new file mode 100644 index 0000000..0207359 --- /dev/null +++ b/internal/fqapi/gorgon_utils.go @@ -0,0 +1,117 @@ +package fqapi + +import ( + "crypto/md5" + "encoding/hex" + "fanqie/internal/algos" + "fanqie/internal/common" +) + +// CreateMD5 创建MD5哈希 +func CreateMD5(s string) string { + hasher := md5.New() + hasher.Write([]byte(s)) + return hex.EncodeToString(hasher.Sum(nil)) +} + +// GorgonRequest Gorgon请求(使用签名切换器) +func GorgonRequest(urlStr string, options map[string]interface{}) (*common.ClientResponse, error) { + headers, ok := options["headers"].(map[string]string) + if !ok { + headers = make(map[string]string) + options["headers"] = headers + } + + dataMD5 := "" + if body, ok := options["body"].(string); ok && body != "" { + dataMD5 = CreateMD5(body) + } else if body, ok := options["body"].([]byte); ok && len(body) > 0 { + dataMD5 = CreateMD5(string(body)) + } + + if dataMD5 != "" { + headers["x-ss-stub"] = dataMD5 + } + + // 使用签名切换器(sign2 或 sign4)生成签名 + signedHeaders := algos.GlobalSignatureSwitcher.Sign(urlStr, headers) + + // 更新headers + for k, v := range signedHeaders { + headers[k] = v + } + + // 调用实际的HTTP请求 + client := common.NewHTTPClient() + return client.Request(urlStr, options) +} + +// GorgonRequestSync Gorgon同步请求(使用签名切换器) +func GorgonRequestSync(urlStr string, options map[string]interface{}) (*common.ClientResponse, error) { + headers, ok := options["headers"].(map[string]string) + if !ok { + headers = make(map[string]string) + options["headers"] = headers + } + + dataMD5 := "" + if body, ok := options["body"].(string); ok && body != "" { + dataMD5 = CreateMD5(body) + } else if body, ok := options["body"].([]byte); ok && len(body) > 0 { + dataMD5 = CreateMD5(string(body)) + } + + if dataMD5 != "" { + headers["x-ss-stub"] = dataMD5 + } + + // 使用签名切换器(sign2 或 sign4)生成签名 + signedHeaders := algos.GlobalSignatureSwitcher.Sign(urlStr, headers) + + // 删除x-ss-stub(如果存在) + if _, exists := signedHeaders["x-ss-stub"]; exists { + delete(signedHeaders, "x-ss-stub") + } + + // 更新headers + for k, v := range signedHeaders { + headers[k] = v + } + + // 调用实际的HTTP请求 + client := common.NewHTTPClient() + return client.Request(urlStr, options) +} + +// ContentRequest Content请求(硬编码使用Sign2,不受配置影响) +func ContentRequest(urlStr string, options map[string]interface{}) (*common.ClientResponse, error) { + headers, ok := options["headers"].(map[string]string) + if !ok { + headers = make(map[string]string) + options["headers"] = headers + } + + dataMD5 := "" + if body, ok := options["body"].(string); ok && body != "" { + dataMD5 = CreateMD5(body) + } else if body, ok := options["body"].([]byte); ok && len(body) > 0 { + dataMD5 = CreateMD5(string(body)) + } + + if dataMD5 != "" { + headers["x-ss-stub"] = dataMD5 + } + + // 硬编码使用 Sign2(不受配置影响) + signedHeaders := algos.Sign2Wrapper(urlStr, headers) + + // 更新headers + for k, v := range signedHeaders { + headers[k] = v + } + + // 调用实际的HTTP请求 + client := common.NewHTTPClient() + return client.Request(urlStr, options) +} + diff --git a/internal/fqapi/item_summary.go b/internal/fqapi/item_summary.go new file mode 100644 index 0000000..083877a --- /dev/null +++ b/internal/fqapi/item_summary.go @@ -0,0 +1,45 @@ +package fqapi + +import ( + "fanqie/internal/common" + "fmt" +) + +var itemSummaryLogger = common.NewLogger("item_summary", "INFO", "") + +// HandleItemSummary 处理获取章节摘要 +func HandleItemSummary(itemIDs, bookID string) (string, error) { + if itemIDs == "" || bookID == "" { + return BuildErrorResponseWithCode(100144, "参数错误"), nil + } + + reqURL := BuildURL( + "https://reading.snssdk.com/reading/reader/item_summary/mget/v", + "book_id="+bookID, + "item_ids="+itemIDs, + ) + + resp, err := GorgonRequest(reqURL, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + itemSummaryLogger.Error(fmt.Sprintf("获取章节摘要失败: %v", err)) + return BuildErrorResponse("请求失败"), err + } + + respJSON, err := resp.JSON() + if err != nil { + return BuildErrorResponse("解析响应失败"), err + } + + if code, ok := respJSON["code"].(float64); ok && code == 0 { + return resp.Text, nil + } + + return BuildErrorResponse("获取失败"), nil +} + diff --git a/internal/fqapi/manga.go b/internal/fqapi/manga.go new file mode 100644 index 0000000..20bd570 --- /dev/null +++ b/internal/fqapi/manga.go @@ -0,0 +1,616 @@ +package fqapi + +import ( + "bytes" + "compress/gzip" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "sync" + "time" + + "fanqie/internal/common" + "fanqie/internal/config" +) + +var ( + mangaLogger = common.NewLogger("manga", "INFO", "") + // 存储漫画图片信息的缓存(用于代理模式) + mangaImageCache = make(map[string]*MangaImageInfo) + mangaCacheMutex sync.RWMutex +) + +// MangaImageInfo 漫画图片信息 +type MangaImageInfo struct { + PicURL string + EncryptKey string + Width int + Height int + CachedAt time.Time +} + +// MangaData 漫画数据结构 +type MangaData struct { + PicInfos []PicInfo `json:"picInfos"` + EncryptKey string `json:"encrypt_key"` +} + +// PicInfo 图片信息 +type PicInfo struct { + PicURL string `json:"picUrl"` + Md5 string `json:"md5"` + Width int `json:"width"` + Height int `json:"height"` +} + +// ImageResult 图片结果 +type ImageResult struct { + Data string `json:"data"` + Width int `json:"width"` + Height int `json:"height"` + Format string `json:"format"` + Index int `json:"index"` +} + +// HandleGetManga 处理漫画请求 +func HandleGetManga(itemID string, showHTML bool, urlOutputMode string) (interface{}, error) { + return getMangaWithRetry(itemID, showHTML, urlOutputMode, 0) +} + +// getMangaWithRetry 获取漫画内容,支持设备切换重试 +func getMangaWithRetry(itemID string, showHTML bool, urlOutputMode string, retryCount int) (interface{}, error) { + // 防止无限递归 + const maxRetries = 5 + if retryCount >= maxRetries { + mangaLogger.Warning(fmt.Sprintf("已重试 %d 次,获取漫画失败", maxRetries)) + return nil, fmt.Errorf("已重试 %d 次,所有设备均失败", maxRetries) + } + + // 1. 从设备密钥池获取设备 + pool := GetDeviceKeyPool() + item, err := pool.GetNext() + if err != nil { + return nil, fmt.Errorf("从设备池获取设备失败: %w", err) + } + + header := item.DeviceHeader + keyInfo := &KeyInfo{ + Key: item.Key, + Ver: item.KeyVer, + } + mangaLogger.Debug(fmt.Sprintf("使用池中设备 ID=%d (重试次数: %d)", item.ID, retryCount)) + + // 2. 构建URL - 使用 BuildURL 添加设备参数 + headerWithoutVersion := strings.Replace(header, "&version_name=5.8.9.32", "", -1) + + url := BuildURL("https://reading.snssdk.com/reading/reader/full/v/", + "aid=1967", + "app_name=novelapp", + "channel=0", + "device_platform=android", + "version_code=66.9", + "item_id="+itemID, + "os_version=0", + headerWithoutVersion, + ) + + // 3. 调用API - 使用 GorgonRequest 添加签名 + response, err := GorgonRequest(url, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + if err != nil { + mangaLogger.Warning(fmt.Sprintf("API请求失败,切换设备重试: %v", err)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return getMangaWithRetry(itemID, showHTML, urlOutputMode, retryCount+1) + } + + // 4. 解析响应 + var body map[string]interface{} + if err := json.Unmarshal(response.Content, &body); err != nil { + mangaLogger.Warning(fmt.Sprintf("JSON解析失败,切换设备重试: %v", err)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return getMangaWithRetry(itemID, showHTML, urlOutputMode, retryCount+1) + } + + // 5. 获取加密的content + data, ok := body["data"].(map[string]interface{}) + if !ok { + mangaLogger.Warning("响应data字段无效,切换设备重试") + pool.MarkInvalid(header) + pool.RemoveInvalid() + return getMangaWithRetry(itemID, showHTML, urlOutputMode, retryCount+1) + } + + encryptedContent, ok := data["content"].(string) + if !ok || encryptedContent == "Invalid" { + mangaLogger.Warning(fmt.Sprintf("内容返回 Invalid,标记设备无效并切换设备 (重试: %d)", retryCount)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return getMangaWithRetry(itemID, showHTML, urlOutputMode, retryCount+1) + } + + // 检查密钥版本 + if keyVersion, ok := data["key_version"].(float64); ok { + if int(keyVersion) != keyInfo.Ver { + mangaLogger.Warning("密钥版本不匹配,重新获取密钥") + newKeyInfo, err := GetKey(header) + if err == nil { + UpdateKeyInPool(header, newKeyInfo.Key, newKeyInfo.Ver) + keyInfo = newKeyInfo + } else { + mangaLogger.Warning("获取密钥失败,切换设备重试") + pool.MarkInvalid(header) + pool.RemoveInvalid() + return getMangaWithRetry(itemID, showHTML, urlOutputMode, retryCount+1) + } + } + } + + // 6. 解密content + decrypted, err := decryptAESContent(encryptedContent, keyInfo.Key) + if err != nil { + mangaLogger.Warning(fmt.Sprintf("解密失败,切换设备重试: %v", err)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return getMangaWithRetry(itemID, showHTML, urlOutputMode, retryCount+1) + } + + // 7. 判断是否为漫画内容 + if strings.Contains(decrypted, "picInfos") { + // 是漫画,决定输出模式 + // 优先级:URL参数 > 配置文件 > 默认值 + outputMode := "proxy" // 默认值 + + // 从配置文件读取 + cfg := config.GetConfig() + if cfg != nil && cfg.Fanqie.Manga.OutputMode != "" { + outputMode = cfg.Fanqie.Manga.OutputMode + } + + // URL参数优先级最高 + if urlOutputMode != "" { + outputMode = urlOutputMode + } + + // 解析漫画图片(传入keyInfo用于解密模式) + images, err := parseMangaImages(decrypted, showHTML, outputMode, itemID, keyInfo) + if err != nil { + return nil, fmt.Errorf("解析漫画失败: %w", err) + } + return images, nil + } + + // 不是漫画,返回普通文本 + return map[string]interface{}{ + "content": decrypted, + }, nil +} + +// decryptAESContent 使用AES-128-CBC解密content +func decryptAESContent(encrypted, secretKey string) (string, error) { + // Base64解码加密内容 + raw, err := base64.StdEncoding.DecodeString(encrypted) + if err != nil { + return "", fmt.Errorf("Base64解码失败: %w", err) + } + + if len(raw) < 16 { + return "", fmt.Errorf("数据长度不足") + } + + // 提取IV和密文 + iv := raw[:16] + ciphertext := raw[16:] + + // Base64解码密钥 + keyBytes, err := base64.StdEncoding.DecodeString(secretKey) + if err != nil { + return "", fmt.Errorf("密钥解码失败: %w", err) + } + + if len(keyBytes) != 16 { + return "", fmt.Errorf("密钥长度必须为16字节,实际: %d", len(keyBytes)) + } + + // 创建AES cipher + block, err := aes.NewCipher(keyBytes) + if err != nil { + return "", fmt.Errorf("创建cipher失败: %w", err) + } + + // CBC模式解密 + mode := cipher.NewCBCDecrypter(block, iv) + plaintext := make([]byte, len(ciphertext)) + mode.CryptBlocks(plaintext, ciphertext) + + // PKCS7 unpadding + plaintext = pkcs7Unpad(plaintext) + + // 尝试gzip解压 + if len(plaintext) > 2 && plaintext[0] == 0x1f && plaintext[1] == 0x8b { + // 是gzip压缩的数据 + reader, err := gzip.NewReader(bytes.NewReader(plaintext)) + if err == nil { + defer reader.Close() + decompressed, err := io.ReadAll(reader) + if err == nil { + return string(decompressed), nil + } + } + } + + return string(plaintext), nil +} + +// pkcs7Unpad PKCS7 unpadding +func pkcs7Unpad(data []byte) []byte { + length := len(data) + if length == 0 { + return data + } + unpadding := int(data[length-1]) + if unpadding > length { + return data + } + return data[:(length - unpadding)] +} + +// buildMangaResponse 构建漫画响应(统一HTML和JSON格式输出) +func buildMangaResponse(images []ImageResult, showHTML bool) interface{} { + if showHTML { + var htmlContent strings.Builder + for _, img := range images { + htmlContent.WriteString(fmt.Sprintf(`图片%d`, + img.Data, img.Width, img.Height, img.Index)) + htmlContent.WriteString("\n") + } + return map[string]interface{}{ + "content": htmlContent.String(), + } + } + + return map[string]interface{}{ + "images": images, + } +} + +// processURLMode 处理URL模式(MD5直链) +func processURLMode(mangaData *MangaData) []ImageResult { + baseDomain := "https://p6-novel.byteimg.com/origin/" + validPath := detectMangaPath(baseDomain, mangaData.PicInfos[0].Md5) + + images := make([]ImageResult, len(mangaData.PicInfos)) + for i, pic := range mangaData.PicInfos { + imageURL := baseDomain + validPath + pic.Md5 + images[i] = ImageResult{ + Data: imageURL, + Width: pic.Width, + Height: pic.Height, + Format: "unknown", + Index: i, + } + } + return images +} + +// processProxyMode 处理代理模式(生成短URL并缓存) +func processProxyMode(mangaData *MangaData, itemID string) []ImageResult { + images := make([]ImageResult, len(mangaData.PicInfos)) + for i, pic := range mangaData.PicInfos { + cacheKey := fmt.Sprintf("%s_%d", itemID, i) + + // 存储图片信息到缓存 + mangaCacheMutex.Lock() + mangaImageCache[cacheKey] = &MangaImageInfo{ + PicURL: pic.PicURL, + EncryptKey: mangaData.EncryptKey, + Width: pic.Width, + Height: pic.Height, + CachedAt: time.Now(), + } + mangaCacheMutex.Unlock() + + // 返回短的代理URL + imageData := fmt.Sprintf("/manga/image/%s/%d", itemID, i) + images[i] = ImageResult{ + Data: imageData, + Width: pic.Width, + Height: pic.Height, + Format: "unknown", + Index: i, + } + } + return images +} + +// fillMangaCache 填充漫画图片缓存(供所有模式使用) +func fillMangaCache(mangaData *MangaData, itemID string) { + for i, pic := range mangaData.PicInfos { + cacheKey := fmt.Sprintf("%s_%d", itemID, i) + + // 检查是否已经存在,避免覆盖 + mangaCacheMutex.RLock() + _, exists := mangaImageCache[cacheKey] + mangaCacheMutex.RUnlock() + + if exists { + continue + } + + // 存储图片信息到缓存 + mangaCacheMutex.Lock() + mangaImageCache[cacheKey] = &MangaImageInfo{ + PicURL: pic.PicURL, + EncryptKey: mangaData.EncryptKey, + Width: pic.Width, + Height: pic.Height, + CachedAt: time.Now(), + } + mangaCacheMutex.Unlock() + } +} + +// processDataURIMode 处理DataURI模式(下载、解密、Base64编码) +func processDataURIMode(mangaData *MangaData) []ImageResult { + keyBytes, err := hex.DecodeString(mangaData.EncryptKey) + if err != nil { + mangaLogger.Warning(fmt.Sprintf("密钥解码失败: %v", err)) + return []ImageResult{} + } + + images := make([]ImageResult, 0, len(mangaData.PicInfos)) + for i, pic := range mangaData.PicInfos { + // 下载加密图片 + encryptedData, err := downloadImageWithRetry(pic.PicURL, 3) + if err != nil { + mangaLogger.Warning(fmt.Sprintf("下载图片失败 [%d]: %v", i, err)) + continue + } + + // 解密图片 + decryptedData, err := decryptImage(encryptedData, keyBytes) + if err != nil { + mangaLogger.Warning(fmt.Sprintf("解密图片失败 [%d]: %v", i, err)) + continue + } + + // 识别格式 + format := getImageFormat(decryptedData, pic.PicURL) + + // Base64编码图片数据 + base64Data := base64.StdEncoding.EncodeToString(decryptedData) + + // Data URI模式:返回完整的Base64编码 + mimeType := fmt.Sprintf("image/%s", format) + if format == "jpg" { + mimeType = "image/jpeg" + } + imageData := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data) + + images = append(images, ImageResult{ + Data: imageData, + Width: pic.Width, + Height: pic.Height, + Format: format, + Index: i, + }) + } + return images +} + +// parseMangaImages 解析并处理漫画图片 +func parseMangaImages(content string, showHTML bool, outputMode string, itemID string, keyInfo *KeyInfo) (interface{}, error) { + // 提取JSON部分 + jsonStart := strings.Index(content, "{") + jsonEnd := strings.LastIndex(content, "}") + if jsonStart == -1 || jsonEnd == -1 { + return nil, fmt.Errorf("无法找到JSON数据") + } + + jsonStr := content[jsonStart : jsonEnd+1] + var mangaData MangaData + if err := json.Unmarshal([]byte(jsonStr), &mangaData); err != nil { + return nil, fmt.Errorf("JSON解析失败: %w", err) + } + + if len(mangaData.PicInfos) == 0 { + return nil, fmt.Errorf("未找到图片信息") + } + + if mangaData.EncryptKey == "" { + return nil, fmt.Errorf("缺少加密密钥") + } + + // 根据不同模式处理图片 + var images []ImageResult + switch outputMode { + case "url": + images = processURLMode(&mangaData) + case "proxy": + images = processProxyMode(&mangaData, itemID) + default: // "data_uri" + images = processDataURIMode(&mangaData) + } + + // 无论什么模式,都填充缓存以便后续可以通过 /manga/image 访问 + fillMangaCache(&mangaData, itemID) + + return buildMangaResponse(images, showHTML), nil +} + +// downloadImageWithRetry 带重试的图片下载 +func downloadImageWithRetry(url string, maxRetries int) ([]byte, error) { + var lastErr error + + for i := 0; i <= maxRetries; i++ { + data, err := downloadImage(url) + if err == nil && len(data) > 100 { + return data, nil + } + lastErr = err + if i < maxRetries { + // 重试间隔递增(2秒、4秒、6秒) + time.Sleep(time.Duration(i*2+2) * time.Second) + } + } + + return nil, fmt.Errorf("下载失败(已重试%d次): %w", maxRetries, lastErr) +} + +// downloadImage 下载图片 +func downloadImage(url string) ([]byte, error) { + // 创建专用的图片下载客户端,超时时间更长 + client := &http.Client{ + Timeout: 120 * time.Second, + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + // 使用与API一致的请求头 + req.Header.Set("User-Agent", "okhttp/4.12.0") + req.Header.Set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8") + req.Header.Set("Accept-Encoding", "gzip, deflate") + req.Header.Set("Referer", "https://fanqie.novel.com/") + req.Header.Set("Origin", "https://fanqie.novel.com") + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("下载失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("HTTP状态码: %d", resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} + +// decryptImage 解密图片数据(AES-GCM) +func decryptImage(encryptedData, keyBytes []byte) ([]byte, error) { + // 加密格式: IV(12字节) + Ciphertext + Tag(16字节) + dataLen := len(encryptedData) + if dataLen < 28 { + return nil, fmt.Errorf("数据长度不足(至少需要28字节)") + } + + // 提取各部分 + iv := encryptedData[:12] + tag := encryptedData[dataLen-16:] + ciphertext := encryptedData[12 : dataLen-16] + + // 创建AES cipher + block, err := aes.NewCipher(keyBytes) + if err != nil { + return nil, fmt.Errorf("创建cipher失败: %w", err) + } + + // 创建GCM mode + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("创建GCM失败: %w", err) + } + + // 组合ciphertext和tag(Go的GCM要求tag附加在ciphertext后面) + ciphertextWithTag := append(ciphertext, tag...) + + // 解密 + plaintext, err := aesGCM.Open(nil, iv, ciphertextWithTag, nil) + if err != nil { + return nil, fmt.Errorf("解密失败: %w", err) + } + + return plaintext, nil +} + +// getImageFormat 识别图片格式 +// detectMangaPath 探测正确的漫画图片路径(novel-images/ 或 novel-pic/) +func detectMangaPath(baseDomain, firstMd5 string) string { + // 优先尝试 novel-images/ + testURL := baseDomain + "novel-images/" + firstMd5 + + client := &http.Client{ + Timeout: 8 * time.Second, + } + + resp, err := client.Get(testURL) + if err != nil { + // 请求失败,使用novel-pic/ + return "novel-pic/" + } + defer resp.Body.Close() + + // 读取响应内容 + body, err := io.ReadAll(resp.Body) + if err != nil { + return "novel-pic/" + } + + // 检查是否为JSON响应(错误响应通常是JSON) + var jsonData interface{} + if json.Unmarshal(body, &jsonData) == nil { + // 是JSON,说明路径无效,切换到novel-pic/ + return "novel-pic/" + } + + // 不是JSON,说明返回的是图片,路径有效 + return "novel-images/" +} + +func getImageFormat(data []byte, url string) string { + // 首先尝试从URL提取扩展名 + if idx := strings.LastIndex(url, "."); idx != -1 { + ext := strings.ToLower(url[idx+1:]) + // 移除query参数 + if qIdx := strings.Index(ext, "?"); qIdx != -1 { + ext = ext[:qIdx] + } + if ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "gif" || ext == "webp" { + return ext + } + } + + // 通过文件头识别 + if len(data) < 16 { + return "png" + } + + // JPEG: FF D8 FF + if bytes.HasPrefix(data, []byte{0xFF, 0xD8, 0xFF}) { + return "jpg" + } + + // PNG: 89 50 4E 47 + if bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47}) { + return "png" + } + + // GIF: 47 49 46 38 + if bytes.HasPrefix(data, []byte{0x47, 0x49, 0x46, 0x38}) { + return "gif" + } + + // WEBP: RIFF...WEBP + if bytes.HasPrefix(data, []byte{0x52, 0x49, 0x46, 0x46}) && + len(data) >= 12 && + bytes.Equal(data[8:12], []byte{0x57, 0x45, 0x42, 0x50}) { + return "webp" + } + + return "png" +} + diff --git a/internal/fqapi/manga_proxy.go b/internal/fqapi/manga_proxy.go new file mode 100644 index 0000000..13b5ab3 --- /dev/null +++ b/internal/fqapi/manga_proxy.go @@ -0,0 +1,72 @@ +package fqapi + +import ( + "encoding/hex" + "fmt" + "time" +) + +// HandleMangaImageProxy 处理漫画图片代理请求 +func HandleMangaImageProxy(itemID string, index int) ([]byte, string, error) { + // 从缓存获取图片信息 + cacheKey := fmt.Sprintf("%s_%d", itemID, index) + + mangaCacheMutex.RLock() + imageInfo, exists := mangaImageCache[cacheKey] + mangaCacheMutex.RUnlock() + + if !exists { + return nil, "", fmt.Errorf("图片信息不存在,请先调用 /manga 接口") + } + + // 检查缓存是否过期(5分钟) + if time.Since(imageInfo.CachedAt) > 5*time.Minute { + // 清理过期缓存 + mangaCacheMutex.Lock() + delete(mangaImageCache, cacheKey) + mangaCacheMutex.Unlock() + return nil, "", fmt.Errorf("图片信息已过期,请重新调用 /manga 接口") + } + + // 下载加密图片 + encryptedData, err := downloadImageWithRetry(imageInfo.PicURL, 2) + if err != nil { + return nil, "", fmt.Errorf("下载图片失败: %w", err) + } + + // 解密图片 + keyBytes, err := hex.DecodeString(imageInfo.EncryptKey) + if err != nil { + return nil, "", fmt.Errorf("密钥解码失败: %w", err) + } + + decryptedData, err := decryptImage(encryptedData, keyBytes) + if err != nil { + return nil, "", fmt.Errorf("解密失败: %w", err) + } + + // 识别格式 + format := getImageFormat(decryptedData, imageInfo.PicURL) + + // 返回图片数据和MIME类型 + mimeType := fmt.Sprintf("image/%s", format) + if format == "jpg" { + mimeType = "image/jpeg" + } + + return decryptedData, mimeType, nil +} + +// CleanExpiredMangaCache 清理过期的漫画图片缓存 +func CleanExpiredMangaCache() { + mangaCacheMutex.Lock() + defer mangaCacheMutex.Unlock() + + now := time.Now() + for key, info := range mangaImageCache { + if now.Sub(info.CachedAt) > 5*time.Minute { + delete(mangaImageCache, key) + } + } +} + diff --git a/internal/fqapi/media_utils.go b/internal/fqapi/media_utils.go new file mode 100644 index 0000000..415ed72 --- /dev/null +++ b/internal/fqapi/media_utils.go @@ -0,0 +1,192 @@ +package fqapi + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + "net/http" + "path/filepath" + "strings" + "time" +) + +// generateRandomKey 生成随机key +func generateRandomKey(length int) string { + b := make([]byte, length/2) + rand.Read(b) + return hex.EncodeToString(b) +} + +// extractDeviceParam 从deviceHeader字符串中提取指定参数 +func extractDeviceParam(deviceHeader, paramName string) string { + // deviceHeader 格式: device_id=xxx&device_type=xxx&iid=xxx&version_name=xxx + parts := strings.Split(deviceHeader, "&") + for _, part := range parts { + kv := strings.Split(part, "=") + if len(kv) == 2 && kv[0] == paramName { + return kv[1] + } + } + return "" +} + +// downloadMediaWithRetry 带重试的媒体下载,自动切换设备 +func downloadMediaWithRetry(url, deviceID string, headers map[string]string, maxRetries int) ([]byte, string, error) { + var lastErr error + currentDeviceID := deviceID + + for i := 0; i <= maxRetries; i++ { + data, contentType, err := downloadMedia(url, currentDeviceID, headers) + if err == nil && len(data) > 0 { + return data, contentType, nil + } + lastErr = err + + if i < maxRetries { + // 重试时切换到新的有效设备 + newDevice, err := GetValidDevice() + if err == nil { + currentDeviceID = newDevice.DeviceHeader + } + // 重试间隔递增(2秒、4秒、6秒) + time.Sleep(time.Duration(i*2+2) * time.Second) + } + } + + return nil, "", fmt.Errorf("下载失败(已重试%d次): %w", maxRetries, lastErr) +} + +// downloadMedia 下载媒体内容 +func downloadMedia(url, deviceID string, headers map[string]string) ([]byte, string, error) { + // 创建专用的媒体下载客户端,超时时间更长 + client := &http.Client{ + Timeout: 300 * time.Second, // 5分钟超时,适合大文件 + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, "", err + } + + // 设置CDN请求头,模拟安卓客户端请求 + req.Header.Set("User-Agent", "com.dragon.read/7.9.9.32 (Linux; U; Android 13; zh_CN; M2102K1AC; Build/TKQ1.220807.001; Cronet/113.0.5672.62)") + req.Header.Set("Accept", "*/*") + req.Header.Set("Accept-Encoding", "identity") + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Host", req.URL.Host) + + // 添加设备信息(如果提供了deviceID) + if deviceID != "" { + req.Header.Set("X-Device-ID", extractDeviceParam(deviceID, "device_id")) + req.Header.Set("X-Device-Type", extractDeviceParam(deviceID, "device_type")) + req.Header.Set("X-IID", extractDeviceParam(deviceID, "iid")) + req.Header.Set("X-Version-Name", extractDeviceParam(deviceID, "version_name")) + } + + // 添加自定义请求头 + for k, v := range headers { + req.Header.Set(k, v) + } + + resp, err := client.Do(req) + if err != nil { + return nil, "", fmt.Errorf("下载失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, "", fmt.Errorf("HTTP状态码: %d", resp.StatusCode) + } + + // 读取内容 + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, "", fmt.Errorf("读取内容失败: %w", err) + } + + // 获取Content-Type + contentType := resp.Header.Get("Content-Type") + if contentType == "" { + // 根据文件后缀推断 + contentType = getContentTypeFromURL(url) + } + + return data, contentType, nil +} + +// getContentTypeFromURL 根据URL推断Content-Type +func getContentTypeFromURL(url string) string { + ext := strings.ToLower(filepath.Ext(url)) + switch ext { + case ".mp3", ".m4a", ".aac": + return "audio/mpeg" + case ".mp4", ".m4v", ".mov": + return "video/mp4" + case ".flv": + return "video/x-flv" + case ".ts": + return "video/MP2T" + case ".webm": + return "video/webm" + case ".ogg": + return "audio/ogg" + default: + return "application/octet-stream" + } +} + +// DetectMediaType 检测媒体类型 +func DetectMediaType(data []byte) string { + if len(data) < 4 { + return "unknown" + } + + // 检测音频格式 + if data[0] == 0xFF && (data[1] == 0xFB || data[1] == 0xF3 || data[1] == 0xF2) { + return "audio/mpeg" // MP3 + } + if string(data[:4]) == "ftyp" || string(data[:4]) == "moov" || string(data[:4]) == "mdat" { + return "video/mp4" // MP4/M4A + } + if string(data[:4]) == "OggS" { + return "audio/ogg" // OGG + } + if string(data[:4]) == "RIFF" && string(data[8:12]) == "WAVE" { + return "audio/wav" // WAV + } + + // 检测视频格式 + if string(data[:3]) == "FLV" { + return "video/x-flv" // FLV + } + if string(data[:4]) == "webm" { + return "video/webm" // WebM + } + + return "unknown" +} + +// processMediaURL 处理媒体URL,根据output模式返回不同的URL(仅支持音频) +func processMediaURL(originalURL, deviceID string, headers map[string]string, outputMode string, mediaType string) interface{} { + switch outputMode { + case "proxy": + // 生成代理URL + if mediaType == "audio" { + return GenerateAudioProxyURL(originalURL, deviceID, headers) + } + // 其他类型返回原始URL + return originalURL + case "data_uri": + // 直接下载并返回base64 + data, contentType, err := downloadMediaWithRetry(originalURL, deviceID, headers, 2) + if err != nil { + return originalURL + } + return fmt.Sprintf("data:%s;base64,%s", contentType, base64.StdEncoding.EncodeToString(data)) + default: + // 直接返回原始URL + return originalURL + } +} diff --git a/internal/fqapi/multi_content.go b/internal/fqapi/multi_content.go new file mode 100644 index 0000000..7753ab4 --- /dev/null +++ b/internal/fqapi/multi_content.go @@ -0,0 +1,273 @@ +package fqapi + +import ( + "bytes" + "compress/zlib" + "encoding/base64" + "encoding/json" + "fanqie/internal/common" + "fmt" + "io" + "sort" + "strings" + "sync" +) + +var ( + multiContentLogger = common.NewLogger("multi_content", "INFO", "") +) + +// handleDecryptTask 处理解密任务 +func handleDecryptTask(itemID string, itemData map[string]interface{}, keyInfo *KeyInfo, textMode, imageMode bool) (string, map[string]interface{}) { + if code, ok := itemData["code"].(float64); !ok || code != 0 { + return itemID, itemData + } + + contentEncoded, ok := itemData["content"].(string) + if !ok { + return itemID, itemData + } + + // 解码content + contentBytes, err := base64.StdEncoding.DecodeString(contentEncoded) + if err != nil || len(contentBytes) < 16 { + return itemID, itemData + } + + iv := contentBytes[0:16] + encryptedData := contentBytes[16:] + + // 解密 + if keyInfo == nil { + return itemID, itemData + } + + keyBytes, _ := base64.StdEncoding.DecodeString(keyInfo.Key) + decrypted, err := De(encryptedData, keyBytes, iv) + if err != nil { + return itemID, itemData + } + + // 检查是否需要解压 + compressStatus, _ := itemData["compress_status"].(float64) + if compressStatus != 0 { + // zlib解压 + reader, err := zlib.NewReader(bytes.NewReader(decrypted)) + if err == nil { + decompressed, err := io.ReadAll(reader) + reader.Close() + if err == nil { + decrypted = decompressed + } + } + } + + // 更新内容 + bodyCopy := make(map[string]interface{}) + for k, v := range itemData { + bodyCopy[k] = v + } + bodyCopy["content"] = string(decrypted) + + // 缓存 + cacheData := buildCacheData(bodyCopy) + saveCacheJSON("content_"+itemID, cacheData, 86400) + + // 解析内容 + parser := NewContentParser() + bodyCopy["content"] = parser.Parse(string(decrypted), textMode, imageMode) + + return itemID, bodyCopy +} + +// MultiContent 批量获取内容 +func MultiContent(itemIDs interface{}, bookID string, textMode, imageMode, ignoreFailed interface{}, depth int) (string, error) { + // 参数转换 + textModeBool := parseBoolParam(textMode) + imageModeBool := parseBoolParamDefault(imageMode, true) + ignoreFailedBool := parseBoolParamDefault(ignoreFailed, true) + + if depth > 3 { + return BuildErrorResponse("获取失败,请重试!"), nil + } + + if bookID == "" { + return BuildErrorResponse("需要book_id!"), nil + } + + if itemIDs == nil || itemIDs == "" { + return BuildSuccessResponse([]interface{}{}), nil + } + + // 转换itemIDs + var itemIDStr string + switch v := itemIDs.(type) { + case string: + itemIDStr = v + case []string: + itemIDStr = strings.Join(v, ",") + default: + itemIDStr = fmt.Sprintf("%v", v) + } + + // 从设备密钥池获取设备 + pool := GetDeviceKeyPool() + item, err := pool.GetNext() + if err != nil { + return `{"code": -1, "msg": "从设备池获取设备失败"}`, err + } + + header := item.DeviceHeader + currentKeyInfo := &KeyInfo{ + Key: item.Key, + Ver: item.KeyVer, + } + multiContentLogger.Debug(fmt.Sprintf("使用池中设备 ID=%d (depth: %d)", item.ID, depth)) + + // 构建参数 + headerWithoutVersion := removeVersionFromHeader(header) + + params := []string{ + "aid=1967", + "app_name=novelapp", + "channel=0", + "device_platform=android", + "os_version=0", + "version_code=66.9", + "book_id=" + bookID, + "item_ids=" + itemIDStr, + "novel_text_type=1", + "req_type=1", + headerWithoutVersion, + } + + allParams := strings.Join(params, "&") + paramList := strings.Split(allParams, "&") + sort.Strings(paramList) + sortedParams := strings.Join(paramList, "&") + + url := "https://reading.snssdk.com/reading/reader/batch_full/v/?" + sortedParams + + resp, err := ContentRequest(url, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + multiContentLogger.Warning(fmt.Sprintf("批量请求失败,标记设备无效并切换设备 (depth: %d): %v", depth, err)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) + } + + var data map[string]interface{} + json.Unmarshal(resp.Content, &data) + + if code, ok := data["code"].(float64); ok && code == 0 { + dataMap, ok := data["data"].(map[string]interface{}) + if !ok { + multiContentLogger.Warning(fmt.Sprintf("响应data字段无效,切换设备重试 (depth: %d)", depth)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) + } + + // 检查是否有Invalid内容或密钥版本不匹配 + needRetry := false + for _, itemData := range dataMap { + itemMap, ok := itemData.(map[string]interface{}) + if !ok { + continue + } + + itemCode, _ := itemMap["code"].(float64) + if itemCode != 0 && !ignoreFailedBool { + multiContentLogger.Warning(fmt.Sprintf("item code异常: %.0f,切换设备重试 (depth: %d)", itemCode, depth)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) + } + + if content, ok := itemMap["content"].(string); ok && content == "Invalid" { + // 标记设备无效并移除 + multiContentLogger.Warning(fmt.Sprintf("内容返回 Invalid,标记设备无效并切换设备 (depth: %d)", depth)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + needRetry = true + break + } + + if keyVersion, ok := itemMap["key_version"].(float64); ok { + if currentKeyInfo != nil && int(keyVersion) != currentKeyInfo.Ver { + // 更新密钥版本 + multiContentLogger.Warning("密钥版本不匹配,重新获取密钥") + newKeyInfo, err := GetKey(header) + if err == nil { + UpdateKeyInPool(header, newKeyInfo.Key, newKeyInfo.Ver) + } else { + // 获取密钥失败,切换设备 + multiContentLogger.Warning(fmt.Sprintf("获取密钥失败,切换设备重试 (depth: %d)", depth)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + } + needRetry = true + break + } + } + } + + if needRetry { + return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) + } + + // 并发解密 + results := make(map[string]map[string]interface{}) + var mutex sync.Mutex + var wg sync.WaitGroup + + for itemID, itemData := range dataMap { + itemMap, ok := itemData.(map[string]interface{}) + if !ok { + continue + } + + wg.Add(1) + go func(id string, data map[string]interface{}) { + defer wg.Done() + _, decrypted := handleDecryptTask(id, data, currentKeyInfo, textModeBool, imageModeBool) + mutex.Lock() + results[id] = decrypted + mutex.Unlock() + }(itemID, itemMap) + } + + wg.Wait() + + // 检查结果数量 + itemCount := len(strings.Split(itemIDStr, ",")) + if len(results) == 1 && itemCount != 1 { + multiContentLogger.Warning(fmt.Sprintf("结果数量异常,切换设备重试 (depth: %d)", depth)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) + } + + // 构建响应 + response := buildCacheData(results) + return buildJSONResponse(response), nil + } + + // 响应code不为0,切换设备重试 + multiContentLogger.Warning(fmt.Sprintf("响应code异常,切换设备重试 (depth: %d)", depth)) + pool.MarkInvalid(header) + pool.RemoveInvalid() + return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) +} + +// HandleMultiContent 处理批量获取内容 +func HandleMultiContent(itemIDs interface{}, bookID string, textMode, imageMode, ignoreFailed interface{}) (string, error) { + return MultiContent(itemIDs, bookID, textMode, imageMode, ignoreFailed, 0) +} + diff --git a/internal/fqapi/multi_detail.go b/internal/fqapi/multi_detail.go new file mode 100644 index 0000000..d9ea9cf --- /dev/null +++ b/internal/fqapi/multi_detail.go @@ -0,0 +1,215 @@ +package fqapi + +import ( + "encoding/json" + "fanqie/internal/common" + "fmt" + "strings" + "sync" +) + +var multiDetailLogger = common.NewLogger("multi_detail", "INFO", "") + +// singleDetail 获取单个书籍详情 +func singleDetail(bookID string) (map[string]interface{}, error) { + reqURL := BuildURL("https://reading.snssdk.com/reading/bookapi/detail/v/", "book_id="+bookID) + + resp, err := GorgonRequest(reqURL, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + return map[string]interface{}{}, err + } + + respJSON, err := resp.JSON() + if err != nil { + return map[string]interface{}{}, err + } + + if code, ok := respJSON["code"].(float64); ok && code == 0 { + if data, ok := respJSON["data"].(map[string]interface{}); ok { + delete(data, "content") + return data, nil + } + } + + return map[string]interface{}{}, nil +} + +// multiDetail 批量获取书籍详情 +func multiDetail(bookIDs string) ([]map[string]interface{}, error) { + reqURL := BuildURL("https://reading.snssdk.com/reading/bookapi/multi-detail/v/", "book_id="+bookIDs) + + resp, err := GorgonRequest(reqURL, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + // 返回空列表 + count := len(strings.Split(bookIDs, ",")) + result := make([]map[string]interface{}, count) + for i := range result { + result[i] = map[string]interface{}{} + } + return result, err + } + + respJSON, err := resp.JSON() + if err != nil { + count := len(strings.Split(bookIDs, ",")) + result := make([]map[string]interface{}, count) + for i := range result { + result[i] = map[string]interface{}{} + } + return result, err + } + + if code, ok := respJSON["code"].(float64); ok && code == 0 { + if data, ok := respJSON["data"].([]interface{}); ok && len(data) > 0 { + result := make([]map[string]interface{}, len(data)) + for i, item := range data { + if itemMap, ok := item.(map[string]interface{}); ok { + result[i] = itemMap + } else { + result[i] = map[string]interface{}{} + } + } + return result, nil + } + } + + // 返回空列表 + count := len(strings.Split(bookIDs, ",")) + result := make([]map[string]interface{}, count) + for i := range result { + result[i] = map[string]interface{}{} + } + return result, nil +} + +// chunkArray 分块数组 +func chunkArray(arr []string, size int) [][]string { + chunks := make([][]string, 0) + for i := 0; i < len(arr); i += size { + end := i + size + if end > len(arr) { + end = len(arr) + } + chunks = append(chunks, arr[i:end]) + } + return chunks +} + +// HandleGetMultiDetail 处理批量获取书籍详情 +func HandleGetMultiDetail(bookID interface{}) (string, error) { + if bookID == nil { + return BuildErrorResponse("需要参数 book_id"), nil + } + + var bookIDList []string + + switch v := bookID.(type) { + case string: + if v == "" { + return BuildErrorResponse("需要参数 book_id"), nil + } + bookIDList = strings.Split(v, ",") + case []string: + bookIDList = v + default: + return BuildErrorResponse("book_id 参数格式错误"), nil + } + + // 分块处理,每批100个 + chunks := chunkArray(bookIDList, 100) + + // 并发请求 + type result struct { + data []map[string]interface{} + err error + } + + results := make([]result, len(chunks)) + var wg sync.WaitGroup + + for i, chunk := range chunks { + wg.Add(1) + go func(idx int, ch []string) { + defer wg.Done() + data, err := multiDetail(strings.Join(ch, ",")) + results[idx] = result{data: data, err: err} + }(i, chunk) + } + + wg.Wait() + + // 合并结果 + res := make([]map[string]interface{}, 0) + for _, r := range results { + if r.err == nil { + res = append(res, r.data...) + } else { + multiDetailLogger.Error(fmt.Sprintf("批量获取详情失败: %v", r.err)) + } + } + + // 处理下架书籍 + offShelfBookIDs := make([]string, 0) + offShelfIndexes := make([]int, 0) + + for i, item := range res { + if status, ok := item["tomato_book_status"].(string); ok && status == "3" { + if bid, ok := item["book_id"].(string); ok { + offShelfBookIDs = append(offShelfBookIDs, bid) + offShelfIndexes = append(offShelfIndexes, i) + } + } + } + + // 单独获取下架书籍信息 + if len(offShelfBookIDs) > 0 { + type offshelfResult struct { + data map[string]interface{} + idx int + } + + offshelfResults := make([]offshelfResult, len(offShelfBookIDs)) + var wg2 sync.WaitGroup + + for i, bid := range offShelfBookIDs { + wg2.Add(1) + go func(idx int, bookID string, originalIdx int) { + defer wg2.Done() + data, _ := singleDetail(bookID) + offshelfResults[idx] = offshelfResult{data: data, idx: originalIdx} + }(i, bid, offShelfIndexes[i]) + } + + wg2.Wait() + + // 更新结果 + for _, r := range offshelfResults { + if r.idx < len(res) { + res[r.idx] = r.data + } + } + } + + // 序列化结果 + response := map[string]interface{}{ + "code": 0, + "message": "SUCCESS", + "data": res, + } + + resultBytes, _ := json.Marshal(response) + return string(resultBytes), nil +} + diff --git a/internal/fqapi/register_key.go b/internal/fqapi/register_key.go new file mode 100644 index 0000000..5831af0 --- /dev/null +++ b/internal/fqapi/register_key.go @@ -0,0 +1,249 @@ +package fqapi + +import ( + "bytes" + "compress/gzip" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fanqie/internal/common" + "fmt" + "math/rand" + "regexp" + "sort" + "strings" + "time" +) + +// KeyInfo 密钥信息 +type KeyInfo struct { + Key string `json:"key"` + Ver int `json:"ver"` +} + +var ( + keyInfo *KeyInfo + keyLogger = common.NewLogger("register_key", "INFO", "") +) + +// rHex 将数字转换为反转的16进制字符串 +func rHex(num int64) string { + hexStr := fmt.Sprintf("%032x", num) + result := "" + for i := len(hexStr); i > 0; i -= 2 { + result += hexStr[i-2 : i] + } + return result +} + +// createSymmetricCrypto 创建对称加密器 +func createSymmetricCrypto(algorithm string, key, iv []byte) (func([]byte) ([]byte, error), func([]byte) ([]byte, error), error) { + if algorithm != "AES/CBC/PKCS5Padding" { + return nil, nil, fmt.Errorf("不支持的加密算法") + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, nil, err + } + + encrypt := func(data []byte) ([]byte, error) { + // PKCS7填充 + padding := aes.BlockSize - len(data)%aes.BlockSize + padText := make([]byte, padding) + for i := range padText { + padText[i] = byte(padding) + } + paddedData := append(data, padText...) + + ciphertext := make([]byte, len(paddedData)) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext, paddedData) + + return ciphertext, nil + } + + decrypt := func(data []byte) ([]byte, error) { + mode := cipher.NewCBCDecrypter(block, iv) + decrypted := make([]byte, len(data)) + mode.CryptBlocks(decrypted, data) + + // 移除PKCS7填充 + padding := int(decrypted[len(decrypted)-1]) + if padding > len(decrypted) || padding > aes.BlockSize { + return nil, fmt.Errorf("无效的填充") + } + + return decrypted[:len(decrypted)-padding], nil + } + + return encrypt, decrypt, nil +} + +// En AES加密 +func En(data, key, iv []byte) ([]byte, error) { + encrypt, _, err := createSymmetricCrypto("AES/CBC/PKCS5Padding", key, iv) + if err != nil { + return nil, err + } + return encrypt(data) +} + +// De AES解密 +func De(data, key, iv []byte) ([]byte, error) { + _, decrypt, err := createSymmetricCrypto("AES/CBC/PKCS5Padding", key, iv) + if err != nil { + return nil, err + } + return decrypt(data) +} + +// randomStringKey 生成随机字符串(密钥用) +func randomStringKey(length int) string { + const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + b := make([]byte, length) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) +} + +// GetKey 获取密钥 +func GetKey(header string) (*KeyInfo, error) { + if header == "" { + keyLogger.Error("请先注册设备") + return nil, fmt.Errorf("请先注册设备") + } + + key, _ := base64.StdEncoding.DecodeString("rCXGfd2POMGzeiNIgo4iLg==") + iv := []byte(randomStringKey(16)) + + // 提取device_id + re := regexp.MustCompile(`device_id=(\d+)`) + matches := re.FindStringSubmatch(header) + if len(matches) < 2 { + return nil, fmt.Errorf("无法从header中提取device_id") + } + + var deviceIDInt int64 + fmt.Sscanf(matches[1], "%d", &deviceIDInt) + + // 转换device_id + hexStr := rHex(deviceIDInt) + data, _ := hex.DecodeString(hexStr) + + keyLogger.Debug(fmt.Sprintf("rKey: %v", iv)) + + // 加密 + encrypted, err := En(data, key, iv) + if err != nil { + return nil, err + } + + // 组合iv和加密数据 + newArr := append(iv, encrypted...) + content := base64.StdEncoding.EncodeToString(newArr) + + keyLogger.Info("正在获取密钥...") + + // 构建参数 + params := []string{ + "aid=1967", + "app_name=novelapp", + "channel=0", + "device_platform=android", + "os_version=0", + "version_code=5.8.9.32", + header, + } + + allParams := strings.Join(params, "&") + paramList := strings.Split(allParams, "&") + sort.Strings(paramList) + sortedParams := strings.Join(paramList, "&") + + // 准备请求体 + requestBody := map[string]string{ + "content": content, + } + + bodyJSON, _ := json.Marshal(requestBody) + + // gzip压缩 + var gzipBuf bytes.Buffer + gzipWriter := gzip.NewWriter(&gzipBuf) + gzipWriter.Write(bodyJSON) + gzipWriter.Close() + + reqURL := "https://reading.snssdk.com/reading/crypt/registerkey?" + sortedParams + + resp, err := GorgonRequest(reqURL, map[string]interface{}{ + "method": "POST", + "body": gzipBuf.Bytes(), + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + "Content-Encoding": "gzip", + }, + }) + + if err != nil { + keyLogger.Error("获取密钥失败: " + err.Error()) + return nil, err + } + + if resp.Status == 200 { + var respData map[string]interface{} + json.Unmarshal(resp.Content, &respData) + + dataObj, ok := respData["data"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("响应格式错误") + } + + keyStr, ok := dataObj["key"].(string) + if !ok { + return nil, fmt.Errorf("密钥字段不存在") + } + + // 解码返回的密钥 + res, _ := base64.StdEncoding.DecodeString(keyStr) + if len(res) < 32 { + return nil, fmt.Errorf("密钥长度不足") + } + + data := res[16:] + ivResp := res[0:16] + + // 解密密钥 + decryptedKey, err := De(data, key, ivResp) + if err != nil { + return nil, err + } + + finalKey := base64.StdEncoding.EncodeToString(decryptedKey) + + keyVer, _ := dataObj["keyver"].(float64) + keyInfo = &KeyInfo{ + Key: finalKey, + Ver: int(keyVer), + } + + keyLogger.Info(fmt.Sprintf("密钥获取成功!")) + + return keyInfo, nil + } + + return nil, fmt.Errorf("获取密钥失败") +} + +// GetKeyInfo 获取当前密钥信息 +func GetKeyInfo() *KeyInfo { + return keyInfo +} + +func init() { + rand.Seed(time.Now().UnixNano()) +} + diff --git a/internal/fqapi/review.go b/internal/fqapi/review.go new file mode 100644 index 0000000..53eae5b --- /dev/null +++ b/internal/fqapi/review.go @@ -0,0 +1,371 @@ +package fqapi + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "fanqie/internal/common" + "fmt" + "strings" +) + +var ( + reviewLogger = common.NewLogger("review", "INFO", "") +) + +// compressJSON 压缩 JSON 数据为 Gzip 格式 +func compressJSON(data interface{}) ([]byte, error) { + jsonData, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("JSON序列化失败: %w", err) + } + + var compressedData bytes.Buffer + gzw := gzip.NewWriter(&compressedData) + if _, err := gzw.Write(jsonData); err != nil { + gzw.Close() + return nil, fmt.Errorf("Gzip压缩失败: %w", err) + } + if err := gzw.Close(); err != nil { + return nil, fmt.Errorf("Gzip关闭失败: %w", err) + } + + return compressedData.Bytes(), nil +} + +// postReviewAPI 发送段评相关的 POST 请求 +func postReviewAPI(url string, requestBody interface{}) (map[string]interface{}, error) { + // 压缩 JSON + compressedData, err := compressJSON(requestBody) + if err != nil { + return nil, err + } + + // 使用 GorgonRequest 添加签名 + resp, err := GorgonRequest(url, map[string]interface{}{ + "method": "POST", + "body": compressedData, + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + "Content-Type": "application/json", + "Content-Encoding": "gzip", + }, + }) + + if err != nil { + return nil, fmt.Errorf("请求失败: %w", err) + } + + // 解析响应 + var result map[string]interface{} + if err := json.Unmarshal(resp.Content, &result); err != nil { + return nil, fmt.Errorf("解析响应失败: %w", err) + } + + // 提取 data 字段 + if data, ok := result["data"].(map[string]interface{}); ok { + return data, nil + } + + return result, nil +} + +// CommentData 评论数据结构 +type CommentData struct { + Data map[string]struct { + Count int `json:"count"` + Hot string `json:"hot"` + IsAuthorComment bool `json:"is_author_comment"` + UserCount int `json:"user_count"` + } `json:"data"` + ItemVersion string `json:"item_version"` +} + +// GetChapterReview 获取章节的段落评论统计(想法评论) +func GetChapterReview(itemID string) (map[string]interface{}, error) { + // 使用 BuildURL 添加设备参数 + url := BuildURL(fmt.Sprintf("https://reading.snssdk.com/novel/commentapi/idea/list/%s/v1", itemID)) + + requestBody := map[string]interface{}{ + "comment_source": 3, + "item_id": itemID, + "item_version": "cdd11695939bcc41d1ff8c3f10c67802_1_e5c2474f6821deb", + } + + return postReviewAPI(url, requestBody) +} + +// GetChapterMixData 获取章节混合数据(评论) +func GetChapterMixData(bookID, itemID string) (map[string]interface{}, error) { + // 使用 BuildURL 添加设备参数 + url := BuildURL("https://api5-normal-sinfonlinec.fqnovel.com/reading/ugc/item/mix_data/get/v") + + requestBody := map[string]interface{}{ + "count": 5000, + "item_id": itemID, + "source_type": 51, + "book_id": bookID, + } + + return postReviewAPI(url, requestBody) +} + +// GetParaReview 获取段落评论或评论回复 +// sort: 1=热门, 3=最新 +// offset: 分页偏移量 +// count: 每页数量 +func GetParaReview(itemID, bookID, commentID string, para, sort, offset, count int) (map[string]interface{}, error) { + // 默认排序值 + if sort != 1 && sort != 3 { + sort = 1 // 默认热门 + } + + // 默认数量 + if count <= 0 { + count = 20 + } + + var url string + if commentID != "" { + // 获取评论的回复列表 - 使用 BuildURL 添加设备参数 + url = BuildURL("https://api5-normal-sinfonlineb.fqnovel.com/reading/ugc/reply/topic/v/", + fmt.Sprintf("offset=%d", offset), + fmt.Sprintf("group_id=%s", itemID), + "service_id=1", + fmt.Sprintf("count=%d", count), + "source_type=63", + fmt.Sprintf("book_id=%s", bookID), + fmt.Sprintf("comment_id=%s", commentID), + ) + } else { + // 获取段落的评论列表 - 使用 BuildURL 添加设备参数 + url = BuildURL("https://api5-normal-sinfonlineb.fqnovel.com/reading/ugc/idea/comment_list/v/", + "item_version=c63f0e6195259adec926ae3209291854_1_1099f2b8324de5f", + fmt.Sprintf("offset=%d", offset), + "author_user_id=104957777655", + fmt.Sprintf("item_id=%s", itemID), + fmt.Sprintf("count=%d", count), + fmt.Sprintf("para_index=%d", para), + fmt.Sprintf("book_id=%s", bookID), + fmt.Sprintf("sort=%d", sort), + ) + } + + // 使用 GorgonRequest 添加签名 + resp, err := GorgonRequest(url, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + return nil, fmt.Errorf("请求失败: %w", err) + } + + // 解析响应 + var result map[string]interface{} + if err := json.Unmarshal(resp.Content, &result); err != nil { + return nil, fmt.Errorf("解析响应失败: %w", err) + } + + // 提取 data 字段 + if data, ok := result["data"].(map[string]interface{}); ok { + return data, nil + } + + return result, nil +} + +// GenerateParagraphsWithComments 生成带评论标记的内容 +func GenerateParagraphsWithComments(itemID, bookID, content, baseURL string) (string, error) { + // 获取段评数据 + data, err := GetChapterReview(itemID) + if err != nil { + reviewLogger.Warning("获取段评数据失败: " + err.Error()) + // 如果获取失败,返回原始内容 + return content, nil + } + + // 解析评论数据 + commentData := make(map[string]struct { + Count int `json:"count"` + Hot string `json:"hot"` + IsAuthorComment bool `json:"is_author_comment"` + UserCount int `json:"user_count"` + }) + + if dataMap, ok := data["data"].(map[string]interface{}); ok { + for key, val := range dataMap { + if valMap, ok := val.(map[string]interface{}); ok { + count := 0 + if c, ok := valMap["count"].(float64); ok { + count = int(c) + } + hot := "" + if h, ok := valMap["hot"].(string); ok { + hot = h + } + isAuthor := false + if ia, ok := valMap["is_author_comment"].(bool); ok { + isAuthor = ia + } + userCount := 0 + if uc, ok := valMap["user_count"].(float64); ok { + userCount = int(uc) + } + + commentData[key] = struct { + Count int `json:"count"` + Hot string `json:"hot"` + IsAuthorComment bool `json:"is_author_comment"` + UserCount int `json:"user_count"` + }{ + Count: count, + Hot: hot, + IsAuthorComment: isAuthor, + UserCount: userCount, + } + } + } + } + + // 检测内容格式:是否包含HTML标签(检测实际内容,而非配置) + isHTMLContent := strings.Contains(content, "

") || + strings.Contains(content, "标签) + // 因为标签需要XML兼容的格式 + hasAnyComments := len(commentData) > 0 + if hasAnyComments && !isHTMLContent { + isHTMLContent = true + } + + lines := strings.Split(content, "\n") + var result strings.Builder + paragraphIndex := 0 + + for i, line := range lines { + isLastLine := i == len(lines)-1 + trimmedLine := strings.TrimSpace(line) + + // 完全空行:直接输出换行符 + if trimmedLine == "" { + if !isLastLine { + result.WriteString("\n") + } + continue + } + + // 跳过图片标签和div标签 + if strings.HasPrefix(trimmedLine, "") || + strings.HasPrefix(trimmedLine, "标签,在

之前插入段评标记 + if strings.HasPrefix(trimmedLine, "

") && strings.HasSuffix(trimmedLine, "

") { + // 提取

和

之间的内容 + innerContent := trimmedLine[3 : len(trimmedLine)-4] + result.WriteString("

") + result.WriteString(innerContent) + + // 添加评论标记 + key := fmt.Sprintf("%d", paragraphIndex) + if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 { + commentTag := fmt.Sprintf(``, + baseURL, itemID, bookID, paragraphIndex, commentInfo.Count) + result.WriteString(commentTag) + } + + result.WriteString("

") + if !isLastLine { + result.WriteString("\n") + } + paragraphIndex++ + continue + } + + // 如果是HTML模式,包裹

标签 + if isHTMLContent { + result.WriteString("

") + result.WriteString(line) + + // 添加评论标记 + key := fmt.Sprintf("%d", paragraphIndex) + if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 { + commentTag := fmt.Sprintf(``, + baseURL, itemID, bookID, paragraphIndex, commentInfo.Count) + result.WriteString(commentTag) + } + + result.WriteString("

") + if !isLastLine { + result.WriteString("\n") + } + } else { + // 纯文本模式:只在行尾添加comment标记 + result.WriteString(line) + + // 添加评论标记(纯文本格式) + key := fmt.Sprintf("%d", paragraphIndex) + if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 { + commentTag := fmt.Sprintf(``, + baseURL, itemID, bookID, paragraphIndex, commentInfo.Count) + result.WriteString(commentTag) + } + + if !isLastLine { + result.WriteString("\n") + } + } + + paragraphIndex++ + } + + return result.String(), nil +} + +// extractDeviceID 从 device header 中提取 device_id +func extractDeviceID(header string) string { + if header == "" { + return "0" + } + + // 从 header 中提取 device_id + parts := strings.Split(header, "&") + for _, part := range parts { + if strings.HasPrefix(part, "device_id=") { + return strings.TrimPrefix(part, "device_id=") + } + } + + return "0" +} + +// ParseJSON 解析 JSON 字符串 +func ParseJSON(data []byte, v interface{}) error { + return json.Unmarshal(data, v) +} + +// ToJSON 将对象序列化为 JSON 字符串 +func ToJSON(v interface{}) (string, error) { + buffer := &strings.Builder{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) // 禁用HTML转义,避免 " 被转义为 \" + err := encoder.Encode(v) + if err != nil { + return "", err + } + // Encode 会在末尾添加换行符,需要去掉 + result := buffer.String() + return strings.TrimSuffix(result, "\n"), nil +} diff --git a/internal/fqapi/search.go b/internal/fqapi/search.go new file mode 100644 index 0000000..678d490 --- /dev/null +++ b/internal/fqapi/search.go @@ -0,0 +1,59 @@ +package fqapi + +import ( + "fanqie/internal/common" + "fmt" + "net/url" +) + +var searchLogger = common.NewLogger("search", "INFO", "") + +// GetDeviceHeader 获取设备header(需要从外部注入) +var GetDeviceHeader func() string + +// HandleBookSearch 处理书籍搜索 +func HandleBookSearch(key string, offset int, tabType int) (string, error) { + if key == "" { + return BuildErrorResponseWithCode(1, "需要参数 key"), nil + } + + if tabType == 0 { + tabType = 3 + } + + // 编码查询关键字 + encodedKey := url.QueryEscape(key) + + // 使用新的参数构建函数 + reqURL := BuildURL("https://reading.snssdk.com/reading/bookapi/search/tab/v/", + fmt.Sprintf("tab_type=%d", tabType), + "update_version_code=58932", + "query="+encodedKey, + fmt.Sprintf("passback=%d", offset), + ) + + resp, err := GorgonRequest(reqURL, map[string]interface{}{ + "method": "GET", + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + }, + }) + + if err != nil { + searchLogger.Error(fmt.Sprintf("搜索请求失败: %v", err)) + return BuildErrorResponseWithCode(1, "搜索请求失败"), err + } + + respJSON, err := resp.JSON() + if err != nil { + searchLogger.Error(fmt.Sprintf("解析响应失败: %v", err)) + return BuildErrorResponseWithCode(1, "解析响应失败"), err + } + + if code, ok := respJSON["code"].(float64); ok && code == 0 { + return resp.Text, nil + } + + return BuildErrorResponseWithCode(1, "搜索失败"), nil +} + diff --git a/internal/fqapi/ttencrypt.go b/internal/fqapi/ttencrypt.go new file mode 100644 index 0000000..23fd07f --- /dev/null +++ b/internal/fqapi/ttencrypt.go @@ -0,0 +1,196 @@ +package fqapi + +import ( + "bytes" + "compress/gzip" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha512" + "encoding/base64" + "fmt" + "io" +) + +// FIXED_STRING 固定字符串(base64解码后的值) +var FIXED_STRING = []byte{ + 0x4D, 0xD4, 0xC2, 0xE6, 0xB8, 0x31, 0x62, 0x09, + 0x0E, 0x52, 0xB3, 0xC7, 0xA6, 0x73, 0x3B, 0xA4, + 0x1C, 0xB2, 0x46, 0x2B, 0x82, 0x9A, 0xB5, 0x8A, + 0x19, 0x6B, 0x39, 0xDB, 0x57, 0x17, 0x75, 0x24, + 0xF4, 0x9B, 0xAF, 0x7F, 0x08, 0xE8, 0xD6, 0x8D, + 0x26, 0xA7, 0x2E, 0x37, 0xC1, 0xA9, 0x5A, 0x2F, + 0x1F, 0x05, 0xA5, 0x18, 0x92, 0xAE, 0xF2, 0x94, + 0x97, 0x32, 0xB6, 0x2A, 0x38, 0xAA, 0xDD, 0x58, +} + +// sha512Hash 计算SHA512哈希值 +func sha512Hash(data []byte) []byte { + hash := sha512.Sum512(data) + return hash[:] +} + +// generateRandomBytes 生成指定长度的随机字节 +func generateRandomBytes(size int) []byte { + bytes := make([]byte, size) + rand.Read(bytes) + return bytes +} + +// gzipCompress 使用gzip压缩数据 +func gzipCompress(data []byte) ([]byte, error) { + buf := new(bytes.Buffer) + writer := gzip.NewWriter(buf) + _, err := writer.Write(data) + if err != nil { + return nil, err + } + err = writer.Close() + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// gzipDecompress 使用gzip解压缩数据 +func gzipDecompress(data []byte) ([]byte, error) { + reader, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + defer reader.Close() + + result, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + return result, nil +} + +// encryptData 使用AES-CBC模式加密数据 +func encryptData(data []byte, key []byte, iv []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // PKCS7填充 + padding := aes.BlockSize - len(data)%aes.BlockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + paddedData := append(data, padText...) + + ciphertext := make([]byte, len(paddedData)) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext, paddedData) + + return ciphertext, nil +} + +// decryptData 使用AES-CBC模式解密数据 +func decryptData(data []byte, key []byte, iv []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + mode := cipher.NewCBCDecrypter(block, iv) + decrypted := make([]byte, len(data)) + mode.CryptBlocks(decrypted, data) + + // 移除PKCS7填充 + padding := int(decrypted[len(decrypted)-1]) + return decrypted[:len(decrypted)-padding], nil +} + +// TTEncrypt 加密函数 +func TTEncrypt(data []byte) ([]byte, error) { + // 生成随机值 + randomValue := generateRandomBytes(32) + + // 计算SHA512哈希值 + hashValue := sha512Hash(append(sha512Hash(randomValue), FIXED_STRING...)) + + // 提取key和iv + key := hashValue[:16] + iv := hashValue[16:32] + + // 压缩数据并计算哈希值 + compressedData, err := gzipCompress(data) + if err != nil { + return nil, err + } + hashedData := append(sha512Hash(compressedData), compressedData...) + + // 加密数据 + encryptedData, err := encryptData(hashedData, key, iv) + if err != nil { + return nil, err + } + + // 返回结果 + header := []byte{116, 99, 5, 16, 0, 0} + result := append(header, randomValue...) + result = append(result, encryptedData...) + + return result, nil +} + +// TTDecrypt 解密函数 +func TTDecrypt(encryptedData []byte) ([]byte, error) { + // 提取固定头、随机值和加密数据 + // header := encryptedData[:6] + randomValue := encryptedData[6:38] + encryptedPayload := encryptedData[38:] + + // 计算SHA512哈希值 + hashValue := sha512Hash(append(sha512Hash(randomValue), FIXED_STRING...)) + + // 提取key和iv + key := hashValue[:16] + iv := hashValue[16:32] + + // 解密数据 + decryptedData, err := decryptData(encryptedPayload, key, iv) + if err != nil { + return nil, err + } + + // 分离哈希值和压缩数据 + hashLength := 64 + hashedValue := decryptedData[:hashLength] + compressedData := decryptedData[hashLength:] + + // 验证哈希值 + expectedHash := sha512Hash(compressedData) + for i := 0; i < hashLength; i++ { + if hashedValue[i] != expectedHash[i] { + return nil, fmt.Errorf("hash verification failed") + } + } + + // 解压缩数据 + originalData, err := gzipDecompress(compressedData) + if err != nil { + return nil, err + } + + return originalData, nil +} + +// Encrypt 导出的加密函数 +func Encrypt(data []byte) string { + result, _ := TTEncrypt(data) + return base64.StdEncoding.EncodeToString(result) +} + +// Decrypt 导出的解密函数 +func Decrypt(encryptedStr string) ([]byte, error) { + encryptedData, err := base64.StdEncoding.DecodeString(encryptedStr) + if err != nil { + return nil, err + } + return TTDecrypt(encryptedData) +} + diff --git a/internal/fqapi/video.go b/internal/fqapi/video.go new file mode 100644 index 0000000..72d43b1 --- /dev/null +++ b/internal/fqapi/video.go @@ -0,0 +1,266 @@ +package fqapi + +import ( + "encoding/base64" + "encoding/json" + "fanqie/internal/common" + "fmt" + "math" +) + +var videoLogger = common.NewLogger("video", "INFO", "") + +// HandleVideo 处理获取视频(支持批量和单个接口) +func HandleVideo(itemIDs interface{}) (string, error) { + if itemIDs == nil || itemIDs == "" { + return BuildSuccessResponse([]interface{}{}), nil + } + + // 处理itemIDs - 使用ParseStringParam + itemIDStr := ParseStringParam(itemIDs, "") + + // 判断是单个ID还是多个ID(用逗号分隔) + isSingle := true + if str, ok := itemIDs.(string); ok { + for _, ch := range str { + if ch == ',' { + isSingle = false + break + } + } + } + + // 根据ID数量选择不同的接口 + var url string + if isSingle { + // 单个视频接口 - 使用带签名的URL + url = BuildURL("https://api5-normal-sinfonlineb.fqnovel.com/novel/player/video_model/v1") + } else { + // 批量视频接口 + url = BuildURL("https://reading.snssdk.com/novel/player/multi_video_model/v1/") + } + + // 构建请求体,添加更多设备参数,请求所有清晰度 + requestBody := fmt.Sprintf(`{"biz_param":{"device_level":3,"need_all_video_definition":true,"need_mp4_align":false,"use_os_player":false,"video_platform":1024},"video_id":"%s"}`, + itemIDStr) + + // 使用 GorgonRequest 添加签名和设备信息 + resp, err := GorgonRequest(url, map[string]interface{}{ + "method": "POST", + "body": requestBody, + "headers": map[string]string{ + "User-Agent": "com.dragon.read", + "Content-Type": "application/json", + }, + }) + + if err != nil { + videoLogger.Error(fmt.Sprintf("获取视频失败: %v", err)) + return BuildErrorResponse("FAILED"), err + } + + respJSON, err := resp.JSON() + if err != nil { + videoLogger.Error(fmt.Sprintf("解析响应失败: %v", err)) + return BuildErrorResponse("FAILED"), err + } + + // 检查响应码(可能返回 Code 或 code) + var respCode float64 + var okCode bool + + if code, ok := respJSON["code"].(float64); ok { + respCode, okCode = code, code == 0 + } else if code, ok := respJSON["Code"].(float64); ok { + respCode, okCode = code, code == 0 + } + + if okCode { + // 尝试获取 data(可能在不同位置) + var data map[string]interface{} + if d, ok := respJSON["data"].(map[string]interface{}); ok { + data = d + } else if d, ok := respJSON["Data"].(map[string]interface{}); ok { + data = d + } + + if data != nil { + if videoModelStr, ok := data["video_model"].(string); ok && isSingle { + decodedModel := decodeVideoModelAsJSON(videoModelStr) + + // 单个接口直接返回 video_model + response := map[string]interface{}{ + "code": 200, + "message": "SUCCESS", + "data": decodedModel, + } + + resultBytes, _ := json.Marshal(response) + return string(resultBytes), nil + } else { + // 批量接口:data 是一个map,key是ID,value包含video_model + result := make(map[string]interface{}) + + for itemID, itemData := range data { + if itemMap, ok := itemData.(map[string]interface{}); ok { + if videoModelStr, ok := itemMap["video_model"].(string); ok { + decodedModel := decodeVideoModelAsJSON(videoModelStr) + result[itemID] = map[string]interface{}{ + "video_model": decodedModel, + } + } + } + } + + // 构建返回结果 + response := map[string]interface{}{ + "code": 200, + "message": "SUCCESS", + "data": result, + } + + resultBytes, _ := json.Marshal(response) + return string(resultBytes), nil + } + } + } + + // 返回错误信息 + errMsg := "获取视频失败" + if msg, ok := respJSON["message"].(string); ok { + errMsg = msg + } else if msg, ok := respJSON["Message"].(string); ok { + errMsg = msg + } + videoLogger.Error(fmt.Sprintf("获取视频失败: code=%v, msg=%s", respCode, errMsg)) + return BuildErrorResponse(errMsg), nil +} + +// decodeVideoModelAsJSON 解析并解码 video_model,返回关键字段的 JSON 对象 +func decodeVideoModelAsJSON(videoModelStr string) interface{} { + // 解析 video_model JSON 字符串 + var videoModel map[string]interface{} + if err := json.Unmarshal([]byte(videoModelStr), &videoModel); err != nil { + // 解析失败,返回空对象 + return map[string]interface{}{} + } + + // 构建精简的返回结果,只包含关键字段 + result := make(map[string]interface{}) + + // 提取关键字段 + if videoID, ok := videoModel["video_id"].(string); ok { + result["video_id"] = videoID + } + if duration, ok := videoModel["video_duration"].(float64); ok { + result["video_duration"] = formatDuration(duration) + } + if posterURL, ok := videoModel["poster_url"].(string); ok { + result["poster_url"] = posterURL + } + + // 处理 video_list + if videoList, ok := videoModel["video_list"].(map[string]interface{}); ok { + processedVideoList := make(map[string]interface{}) + + for quality, qualityData := range videoList { + if qualityMap, ok := qualityData.(map[string]interface{}); ok { + // 只保留关键字段 + qualityInfo := make(map[string]interface{}) + + if definition, ok := qualityMap["definition"].(string); ok { + qualityInfo["definition"] = definition + } + if vtype, ok := qualityMap["vtype"].(string); ok { + qualityInfo["vtype"] = vtype + } + if vwidth, ok := qualityMap["vwidth"].(float64); ok { + qualityInfo["vwidth"] = int(vwidth) + } + if vheight, ok := qualityMap["vheight"].(float64); ok { + qualityInfo["vheight"] = int(vheight) + } + if bitrate, ok := qualityMap["bitrate"].(float64); ok { + qualityInfo["bitrate"] = formatBitrate(bitrate) + } + if size, ok := qualityMap["size"].(float64); ok { + qualityInfo["size"] = formatFileSize(size) + } + if fps, ok := qualityMap["fps"].(float64); ok { + qualityInfo["fps"] = int(fps) + } + + // 解码 main_url + if mainURL, ok := qualityMap["main_url"].(string); ok { + if decoded, err := base64.StdEncoding.DecodeString(mainURL); err == nil { + qualityInfo["main_url"] = string(decoded) + } + } + + // 解码 backup_url_1 + if backupURL, ok := qualityMap["backup_url_1"].(string); ok { + if decoded, err := base64.StdEncoding.DecodeString(backupURL); err == nil { + qualityInfo["backup_url_1"] = string(decoded) + } + } + + processedVideoList[quality] = qualityInfo + } + } + + result["video_list"] = processedVideoList + } + + return result +} + +// formatDuration 将秒数转换为可读的时间格式 +func formatDuration(seconds float64) string { + totalSeconds := int(math.Round(seconds)) + + hours := totalSeconds / 3600 + minutes := (totalSeconds % 3600) / 60 + secs := totalSeconds % 60 + + if hours > 0 { + return fmt.Sprintf("%d小时%d分%d秒", hours, minutes, secs) + } else if minutes > 0 { + return fmt.Sprintf("%d分%d秒", minutes, secs) + } + return fmt.Sprintf("%d秒", secs) +} + +// formatFileSize 将字节数转换为可读的文件大小格式 +func formatFileSize(bytes float64) string { + const unit = 1024.0 + if bytes < unit { + return fmt.Sprintf("%.0f B", bytes) + } + + kb := bytes / unit + if kb < unit { + return fmt.Sprintf("%.2f KB", kb) + } + + mb := kb / unit + if mb < unit { + return fmt.Sprintf("%.2f MB", mb) + } + + gb := mb / unit + return fmt.Sprintf("%.2f GB", gb) +} + +// formatBitrate 将比特率转换为可读的格式 +func formatBitrate(bitrate float64) string { + const unit = 1000.0 + kbps := bitrate / unit + + if kbps < unit { + return fmt.Sprintf("%.0f Kbps", kbps) + } + + mbps := kbps / unit + return fmt.Sprintf("%.2f Mbps", mbps) +} + diff --git a/internal/handlers/book.go b/internal/handlers/book.go new file mode 100644 index 0000000..627fac0 --- /dev/null +++ b/internal/handlers/book.go @@ -0,0 +1,90 @@ +package handlers + +import ( + "fanqie/internal/fqapi" + "fmt" + "net/http" + "strconv" +) + +// HandleSearch 搜索处理 +func HandleSearch(w http.ResponseWriter, r *http.Request) { + incrementTotal() + incrementSearch() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + query := r.URL.Query().Get("query") + if query == "" { + query = r.URL.Query().Get("key") + } + + offsetStr := r.URL.Query().Get("offset") + offset := 0 + if offsetStr != "" { + offset, _ = strconv.Atoi(offsetStr) + } + + tabTypeStr := r.URL.Query().Get("tab_type") + tabType := 3 + if tabTypeStr != "" { + tabType, _ = strconv.Atoi(tabTypeStr) + } + + result, err := fqapi.HandleBookSearch(query, offset, tabType) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "搜索失败: %s"}`, err.Error()) + return + } + + fmt.Fprint(w, result) +} + +// HandleDetail 详情处理 +func HandleDetail(w http.ResponseWriter, r *http.Request) { + incrementTotal() + incrementDetail() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + bookID := r.URL.Query().Get("book_id") + + result, err := fqapi.HandleGetDetail(bookID) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "获取详情失败: %s"}`, err.Error()) + return + } + + fmt.Fprint(w, result) +} + +// HandleCatalog 目录处理 +func HandleCatalog(w http.ResponseWriter, r *http.Request) { + incrementTotal() + incrementCatalog() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + bookID := r.URL.Query().Get("book_id") + + result, err := fqapi.HandleGetCatalog(bookID) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "获取目录失败: %s"}`, err.Error()) + return + } + + fmt.Fprint(w, result) +} + +// HandleMultiDetail 批量详情处理 +func HandleMultiDetail(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + bookID := r.URL.Query().Get("book_id") + + result, err := fqapi.HandleGetMultiDetail(bookID) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "批量获取详情失败: %s"}`, err.Error()) + return + } + + fmt.Fprint(w, result) +} + diff --git a/internal/handlers/category.go b/internal/handlers/category.go new file mode 100644 index 0000000..c941055 --- /dev/null +++ b/internal/handlers/category.go @@ -0,0 +1,46 @@ +package handlers + +import ( + "fanqie/internal/fqapi" + "fmt" + "net/http" +) + +// HandleCategoryFront 分类列表处理 +func HandleCategoryFront(w http.ResponseWriter, r *http.Request) { + incrementTotal() + incrementCategory() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + tab := r.URL.Query().Get("tab") + + result, err := fqapi.HandleCategoryFront(tab) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "获取分类列表失败: %s"}`, err.Error()) + return + } + + fmt.Fprint(w, result) +} + +// HandleCategoryLanding 分类详情处理 +func HandleCategoryLanding(w http.ResponseWriter, r *http.Request) { + incrementTotal() + incrementCategory() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + categoryID := r.URL.Query().Get("category_id") + offset := r.URL.Query().Get("offset") + wordNumber := r.URL.Query().Get("word_number") + bookStatus := r.URL.Query().Get("book_status") + sortBy := r.URL.Query().Get("sort_by") + + result, err := fqapi.HandleCategoryLanding(categoryID, offset, wordNumber, bookStatus, sortBy) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "获取分类详情失败: %s"}`, err.Error()) + return + } + + fmt.Fprint(w, result) +} + diff --git a/internal/handlers/common.go b/internal/handlers/common.go new file mode 100644 index 0000000..4a100aa --- /dev/null +++ b/internal/handlers/common.go @@ -0,0 +1,174 @@ +package handlers + +import ( + "encoding/json" + "fanqie/internal/config" + "fanqie/internal/fqapi" + "fmt" + "net/http" + "sync/atomic" + "time" +) + +var ( + startTime = time.Now() + deviceHeader string + + // 请求统计 + totalRequests int64 + searchRequests int64 + detailRequests int64 + catalogRequests int64 + contentRequests int64 + mangaRequests int64 + audioRequests int64 + videoRequests int64 + categoryRequests int64 + + // 统计保存定时器 + statsSaveTicker *time.Ticker +) + +// 统计函数 +func incrementTotal() { atomic.AddInt64(&totalRequests, 1) } +func incrementSearch() { atomic.AddInt64(&searchRequests, 1) } +func incrementDetail() { atomic.AddInt64(&detailRequests, 1) } +func incrementCatalog() { atomic.AddInt64(&catalogRequests, 1) } +func incrementContent() { atomic.AddInt64(&contentRequests, 1) } +func incrementManga() { atomic.AddInt64(&mangaRequests, 1) } +func incrementAudio() { atomic.AddInt64(&audioRequests, 1) } +func incrementVideo() { atomic.AddInt64(&videoRequests, 1) } +func incrementCategory() { atomic.AddInt64(&categoryRequests, 1) } + +func getTotal() int64 { return atomic.LoadInt64(&totalRequests) } +func getSearch() int64 { return atomic.LoadInt64(&searchRequests) } +func getDetail() int64 { return atomic.LoadInt64(&detailRequests) } +func getCatalog() int64 { return atomic.LoadInt64(&catalogRequests) } +func getContent() int64 { return atomic.LoadInt64(&contentRequests) } +func getManga() int64 { return atomic.LoadInt64(&mangaRequests) } +func getAudio() int64 { return atomic.LoadInt64(&audioRequests) } +func getVideo() int64 { return atomic.LoadInt64(&videoRequests) } +func getCategory() int64 { return atomic.LoadInt64(&categoryRequests) } + +// SetDeviceHeader 设置设备header +func SetDeviceHeader(header string) { + deviceHeader = header +} + +// LoadStatistics 从数据库加载统计数据 +func LoadStatistics() { + stats, err := fqapi.LoadStatistics() + if err != nil { + fmt.Printf("加载统计数据失败: %v\n", err) + return + } + + atomic.StoreInt64(&totalRequests, stats["total"]) + atomic.StoreInt64(&searchRequests, stats["search"]) + atomic.StoreInt64(&detailRequests, stats["detail"]) + atomic.StoreInt64(&catalogRequests, stats["catalog"]) + atomic.StoreInt64(&contentRequests, stats["content"]) + atomic.StoreInt64(&mangaRequests, stats["manga"]) + atomic.StoreInt64(&audioRequests, stats["audio"]) + atomic.StoreInt64(&videoRequests, stats["video"]) + atomic.StoreInt64(&categoryRequests, stats["category"]) +} + +// SaveStatistics 保存统计数据到数据库 +func SaveStatistics() { + stats := map[string]int64{ + "total": atomic.LoadInt64(&totalRequests), + "search": atomic.LoadInt64(&searchRequests), + "detail": atomic.LoadInt64(&detailRequests), + "catalog": atomic.LoadInt64(&catalogRequests), + "content": atomic.LoadInt64(&contentRequests), + "manga": atomic.LoadInt64(&mangaRequests), + "audio": atomic.LoadInt64(&audioRequests), + "video": atomic.LoadInt64(&videoRequests), + "category": atomic.LoadInt64(&categoryRequests), + } + + if err := fqapi.SaveStatistics(stats); err != nil { + fmt.Printf("保存统计数据失败: %v\n", err) + } +} + +// StartStatisticsSaver 启动统计数据定期保存 +func StartStatisticsSaver() { + // 每30秒保存一次统计数据 + statsSaveTicker = time.NewTicker(30 * time.Second) + go func() { + for range statsSaveTicker.C { + SaveStatistics() + } + }() +} + +// StopStatisticsSaver 停止统计数据定期保存 +func StopStatisticsSaver() { + if statsSaveTicker != nil { + statsSaveTicker.Stop() + // 最后保存一次 + SaveStatistics() + } +} + +// RequireAccessKey 验证访问密钥中间件 +func RequireAccessKey(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cfg := config.GetConfig() + + // 如果未配置 access_key 或为空,则不启用验证 + if cfg == nil || cfg.Server.AccessKey == "" { + handler(w, r) + return + } + + // 从查询参数或 Header 中获取 key + key := r.URL.Query().Get("key") + if key == "" { + key = r.Header.Get("X-Access-Key") + } + + // 验证密钥 + if key != cfg.Server.AccessKey { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, `{"code": -1, "msg": "访问被拒绝:无效的访问密钥"}`) + return + } + + // 验证通过,继续处理请求 + handler(w, r) + } +} + +// HandleMain 主页处理 +func HandleMain(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + response := map[string]interface{}{ + "code": 0, + "message": "success", + "data": nil, + } + json.NewEncoder(w).Encode(response) +} + +// HandleSelfInfo 设备信息处理 +func HandleSelfInfo(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // 获取设备池统计信息 + pool := fqapi.GetDeviceKeyPool() + poolStats := pool.GetStats() + + response := map[string]interface{}{ + "code": 0, + "message": "SUCCESS", + "data": map[string]interface{}{ + "device_pool_enabled": pool.IsEnabled(), + "device_pool_stats": poolStats, + }, + } + json.NewEncoder(w).Encode(response) +} diff --git a/internal/handlers/content.go b/internal/handlers/content.go new file mode 100644 index 0000000..a66aa13 --- /dev/null +++ b/internal/handlers/content.go @@ -0,0 +1,156 @@ +package handlers + +import ( + "fanqie/internal/config" + "fanqie/internal/fqapi" + "fmt" + "net/http" +) + +// HandleContent 内容处理 +func HandleContent(w http.ResponseWriter, r *http.Request) { + incrementTotal() + incrementContent() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // 从查询参数获取item_id + itemID := r.URL.Query().Get("item_id") + + // 获取URL参数(支持动态覆盖配置) + textMode := r.URL.Query().Get("text_mode") // 1=纯文本模式 + imageMode := r.URL.Query().Get("image_mode") // 0=移除图片, 1=保留图片 + reviewParam := r.URL.Query().Get("review") // 1=启用段评, 0=禁用段评 + + result, err := fqapi.HandleGetContent(itemID, textMode, imageMode) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "获取内容失败: %s"}`, err.Error()) + return + } + + // 判断是否启用段评功能 + // 优先使用URL参数,如果没有则使用配置文件 + enableReview := false + if reviewParam == "1" { + enableReview = true + } else if reviewParam == "0" { + enableReview = false + } else { + // 没有URL参数时,使用配置文件 + cfg := config.GetConfig() + if cfg != nil { + enableReview = cfg.Fanqie.Content.EnableReview + } + } + + // 如果启用了段评功能,处理正文内容添加段评标记 + if enableReview { + result = addReviewToContent(result, itemID, r) + } + + fmt.Fprint(w, result) +} + +// HandleMultiContent 批量内容处理 +func HandleMultiContent(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + bookID := r.URL.Query().Get("book_id") + itemIDs := r.URL.Query().Get("item_ids") + textMode := r.URL.Query().Get("text_mode") + imageMode := r.URL.Query().Get("image_mode") + ignoreFailed := r.URL.Query().Get("ignore_failed") + + result, err := fqapi.HandleMultiContent(itemIDs, bookID, textMode, imageMode, ignoreFailed) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "批量获取内容失败: %s"}`, err.Error()) + return + } + + fmt.Fprint(w, result) +} + +// HandleItemSummary 章节摘要处理 +func HandleItemSummary(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + bookID := r.URL.Query().Get("book_id") + itemIDs := r.URL.Query().Get("item_ids") + + result, err := fqapi.HandleItemSummary(itemIDs, bookID) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "获取章节摘要失败: %s"}`, err.Error()) + return + } + + fmt.Fprint(w, result) +} + +// addReviewToContent 为正文内容添加段评标记 +func addReviewToContent(jsonResult, itemID string, r *http.Request) string { + // 解析 JSON 结果 + var resultData map[string]interface{} + if err := fqapi.ParseJSON([]byte(jsonResult), &resultData); err != nil { + return jsonResult + } + + // 获取 data 字段 + data, ok := resultData["data"].(map[string]interface{}) + if !ok { + return jsonResult + } + + // 获取 content 字段 + content, ok := data["content"].(string) + if !ok || content == "" { + return jsonResult + } + + // 从响应数据中提取 book_id (data.novel_data.book_id) + bookID := "" + if novelData, ok := data["novel_data"].(map[string]interface{}); ok { + if bookIDVal, ok := novelData["book_id"].(string); ok { + bookID = bookIDVal + } else if bookIDVal, ok := novelData["book_id"].(float64); ok { + bookID = fmt.Sprintf("%.0f", bookIDVal) + } + } + + // 获取基础 URL + cfg := config.GetConfig() + baseURL := "" + if cfg != nil { + host := cfg.Server.Host + if host == "0.0.0.0" { + host = "localhost" + } + port := cfg.Server.Port + scheme := "http" + + // 检查是否来自外部请求 + if r.Host != "" { + baseURL = scheme + "://" + r.Host + } else { + baseURL = scheme + "://" + host + ":" + port + } + } + + // 生成带段评标记的内容 + newContent, err := fqapi.GenerateParagraphsWithComments(itemID, bookID, content, baseURL) + if err != nil { + // 如果失败,返回原始内容 + return jsonResult + } + + // 替换 content 字段 + data["content"] = newContent + resultData["data"] = data + + // 重新序列化为 JSON + newJSON, err := fqapi.ToJSON(resultData) + if err != nil { + return jsonResult + } + + return newJSON +} + diff --git a/internal/handlers/manga_proxy.go b/internal/handlers/manga_proxy.go new file mode 100644 index 0000000..69067b9 --- /dev/null +++ b/internal/handlers/manga_proxy.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "fanqie/internal/fqapi" + "fmt" + "net/http" + "strconv" + "strings" +) + +// HandleMangaImageProxy 处理漫画图片代理请求 +func HandleMangaImageProxy(w http.ResponseWriter, r *http.Request) { + // 从URL路径提取参数: /manga/image/{itemID}/{index} + path := strings.TrimPrefix(r.URL.Path, "/manga/image/") + parts := strings.Split(path, "/") + + if len(parts) != 2 { + http.Error(w, "Invalid URL format. Expected: /manga/image/{itemID}/{index}", http.StatusBadRequest) + return + } + + itemID := parts[0] + index, err := strconv.Atoi(parts[1]) + if err != nil { + http.Error(w, "Invalid index parameter", http.StatusBadRequest) + return + } + + // 获取解密后的图片数据 + imageData, mimeType, err := fqapi.HandleMangaImageProxy(itemID, index) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to get image: %s", err.Error()), http.StatusInternalServerError) + return + } + + // 设置响应头 + w.Header().Set("Content-Type", mimeType) + w.Header().Set("Cache-Control", "public, max-age=3600") // 缓存1小时 + w.Header().Set("Content-Length", strconv.Itoa(len(imageData))) + + // 返回图片数据 + w.Write(imageData) +} + diff --git a/internal/handlers/media.go b/internal/handlers/media.go new file mode 100644 index 0000000..0008449 --- /dev/null +++ b/internal/handlers/media.go @@ -0,0 +1,132 @@ +package handlers + +import ( + "encoding/json" + "fanqie/internal/fqapi" + "fmt" + "net/http" + "strings" +) + +// HandleAudio 音频处理 +func HandleAudio(w http.ResponseWriter, r *http.Request) { + incrementTotal() + incrementAudio() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + itemIDs := r.URL.Query().Get("item_ids") + toneID := r.URL.Query().Get("tone_id") + output := r.URL.Query().Get("output") + + result, err := fqapi.HandleAudio(itemIDs, toneID, output) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "获取音频失败: %s"}`, err.Error()) + return + } + + fmt.Fprint(w, result) +} + +// HandleAudioProxy 音频代理处理 +func HandleAudioProxy(w http.ResponseWriter, r *http.Request) { + incrementTotal() + + // 从URL路径提取key: /audio/proxy/{key} + path := r.URL.Path + key := strings.TrimPrefix(path, "/audio/proxy/") + if key == "" || key == path { + http.Error(w, "缺少音频key", http.StatusBadRequest) + return + } + + // 调用代理处理函数 + data, contentType, err := fqapi.HandleAudioProxy(key) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // 设置正确的Content-Type + if contentType != "" { + w.Header().Set("Content-Type", contentType) + } else { + w.Header().Set("Content-Type", "audio/mpeg") + } + + // 直接播放,不强制下载 + // 浏览器会根据Content-Type决定是播放还是下载 + w.Write(data) +} + +// HandleVideo 视频处理 +func HandleVideo(w http.ResponseWriter, r *http.Request) { + incrementTotal() + incrementVideo() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + itemIDs := r.URL.Query().Get("item_ids") + + result, err := fqapi.HandleVideo(itemIDs) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "获取视频失败: %s"}`, err.Error()) + return + } + + fmt.Fprint(w, result) +} + +// HandleManga 漫画处理 +func HandleManga(w http.ResponseWriter, r *http.Request) { + incrementTotal() + incrementManga() + itemID := r.URL.Query().Get("item_ids") + if itemID == "" { + itemID = r.URL.Query().Get("item_id") + } + + if itemID == "" { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprint(w, `{"code": -1, "msg": "缺少item_ids参数"}`) + return + } + + // 检查是否要返回HTML格式 + showHTML := r.URL.Query().Get("show_html") == "1" + + // 检查输出模式参数(优先级高于配置文件) + // 支持:output=url / output=proxy / output=data_uri + outputMode := r.URL.Query().Get("output") + + result, err := fqapi.HandleGetManga(itemID, showHTML, outputMode) + if err != nil { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprintf(w, `{"code": -1, "msg": "获取漫画失败: %s"}`, err.Error()) + return + } + + if showHTML { + // 返回HTML格式 + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if contentMap, ok := result.(map[string]interface{}); ok { + if content, ok := contentMap["content"].(string); ok { + fmt.Fprint(w, content) + return + } + } + fmt.Fprint(w, `

未找到漫画内容

`) + } else { + // 返回JSON格式 + w.Header().Set("Content-Type", "application/json; charset=utf-8") + jsonData, err := json.Marshal(map[string]interface{}{ + "code": 0, + "msg": "success", + "data": result, + }) + if err != nil { + fmt.Fprintf(w, `{"code": -1, "msg": "JSON编码失败: %s"}`, err.Error()) + return + } + fmt.Fprint(w, string(jsonData)) + } +} + diff --git a/internal/handlers/review.go b/internal/handlers/review.go new file mode 100644 index 0000000..c59a4f6 --- /dev/null +++ b/internal/handlers/review.go @@ -0,0 +1,1281 @@ +package handlers + +import ( + "encoding/json" + "fanqie/internal/fqapi" + "fmt" + "net/http" + "strconv" + "strings" + "time" +) + +// validateParams 验证必需参数 +func validateParams(r *http.Request, required ...string) (map[string]string, error) { + params := make(map[string]string) + for _, key := range required { + value := r.URL.Query().Get(key) + if value == "" { + return nil, fmt.Errorf("缺少 %s 参数", key) + } + params[key] = value + } + return params, nil +} + +// getOptionalParam 获取可选参数(带默认值) +func getOptionalParam(r *http.Request, key, defaultValue string) string { + value := r.URL.Query().Get(key) + if value == "" { + return defaultValue + } + return value +} + +// parseIntParam 解析整数参数 +func parseIntParam(value string, paramName string) (int, error) { + if value == "" { + return 0, nil + } + result, err := strconv.Atoi(value) + if err != nil { + return 0, fmt.Errorf("%s 参数格式错误", paramName) + } + return result, nil +} + +// handleReviewError 统一处理段评 API 错误 +func handleReviewError(w http.ResponseWriter, errType string, err error) { + status := http.StatusInternalServerError + msg := fmt.Sprintf("%s失败: %s", errType, err.Error()) + + if strings.Contains(err.Error(), "缺少") || strings.Contains(err.Error(), "格式错误") { + status = http.StatusBadRequest + } + + respondJSON(w, status, map[string]interface{}{ + "code": -1, + "msg": msg, + }) +} + +// handleReviewSuccess 统一处理成功响应 +func handleReviewSuccess(w http.ResponseWriter, data interface{}) { + respondJSON(w, http.StatusOK, map[string]interface{}{ + "code": 0, + "msg": "ok", + "data": data, + }) +} + +// HandleChapterReview 处理章节评论请求(混合数据) +func HandleChapterReview(w http.ResponseWriter, r *http.Request) { + incrementTotal() + + // 验证必需参数 + params, err := validateParams(r, "item_id") + if err != nil { + handleReviewError(w, "参数验证", err) + return + } + + // 获取可选参数 + bookID := getOptionalParam(r, "book_id", "") + + // 调用 API + data, err := fqapi.GetChapterMixData(bookID, params["item_id"]) + if err != nil { + handleReviewError(w, "获取章节评论", err) + return + } + + handleReviewSuccess(w, data) +} + +// HandleReview 处理段落评论请求 +func HandleReview(w http.ResponseWriter, r *http.Request) { + incrementTotal() + + // 验证必需参数 + params, err := validateParams(r, "item_id") + if err != nil { + handleReviewError(w, "参数验证", err) + return + } + + // 获取可选参数 + bookID := getOptionalParam(r, "book_id", "") + commentID := getOptionalParam(r, "comment_id", "") + paraStr := getOptionalParam(r, "para", "0") + sortStr := getOptionalParam(r, "sort", "1") + offsetStr := getOptionalParam(r, "offset", "0") + countStr := getOptionalParam(r, "count", "20") + + // 解析整数参数 + para, err := parseIntParam(paraStr, "para") + if err != nil { + handleReviewError(w, "参数解析", err) + return + } + + sort, err := parseIntParam(sortStr, "sort") + if err != nil { + sort = 1 // 默认热门 + } + + offset, _ := parseIntParam(offsetStr, "offset") + count, _ := parseIntParam(countStr, "count") + if count <= 0 { + count = 20 + } + + // 调用 API + data, err := fqapi.GetParaReview(params["item_id"], bookID, commentID, para, sort, offset, count) + if err != nil { + handleReviewError(w, "获取段落评论", err) + return + } + + handleReviewSuccess(w, data) +} + +// HandleReviewHTML 处理段落评论 HTML 显示 +func HandleReviewHTML(w http.ResponseWriter, r *http.Request) { + incrementTotal() + + // 验证必需参数 + params, err := validateParams(r, "item_id", "para") + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "

%s

", err.Error()) + return + } + + // 解析整数参数 + para, err := parseIntParam(params["para"], "para") + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "

%s

", err.Error()) + return + } + + // 获取可选参数 + bookID := getOptionalParam(r, "book_id", "") + pageStr := getOptionalParam(r, "page", "1") + pageSizeStr := getOptionalParam(r, "page_size", "20") + sortStr := getOptionalParam(r, "sort", "1") + + // 解析分页参数 + page, _ := parseIntParam(pageStr, "page") + if page < 1 { + page = 1 + } + pageSize, _ := parseIntParam(pageSizeStr, "page_size") + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + // 解析排序参数 + sort, _ := parseIntParam(sortStr, "sort") + if sort != 1 && sort != 3 { + sort = 1 // 默认热门 + } + + // 计算 offset(基于页码) + offset := (page - 1) * pageSize + + // 调用 API + data, err := fqapi.GetParaReview(params["item_id"], bookID, "", para, sort, offset, pageSize) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "

获取评论失败: %s

", err.Error()) + return + } + + // 如果是 AJAX 请求,直接返回 JSON 数据 + if r.Header.Get("X-Requested-With") == "XMLHttpRequest" { + respondJSON(w, http.StatusOK, map[string]interface{}{ + "code": 0, + "data": data, + }) + return + } + + // 生成完整 HTML 页面(仅首次加载) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + + html := generateReviewHTML(data, params["item_id"], bookID, para, page, pageSize, sort) + fmt.Fprint(w, html) +} + +// 在 init 函数中初始化表情管理器 +func init() { + // 初始化表情管理器 + em := fqapi.GetEmojiManager() + em.LoadEmojis() +} + + + +// replaceEmojiWithImages 将表情文本替换为图片标签 +func replaceEmojiWithImages(text string) string { + return fqapi.ReplaceEmojisInText(text) +} + +// parseCommentData 解析评论数据的通用函数 +type CommentInfo struct { + UserName string + UserAvatar string + IsAuthor bool + StickerImageURL string + CreateTime string + CommentText string + CommentID string + DiggCount int + ReplyCount int + ImageURL string + ImageLargeURL string +} + +func parseCommentData(comment map[string]interface{}) CommentInfo { + info := CommentInfo{ + UserName: "匿名用户", + } + + // 提取 comment_id + if cid, ok := comment["comment_id"].(string); ok { + info.CommentID = cid + } + + // 用户信息 + if userInfo, ok := comment["user_info"].(map[string]interface{}); ok { + if name, ok := userInfo["user_name"].(string); ok { + info.UserName = name + } + if avatar, ok := userInfo["user_avatar"].(string); ok { + info.UserAvatar = avatar + } + if author, ok := userInfo["is_author"].(bool); ok { + info.IsAuthor = author + } + } + + // 创建时间 + if timestamp, ok := comment["create_timestamp"].(float64); ok { + t := time.Unix(int64(timestamp), 0) + info.CreateTime = t.Format("2006-01-02 15:04") + } + + // 评论内容 + if text, ok := comment["text"].(string); ok { + info.CommentText = text + } + + // 提取图片 + if imageData, ok := comment["image_data"].([]interface{}); ok && len(imageData) > 0 { + if imgMap, ok := imageData[0].(map[string]interface{}); ok { + if url, ok := imgMap["web_uri"].(string); ok && url != "" { + info.ImageURL = url + } + if largeURL, ok := imgMap["expand_web_url"].(string); ok && largeURL != "" { + info.ImageLargeURL = largeURL + } else { + info.ImageLargeURL = info.ImageURL + } + } + } + + // 统计信息 + if count, ok := comment["digg_count"].(float64); ok { + info.DiggCount = int(count) + } else if count, ok := comment["digg_count"].(int); ok { + info.DiggCount = count + } + + // 回复数量 + if replyCountVal, ok := comment["reply_count"].(float64); ok { + info.ReplyCount = int(replyCountVal) + } else if replyCountVal, ok := comment["reply_count"].(int); ok { + info.ReplyCount = replyCountVal + } else if replyCnt, ok := comment["reply_cnt"].(float64); ok { + info.ReplyCount = int(replyCnt) + } else if replyCnt, ok := comment["reply_cnt"].(int); ok { + info.ReplyCount = replyCnt + } + + return info +} + +// generateReviewHTML 生成评论 HTML 页面 +func generateReviewHTML(data map[string]interface{}, itemID, bookID string, para, page, pageSize, sort int) string { + var html strings.Builder + + html.WriteString(` + + + + + 番茄段评 · 第 `) + html.WriteString(strconv.Itoa(para + 1)) + html.WriteString(` 段 + + + + + +
+

段落评论 · `) + + // 提取评论总数和是否有更多 + total := 0 + hasMore := false + nextOffset := 0 + + // 新接口返回 count 字段表示总数 + if countVal, ok := data["count"].(float64); ok { + total = int(countVal) + } else if countVal, ok := data["count"].(int); ok { + total = countVal + } + + // 检查是否有更多数据 + if hasMoreVal, ok := data["has_more"].(bool); ok { + hasMore = hasMoreVal + } else if hasMoreVal, ok := data["has_more"].(float64); ok { + hasMore = hasMoreVal > 0 + } else if hasMoreVal, ok := data["has_more"].(int); ok { + hasMore = hasMoreVal > 0 + } + + // 获取下一页的 offset + if nextOffsetVal, ok := data["next_offset"].(float64); ok { + nextOffset = int(nextOffsetVal) + } else if nextOffsetVal, ok := data["next_offset"].(int); ok { + nextOffset = nextOffsetVal + } + + html.WriteString(strconv.Itoa(total)) + html.WriteString(` 条

+ + + +
+
最热
+
最新
+
+
+`) + + // 解析评论数据 + comments := []map[string]interface{}{} + if commentsList, ok := data["comments"].([]interface{}); ok { + for _, item := range commentsList { + if comment, ok := item.(map[string]interface{}); ok { + comments = append(comments, comment) + } + } + } + + // 计算分页 + totalComments := len(comments) + startIdx := (page - 1) * pageSize + endIdx := startIdx + pageSize + if endIdx > totalComments { + endIdx = totalComments + } + + // 获取当前页的评论 + var pageComments []map[string]interface{} + if startIdx < totalComments { + pageComments = comments[startIdx:endIdx] + } + + html.WriteString(`
+`) + + if len(pageComments) == 0 { + html.WriteString(`
暂无评论
`) + } else { + for idx, comment := range pageComments { + info := parseCommentData(comment) + + // 生成 HTML + html.WriteString(`
+ +
`) + html.WriteString(replaceEmojiWithImages(htmlEscape(info.CommentText))) + html.WriteString(`
+`) + + if info.ImageURL != "" { + html.WriteString(` 评论图片 +`) + } + + html.WriteString(`
`) + html.WriteString(info.CreateTime) + html.WriteString(`
+
+ `) + html.WriteString(strconv.Itoa(info.DiggCount)) + html.WriteString(` + `) + html.WriteString(strconv.Itoa(info.ReplyCount)) + html.WriteString(` +
+
+
+`) + } + } + + // 如果初始加载就没有更多数据了,显示提示 + if !hasMore && len(pageComments) > 0 { + html.WriteString(` +
没有更多评论了
`) + } + + html.WriteString(` +
+ +
+ × + 图片预览 +
+ +
+ +
+ + + +`) + + return html.String() +} + +// htmlEscape HTML 转义 +func htmlEscape(s string) string { + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, "<", "<") + s = strings.ReplaceAll(s, ">", ">") + s = strings.ReplaceAll(s, "\"", """) + s = strings.ReplaceAll(s, "'", "'") + return s +} + +// generateEmojiMapJS 生成 JavaScript 格式的表情映射 +func generateEmojiMapJS() string { + em := fqapi.GetEmojiManager() + emojiMap := em.GetAllEmojis() + + var sb strings.Builder + sb.WriteString("{") + first := true + for emoji, url := range emojiMap { + if !first { + sb.WriteString(",") + } + first = false + // 转义特殊字符 + escapedEmoji := strings.ReplaceAll(emoji, "\\", "\\\\") + escapedEmoji = strings.ReplaceAll(escapedEmoji, "'", "\\'") + escapedURL := strings.ReplaceAll(url, "\\", "\\\\") + escapedURL = strings.ReplaceAll(escapedURL, "'", "\\'") + sb.WriteString(fmt.Sprintf("'%s':'%s'", escapedEmoji, escapedURL)) + } + sb.WriteString("}") + return sb.String() +} + +// respondJSON 响应 JSON +func respondJSON(w http.ResponseWriter, statusCode int, data interface{}) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(data) +} + diff --git a/internal/handlers/status.go b/internal/handlers/status.go new file mode 100644 index 0000000..fb8f83a --- /dev/null +++ b/internal/handlers/status.go @@ -0,0 +1,1030 @@ +package handlers + +import ( + "encoding/json" + "fanqie/internal/algos" + "fanqie/internal/config" + "fanqie/internal/fqapi" + "fmt" + "net/http" + "time" +) + +// HandleVacuum 处理数据库压缩请求 +func HandleVacuum(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // 只允许 POST 请求 + if r.Method != http.MethodPost { + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": false, + "message": "仅支持 POST 请求", + }) + return + } + + // 执行 VACUUM + err := fqapi.VacuumDatabase() + if err != nil { + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": false, + "message": "压缩失败: " + err.Error(), + }) + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "数据库压缩完成", + }) +} + +// HandleStatus 状态页面处理 +func HandleStatus(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + uptime := time.Since(startTime) + + // 格式化运行时间 + days := int(uptime.Hours() / 24) + hours := int(uptime.Hours()) % 24 + minutes := int(uptime.Minutes()) % 60 + seconds := int(uptime.Seconds()) % 60 + var uptimeStr string + if days > 0 { + uptimeStr = fmt.Sprintf("%d天 %d小时 %d分钟 %d秒", days, hours, minutes, seconds) + } else if hours > 0 { + uptimeStr = fmt.Sprintf("%d小时 %d分钟 %d秒", hours, minutes, seconds) + } else if minutes > 0 { + uptimeStr = fmt.Sprintf("%d分钟 %d秒", minutes, seconds) + } else { + uptimeStr = fmt.Sprintf("%d秒", seconds) + } + + // 获取签名信息 + currentSign := algos.GlobalSignatureSwitcher.GetCurrentMethod() + + // 生成签名详情显示 + var signatureDetails string + if currentSign == "sign2" { + signatureDetails = "X-Gorgon + X-Khronos" + } else { + signatureDetails = "X-Gorgon + X-Ladon + X-Argus + X-Khronos" + } + + // 获取统计数据 + total := getTotal() + search := getSearch() + detail := getDetail() + catalog := getCatalog() + content := getContent() + manga := getManga() + audio := getAudio() + video := getVideo() + category := getCategory() + + // 获取设备密钥池状态 + poolStatus := getDeviceKeyPoolStatus() + + html := fmt.Sprintf(` + + + + + + 番茄小说API - 运行状态 + + + + +
+
+

番茄小说API

+

运行状态监控面板

+
+ +
+
+
● 运行状态
+
+ 服务器时间 + %s +
+
+ 运行时长 + %s +
+
+ 启动时间 + %s +
+
+ 签名方法 + %s +
+
+ 数据库大小 + %s +
+
+ 已用空间 + %s +
+
+ 空闲空间 + %s 立即压缩 +
+
+ +
+
◉ 设备密钥池
+
+ 池状态 + %s +
+
+ 总设备数 + %d +
+
+ 有效设备 + %d 可用 +
+
+ 无效设备 + %d +
+
+ 当前索引 + %d +
+
+ +
+
■ 请求统计
+
+ 总请求数 + %d +
+
+ 搜索请求 + %d +
+
+ 详情请求 + %d +
+
+ 目录请求 + %d +
+
+ 内容请求 + %d +
+
+ 漫画请求 + %d +
+
+ 音频请求 + %d +
+
+ 视频请求 + %d +
+
+ 分类请求 + %d +
+
+ +
+
◆ 缓存统计
+
+ 正文缓存数 + %d +
+
+ 正文缓存 + %s +
+
+ 自动清理 + %s +
+
+ 清理间隔 + %s +
+
+
+ +
+
◈ API 接口文档
+ + +
+
书籍相关
+ +
+
GET/search
+
◆ 搜索书籍
+
+
+ query + 必需 + 搜索关键字 +
+
+ offset + 可选 + 偏移量,默认0 +
+
+ tab_type + 可选 + 类型,默认3 +
+
+
+ +
+
GET/detail
+
◆ 获取书籍详情
+
+
+ book_id + 必需 + 书籍ID +
+
+
+ +
+
GET/catalog
+
◆ 获取书籍目录
+
+
+ book_id + 必需 + 书籍ID +
+
+
+ +
+
GET/multi-detail
+
◆ 批量获取书籍详情
+
+
+ book_id + 必需 + 书籍ID列表,逗号分隔 +
+
+
+
+ + +
+
内容相关
+ +
+
GET/content
+
◆ 获取章节正文内容
+
+
+ item_id + 必需 + 章节ID,支持路径参数或查询参数 +
+
+ text_mode + 可选 + 纯文本模式:1/true=纯文本,0/false=HTML +
+
+ image_mode + 可选 + 图片处理:1/true=保留图片,0/false=移除图片 +
+
+ review + 可选 + 段评功能:1=启用,0=禁用 +
+
+
+ +
+
GET/multi-content
+
◆ 批量获取多个章节正文
+
+
+ book_id + 必需 + 书籍ID +
+
+ item_ids + 必需 + 章节ID列表,多个ID用逗号分隔,如:123,456,789 +
+
+ text_mode + 可选 + 纯文本模式:1/true=纯文本,0/false=HTML +
+
+ image_mode + 可选 + 图片处理:1/true=保留图片,0/false=移除图片 +
+
+ ignore_failed + 可选 + 忽略失败章节:1/true=忽略,0/false=不忽略 +
+
+
+ +
+
GET/item-summary
+
◆ 获取章节摘要信息
+
+
+ book_id + 必需 + 书籍ID +
+
+ item_ids + 必需 + 章节ID列表,逗号分隔 +
+
+
+
+ + +
+
段评相关
+ +
+
GET/review
+
◆ 获取段落评论、回复
+
+
+ item_id + 必需 + 章节ID +
+
+ book_id + 必需 + 书籍ID +
+
+ para + 必需 + 段落索引(从0开始) +
+
+ comment_id + 可选 + 评论ID(获取回复时需要) +
+
+
+ +
+
GET/review_html
+
◆ 获取段落评论HTML页面
+
+
+ item_id + 必需 + 章节ID +
+
+ book_id + 必需 + 书籍ID +
+
+ para + 必需 + 段落索引(从0开始) +
+
+
+
+ + +
+
多媒体
+ +
+
GET/manga
+
◆ 获取漫画内容
+
+
+ item_ids + 必需 + 章节ID +
+
+ output + 可选 + 输出格式:url/proxy/data_uri,默认url +
+
+
+ +
+
GET/audio
+
◆ 获取音频信息
+
+
+ item_ids + 必需 + 章节ID +
+
+ tone_id + 可选 + 音色ID,默认1 +
+
+
+ +
+
GET/video
+
◆ 获取视频信息
+
+
+ item_ids + 必需 + 章节ID +
+
+
+
+ + +
+
分类
+ +
+
GET/category-front
+
◆ 获取分类首页
+
+
+ tab + 可选 + 标签页,默认0 +
+
+
+ +
+
GET/category-landing
+
◆ 获取分类详情页
+
+
+ category_id + 必需 + 分类ID +
+
+ offset + 可选 + 偏移量,默认0 +
+
+ limit + 可选 + 每页数量,默认20 +
+
+
+
+ + +
+
系统
+ +
+
GET/status
+
◆ 系统状态页面(当前页面)
+
+ +
+
GET/self-info
+
◆ 设备信息(包含敏感数据)
+
+
+
+
+ + +`, + time.Now().Format("2006-01-02 15:04:05"), + uptimeStr, + startTime.Format("2006-01-02 15:04:05"), + signatureDetails, + getDBTotalSize(), + getDBUsedSize(), + getDBFreeSize(), + poolStatus["status"], + poolStatus["total"], + poolStatus["valid"], + poolStatus["invalid"], + poolStatus["current_index"], + total, + search, + detail, + catalog, + content, + manga, + audio, + video, + category, + getDBCacheCount(), + getCacheStatus(), + getCleanupStatus(), + getCleanupInterval(), + ) + fmt.Fprint(w, html) +} + +// getCacheStatus 获取缓存状态 +func getCacheStatus() string { + cfg := config.GetConfig() + if cfg != nil && cfg.Cache.Enabled { + return "已启用" + } + return "已禁用" +} + +// getCleanupStatus 获取清理状态 +func getCleanupStatus() string { + cfg := config.GetConfig() + if cfg != nil && cfg.Cache.CleanupEnabled { + return "已启用" + } + return "已禁用" +} + +// getCleanupInterval 获取清理间隔 +func getCleanupInterval() string { + cfg := config.GetConfig() + if cfg != nil { + seconds := cfg.Cache.CleanupSeconds + if seconds == 0 { + return "不清理" + } else if seconds < 60 { + return fmt.Sprintf("%d秒", seconds) + } else if seconds < 3600 { + return fmt.Sprintf("%d分钟", seconds/60) + } else { + return fmt.Sprintf("%d小时", seconds/3600) + } + } + return "未知" +} + +// formatBytes 格式化字节大小 +func formatBytes(bytes int64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := int64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} + +// getDBCacheCount 获取数据库缓存记录数 +func getDBCacheCount() int { + stats, err := fqapi.GetDatabaseStats() + if err != nil || stats == nil { + return 0 + } + if count, ok := stats["cache_count"].(int); ok { + return count + } + return 0 +} + +// getDBTotalSize 获取数据库总大小 +func getDBTotalSize() string { + stats, err := fqapi.GetDatabaseStats() + if err != nil || stats == nil { + return "未知" + } + if size, ok := stats["total_size_bytes"].(int64); ok { + return formatBytes(size) + } + return "未知" +} + +// getDBUsedSize 获取数据库已用空间 +func getDBUsedSize() string { + stats, err := fqapi.GetDatabaseStats() + if err != nil || stats == nil { + return "未知" + } + if size, ok := stats["used_size_bytes"].(int64); ok { + return formatBytes(size) + } + return "未知" +} + +// getDBFreeSize 获取数据库空闲空间(带颜色提示) +func getDBFreeSize() string { + stats, err := fqapi.GetDatabaseStats() + if err != nil || stats == nil { + return "未知" + } + freeSize, okFree := stats["free_size_bytes"].(int64) + totalSize, okTotal := stats["total_size_bytes"].(int64) + + if !okFree { + return "未知" + } + + if freeSize == 0 { + return "0 B" + } + + freeSizeStr := formatBytes(freeSize) + + // 如果有总大小,计算并显示百分比 + if okTotal && totalSize > 0 { + percentage := float64(freeSize) * 100 / float64(totalSize) + return fmt.Sprintf("%s (%.1f%%)", freeSizeStr, percentage) + } + + return freeSizeStr +} + +// getDBUsagePercentage 获取数据库使用率 +func getDBUsagePercentage() float64 { + stats, err := fqapi.GetDatabaseStats() + if err != nil || stats == nil { + return 0 + } + usedSize, okUsed := stats["used_size_bytes"].(int64) + totalSize, okTotal := stats["total_size_bytes"].(int64) + + if !okUsed || !okTotal || totalSize == 0 { + return 0 + } + + return float64(usedSize) * 100 / float64(totalSize) +} + +// getDeviceKeyPoolStatus 获取设备密钥池状态 +func getDeviceKeyPoolStatus() map[string]interface{} { + pool := fqapi.GetDeviceKeyPool() + + if !pool.IsEnabled() { + return map[string]interface{}{ + "status": "未启用", + "total": 0, + "valid": 0, + "invalid": 0, + "current_index": 0, + } + } + + stats := pool.GetStats() + return map[string]interface{}{ + "status": "已启用", + "total": stats["total"], + "valid": stats["valid"], + "invalid": stats["invalid"], + "current_index": stats["current_index"], + } +} +