Leeflour commited on
Commit
2a7394b
·
verified ·
1 Parent(s): db19e8d

Upload 41 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ .git
4
+ .venv
5
+ tests
6
+
7
+ # Git repository files
8
+ .git/
9
+ .gitignore
10
+
11
+ # Node modules
12
+ node_modules/
13
+ frontend/node_modules/
14
+
15
+ # Environment files (Ensure sensitive data isn't accidentally included)
16
+ .env
17
+ frontend/.env
18
+
19
+ # Build artifacts (Frontend build happens inside container, but good practice)
20
+ dist/
21
+ build/
22
+ frontend/dist/
23
+ frontend/build/
24
+
25
+ # Logs
26
+ logs/
27
+ *.log
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ pnpm-debug.log*
32
+
33
+ # OS generated files
34
+ .DS_Store
35
+ Thumbs.db
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ MAIL_POINT_API_URL=https://your-endpoint.com # 使用 https://github.com/HChaoHui/msOauth2api 进行部署拿到URL
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .env
2
+ account/*
3
+ account copy/
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:lts-alpine AS frontend-builder
2
+
3
+ WORKDIR /app/frontend
4
+
5
+ RUN npm install -g pnpm
6
+
7
+ COPY frontend/package.json frontend/pnpm-lock.yaml ./
8
+
9
+ RUN pnpm install --frozen-lockfile
10
+
11
+ COPY frontend/ ./
12
+
13
+ RUN pnpm build
14
+
15
+ FROM python:3.10-slim AS final
16
+
17
+ WORKDIR /app
18
+
19
+ COPY requirements.txt ./
20
+ RUN pip install --no-cache-dir --upgrade pip && \
21
+ pip install --no-cache-dir -r requirements.txt
22
+
23
+ RUN mkdir account
24
+
25
+ RUN mkdir -p templates static
26
+
27
+ COPY run.py ./
28
+ COPY utils/ ./utils/
29
+
30
+ COPY --from=frontend-builder /app/frontend/dist/index.html ./templates/
31
+ COPY --from=frontend-builder /app/frontend/dist/* ./static/
32
+
33
+ EXPOSE 5000
34
+
35
+ CMD ["python", "run.py"]
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md CHANGED
@@ -1,10 +1,135 @@
1
- ---
2
- title: Pikpakauto
3
- emoji: 😻
4
- colorFrom: pink
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PikPak 自动邀请
2
+
3
+ 一个帮助管理PikPak邀请的工具,包含前端界面和后端服务。
4
+
5
+ **理论上输入账号后,一下都不用点,等着把列表里面账号注册完成就行**
6
+
7
+ ## 项目结构
8
+
9
+ - `frontend/`: 前端代码,使用 pnpm 管理依赖
10
+ - 后端: Python 实现的服务
11
+
12
+ ## 环境变量
13
+ (可选) MAIL_POINT_API_URL 使用:https://github.com/HChaoHui/msOauth2api 部署后获得
14
+
15
+ 如果不提供此环境变量,需要(邮箱,密码)支持imap登录
16
+
17
+ ## 部署方式
18
+
19
+ ### 前端部署
20
+
21
+ ```bash
22
+ # 进入前端目录
23
+ cd frontend
24
+
25
+ # 安装依赖
26
+ pnpm install
27
+
28
+ # 开发模式运行
29
+ pnpm dev
30
+
31
+ # 构建生产版本
32
+ pnpm build
33
+ ```
34
+
35
+ ### 后端部署
36
+
37
+ #### 1. 环境变量
38
+ 复制 .env.example 到 .env
39
+
40
+ 修改环境变量的值
41
+
42
+ ```bash
43
+ MAIL_POINT_API_URL=https://your-endpoint.com
44
+ ```
45
+
46
+ #### 2. 源码运行
47
+
48
+ ```bash
49
+ # 安装依赖
50
+ pip install -r requirements.txt
51
+
52
+ # 运行应用
53
+ python run.py
54
+ ```
55
+
56
+ ### Docker 部署
57
+
58
+ 项目提供了 Dockerfile,可以一键构建包含前后端的完整应用。
59
+
60
+ #### 运行 Docker 容器
61
+
62
+ ```bash
63
+ # 创建并运行容器
64
+ docker run -d \
65
+ --name pikpak-auto \
66
+ -p 5000:5000 \
67
+ -e MAIL_POINT_API_URL=https://your-endpoint.com \
68
+ -v $(pwd)/account:/app/account \
69
+ vichus/pikpak-invitation:latest
70
+ ```
71
+
72
+ 参数说明:
73
+ - `-d`: 后台运行容器
74
+ - `-p 5000:5000`: 将容器内的 5000 端口映射到主机的 5000 端口
75
+ - `-e MAIL_POINT_API_URL=...`: 设置环境变量
76
+ - `-v $(pwd)/account:/app/account`: 将本地 account 目录挂载到容器内,保存账号数据
77
+
78
+ #### 4. 查看容器日志
79
+
80
+ ```bash
81
+ docker logs -f pikpak-auto
82
+ ```
83
+
84
+ #### 5. 停止和重启容器
85
+
86
+ ```bash
87
+ # 停止容器
88
+ docker stop pikpak-auto
89
+
90
+ # 重启容器
91
+ docker start pikpak-auto
92
+ ```
93
+
94
+ 注意:Windows 用户在使用 PowerShell 时,挂载卷的命令可能需要修改为:
95
+ ```powershell
96
+ docker run -d --name pikpak-auto -p 5000:5000 -e MAIL_POINT_API_URL=https://your-endpoint.com -v ${PWD}/account:/app/account vichus/pikpak-invitation
97
+ ```
98
+
99
+ ### Docker Compose 部署
100
+
101
+ 如果你更喜欢使用 Docker Compose 进行部署,请按照以下步骤操作:
102
+
103
+ #### 1. 启动服务
104
+
105
+ 启动前记得修改 `docker-compose.yml` 的环境变量
106
+
107
+ ```bash
108
+ # 在项目根目录下启动服务
109
+ docker-compose up -d
110
+ ```
111
+
112
+ #### 2. 查看日志
113
+
114
+ ```bash
115
+ # 查看服务日志
116
+ docker-compose logs -f
117
+ ```
118
+
119
+ #### 3. 停止和重启服务
120
+
121
+ ```bash
122
+ # 停止服务
123
+ docker-compose down
124
+
125
+ # 重启服务
126
+ docker-compose up -d
127
+ ```
128
+
129
+ 鸣谢:
130
+
131
+ [Pikpak-Auto-Invitation](https://github.com/Bear-biscuit/Pikpak-Auto-Invitation)
132
+
133
+ [纸鸢地址发布页](https://kiteyuan.info/)
134
+
135
+ [msOauth2api](https://github.com/HChaoHui/msOauth2api)
docker-compose.yml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3'
2
+
3
+ services:
4
+ pikpak-auto:
5
+ image: vichus/pikpak-invitation:latest
6
+ container_name: pikpak-auto
7
+ ports:
8
+ - "5000:5000"
9
+ environment:
10
+ - MAIL_POINT_API_URL=https://your-endpoint.com
11
+ volumes:
12
+ - ./account:/app/account
13
+ restart: unless-stopped
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## Expanding the ESLint configuration
11
+
12
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
13
+
14
+ ```js
15
+ export default tseslint.config({
16
+ extends: [
17
+ // Remove ...tseslint.configs.recommended and replace with this
18
+ ...tseslint.configs.recommendedTypeChecked,
19
+ // Alternatively, use this for stricter rules
20
+ ...tseslint.configs.strictTypeChecked,
21
+ // Optionally, add this for stylistic rules
22
+ ...tseslint.configs.stylisticTypeChecked,
23
+ ],
24
+ languageOptions: {
25
+ // other options...
26
+ parserOptions: {
27
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
28
+ tsconfigRootDir: import.meta.dirname,
29
+ },
30
+ },
31
+ })
32
+ ```
33
+
34
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
35
+
36
+ ```js
37
+ // eslint.config.js
38
+ import reactX from 'eslint-plugin-react-x'
39
+ import reactDom from 'eslint-plugin-react-dom'
40
+
41
+ export default tseslint.config({
42
+ plugins: {
43
+ // Add the react-x and react-dom plugins
44
+ 'react-x': reactX,
45
+ 'react-dom': reactDom,
46
+ },
47
+ rules: {
48
+ // other rules...
49
+ // Enable its recommended typescript rules
50
+ ...reactX.configs['recommended-typescript'].rules,
51
+ ...reactDom.configs.recommended.rules,
52
+ },
53
+ })
54
+ ```
frontend/eslint.config.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ },
28
+ )
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>PikPak 自助邀请助手</title>
8
+ </head>
9
+ <body>
10
+ <div id="root" style="width: 100%; height: 100%;"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
frontend/package.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@ant-design/icons": "^6.0.0",
14
+ "@ant-design/v5-patch-for-react-19": "^1.0.3",
15
+ "antd": "^5.24.9",
16
+ "axios": "^1.9.0",
17
+ "react": "^19.0.0",
18
+ "react-dom": "^19.0.0",
19
+ "react-router-dom": "^7.5.3"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^9.22.0",
23
+ "@types/react": "^19.0.10",
24
+ "@types/react-dom": "^19.0.4",
25
+ "@vitejs/plugin-react": "^4.3.4",
26
+ "eslint": "^9.22.0",
27
+ "eslint-plugin-react-hooks": "^5.2.0",
28
+ "eslint-plugin-react-refresh": "^0.4.19",
29
+ "globals": "^16.0.0",
30
+ "typescript": "~5.7.2",
31
+ "typescript-eslint": "^8.26.1",
32
+ "vite": "^6.3.1"
33
+ }
34
+ }
frontend/pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
frontend/public/vite.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* App.css */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
10
+ 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
11
+ 'Noto Color Emoji';
12
+ background-color: #f0f2f5;
13
+ }
14
+
15
+ /* Remove old layout styles */
16
+ /*
17
+ .app-container {
18
+ display: flex;
19
+ flex-direction: column;
20
+ min-height: 100vh;
21
+ }
22
+
23
+ .content-container {
24
+ flex: 1;
25
+ padding: 20px;
26
+ }
27
+
28
+ @media (max-width: 768px) {
29
+ .content-container {
30
+ padding: 10px;
31
+ }
32
+ }
33
+ */
34
+
35
+ /* Remove or comment out #root restrictions to allow full width */
36
+ /*
37
+ #root {
38
+ max-width: 1280px;
39
+ margin: 0 auto;
40
+ padding: 2rem;
41
+ text-align: center;
42
+ }
43
+ */
44
+
45
+ /* Add styles for the logo in the sidebar */
46
+ .sidebar-logo {
47
+ height: 32px;
48
+ margin: 16px;
49
+ background: rgba(255, 255, 255, 0.2);
50
+ border-radius: 4px;
51
+ text-align: center;
52
+ line-height: 32px;
53
+ color: white;
54
+ font-weight: bold;
55
+ overflow: hidden;
56
+ white-space: nowrap; /* Prevent text wrap when collapsed */
57
+ }
58
+
59
+ /* Remove the rule for the now-deleted .site-layout element */
60
+ /*
61
+ .site-layout {
62
+ flex: 1;
63
+ }
64
+ */
65
+
66
+ /* Keep other potentially useful styles if needed, e.g., card, logo animation, etc. */
67
+ /* These might be template defaults or used elsewhere, review if needed */
68
+ .logo {
69
+ height: 6em;
70
+ padding: 1.5em;
71
+ will-change: filter;
72
+ transition: filter 300ms;
73
+ }
74
+ .logo:hover {
75
+ filter: drop-shadow(0 0 2em #646cffaa);
76
+ }
77
+ .logo.react:hover {
78
+ filter: drop-shadow(0 0 2em #61dafbaa);
79
+ }
80
+
81
+ @keyframes logo-spin {
82
+ from {
83
+ transform: rotate(0deg);
84
+ }
85
+ to {
86
+ transform: rotate(360deg);
87
+ }
88
+ }
89
+
90
+ @media (prefers-reduced-motion: no-preference) {
91
+ a:nth-of-type(2) .logo {
92
+ animation: logo-spin infinite 20s linear;
93
+ }
94
+ }
95
+
96
+ .card {
97
+ padding: 2em;
98
+ }
99
+
100
+ .read-the-docs {
101
+ color: #888;
102
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { BrowserRouter as Router, Routes, Route, Navigate, useLocation, Link } from 'react-router-dom';
3
+ import { ConfigProvider, Layout, Menu } from 'antd';
4
+ import {
5
+ UserAddOutlined,
6
+ CheckCircleOutlined,
7
+ HistoryOutlined
8
+ } from '@ant-design/icons';
9
+ import zhCN from 'antd/lib/locale/zh_CN';
10
+ import './App.css';
11
+
12
+ // 导入页面组件 (needed in MainLayout)
13
+ import Register from './pages/Register';
14
+ import Activate from './pages/Activate';
15
+ import History from './pages/History';
16
+
17
+ const { Sider, Content } = Layout;
18
+
19
+ // Define the new MainLayout component
20
+ const MainLayout: React.FC = () => {
21
+ const [collapsed, setCollapsed] = useState(false); // Move state here
22
+ const location = useLocation(); // Move hook call here
23
+ const currentPath = location.pathname;
24
+
25
+ // Move menu items definition here
26
+ const items = [
27
+ {
28
+ key: '/register',
29
+ icon: <UserAddOutlined />,
30
+ label: <Link to="/register">账号注册</Link>,
31
+ },
32
+ {
33
+ key: '/activate',
34
+ icon: <CheckCircleOutlined />,
35
+ label: <Link to="/activate">账号激活</Link>,
36
+ },
37
+ {
38
+ key: '/history',
39
+ icon: <HistoryOutlined />,
40
+ label: <Link to="/history">历史账号</Link>,
41
+ },
42
+ ];
43
+
44
+ // Move the Layout JSX structure here
45
+ return (
46
+ <Layout style={{ minHeight: '100vh' }}>
47
+ <Sider collapsible collapsed={collapsed} onCollapse={(value) => setCollapsed(value)}>
48
+ <div className="sidebar-logo">
49
+ {collapsed ? "P" : "PikPak 自动邀请"}
50
+ </div>
51
+ <Menu theme="dark" mode="inline" selectedKeys={[currentPath]} items={items} />
52
+ </Sider>
53
+ <Content style={{ margin: '0', width: '100%' }}>
54
+ <div
55
+ className="site-layout-background"
56
+ style={{
57
+ padding: 24,
58
+ minHeight: '100vh',
59
+ background: '#fff',
60
+ boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)'
61
+ }}
62
+ >
63
+ {/* Routes are rendered here, inside the Router context */}
64
+ <Routes>
65
+ <Route path="/" element={<Navigate to="/register" replace />} />
66
+ <Route path="/register" element={<Register />} />
67
+ <Route path="/activate" element={<Activate />} />
68
+ <Route path="/history" element={<History />} />
69
+ </Routes>
70
+ </div>
71
+ </Content>
72
+ </Layout>
73
+ );
74
+ };
75
+
76
+ // Simplify the App component
77
+ function App() {
78
+ return (
79
+ <ConfigProvider locale={zhCN}>
80
+ <Router>
81
+ {/* Render MainLayout inside Router */}
82
+ <MainLayout />
83
+ </Router>
84
+ </ConfigProvider>
85
+ );
86
+ }
87
+
88
+ export default App;
frontend/src/assets/react.svg ADDED
frontend/src/components/Header/index.css ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Comment out or remove old container style
2
+ .header-container {
3
+ display: flex;
4
+ align-items: center;
5
+ padding: 0 24px;
6
+ background-color: #001529;
7
+ color: white;
8
+ height: 64px;
9
+ }
10
+ */
11
+
12
+ /* Add new style for Layout.Header */
13
+ .header-layout {
14
+ display: flex; /* Use flexbox for alignment */
15
+ align-items: center; /* Vertically center items */
16
+ padding: 0 30px; /* Adjust horizontal padding */
17
+ }
18
+
19
+ .logo {
20
+ font-size: 20px;
21
+ font-weight: bold;
22
+ margin-right: 30px; /* Adjust margin for spacing */
23
+ }
24
+
25
+ .logo a {
26
+ /* color: white; Remove this, Layout.Header theme handles it */
27
+ text-decoration: none;
28
+ }
29
+
30
+ .header-menu {
31
+ flex: 1; /* Keep this to fill remaining space */
32
+ }
frontend/src/components/Header/index.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Menu, Layout } from 'antd';
3
+ import { Link, useLocation } from 'react-router-dom';
4
+ import './index.css';
5
+
6
+ const Header: React.FC = () => {
7
+ const location = useLocation();
8
+ const currentPath = location.pathname;
9
+
10
+ const items = [
11
+ {
12
+ key: '/register',
13
+ label: <Link to="/register">账号注册</Link>,
14
+ },
15
+ {
16
+ key: '/activate',
17
+ label: <Link to="/activate">账号激活</Link>,
18
+ },
19
+ {
20
+ key: '/history',
21
+ label: <Link to="/history">历史账号</Link>,
22
+ },
23
+ ];
24
+
25
+ return (
26
+ <Layout.Header className="header-layout">
27
+ <div className="logo">
28
+ <Link to="/">PikPak 自动邀请</Link>
29
+ </div>
30
+ <Menu
31
+ theme="dark"
32
+ mode="horizontal"
33
+ selectedKeys={[currentPath]}
34
+ items={items}
35
+ className="header-menu"
36
+ />
37
+ </Layout.Header>
38
+ );
39
+ };
40
+
41
+ export default Header;
frontend/src/index.css ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ button {
39
+ border-radius: 8px;
40
+ border: 1px solid transparent;
41
+ padding: 0.6em 1.2em;
42
+ font-size: 1em;
43
+ font-weight: 500;
44
+ font-family: inherit;
45
+ background-color: #1a1a1a;
46
+ cursor: pointer;
47
+ transition: border-color 0.25s;
48
+ }
49
+ button:hover {
50
+ border-color: #646cff;
51
+ }
52
+ button:focus,
53
+ button:focus-visible {
54
+ outline: 4px auto -webkit-focus-ring-color;
55
+ }
56
+
57
+ @media (prefers-color-scheme: light) {
58
+ :root {
59
+ color: #213547;
60
+ background-color: #ffffff;
61
+ }
62
+ a:hover {
63
+ color: #747bff;
64
+ }
65
+ button {
66
+ background-color: #f9f9f9;
67
+ }
68
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+ import '@ant-design/v5-patch-for-react-19'
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>,
11
+ )
frontend/src/pages/Activate/index.css ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .activate-container {
2
+ display: flex;
3
+ justify-content: center;
4
+ max-width: 800px;
5
+ margin: 0 auto;
6
+ max-height: calc(100vh - 80px);
7
+ }
8
+
9
+ .activate-card {
10
+ width: 100%;
11
+ height: calc(100vh - 80px);
12
+ overflow-y: auto;
13
+ }
14
+
15
+ .key-input-container {
16
+ display: flex;
17
+ margin-bottom: 24px;
18
+ gap: 16px;
19
+ }
20
+
21
+ .results-container {
22
+ margin-top: 24px;
23
+ border-top: 1px solid #f0f0f0;
24
+ padding-top: 16px;
25
+ }
frontend/src/pages/Activate/index.tsx ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Card, Input, Button, Typography, Tag, message, Result, Spin, Row, Col, Table, Space } from 'antd';
3
+ import './index.css';
4
+ import { activateAccounts, fetchAccounts } from '../../services/api';
5
+ import type { ColumnsType } from 'antd/es/table';
6
+
7
+ const { Paragraph } = Typography;
8
+
9
+ interface AccountResult {
10
+ status: 'success' | 'error';
11
+ account: string;
12
+ message?: string;
13
+ result?: any;
14
+ updated?: boolean;
15
+ }
16
+
17
+ interface Account {
18
+ email: string;
19
+ name: string;
20
+ filename: string;
21
+ user_id?: string;
22
+ version?: string;
23
+ device_id?: string;
24
+ timestamp?: string;
25
+ invite_code?: string; // 新增邀请码字段
26
+ // 其他账户属性...
27
+ }
28
+
29
+ // 格式化时间戳
30
+ const formatTimestamp = (timestampStr?: string): string => {
31
+ if (!timestampStr) return '-';
32
+ try {
33
+ const timestamp = parseInt(timestampStr, 10);
34
+ if (isNaN(timestamp)) return '无效时间戳';
35
+ // 检查时间戳是否是毫秒级,如果不是(例如秒级),乘以1000
36
+ const date = new Date(timestamp < 10000000000 ? timestamp * 1000 : timestamp);
37
+ return date.toLocaleString('zh-CN'); // 使用本地化格式
38
+ } catch (e) {
39
+ console.error("Error formatting timestamp:", e);
40
+ return '格式化错误';
41
+ }
42
+ };
43
+
44
+ const Activate: React.FC = () => {
45
+ const [key, setKey] = useState('');
46
+ const [loading, setLoading] = useState(false);
47
+ const [results, setResults] = useState<AccountResult[]>([]);
48
+ const [view, setView] = useState<'initial' | 'loading' | 'accounts' | 'success_summary'>('initial');
49
+ const [successMessage, setSuccessMessage] = useState<string>('');
50
+
51
+ // 状态
52
+ const [accounts, setAccounts] = useState<Account[]>([]);
53
+ const [selectedAccounts, setSelectedAccounts] = useState<string[]>([]);
54
+ const [loadingAccounts, setLoadingAccounts] = useState(false);
55
+ const [activatingAccount, setActivatingAccount] = useState<string>(''); // 当前正在激活的单个账号
56
+
57
+ // 组件加载时获取账户列表
58
+ useEffect(() => {
59
+ loadAccounts();
60
+ }, []);
61
+
62
+ // 加载账户列表函数
63
+ const loadAccounts = async () => {
64
+ setLoadingAccounts(true);
65
+ setView('loading');
66
+ try {
67
+ const response = await fetchAccounts();
68
+ if (response.data.status === 'success') {
69
+ setAccounts(response.data.accounts || []);
70
+ setView('accounts'); // 加载成功后显示账户列表
71
+ } else {
72
+ message.error(response.data.message || '获取账户列表失败');
73
+ setView('initial'); // 失败返回初始状态
74
+ }
75
+ } catch (error: any) {
76
+ console.error('获取账户错误:', error);
77
+ message.error('获取账户列表时出错: ' + (error.message || '未知错误'));
78
+ setView('initial'); // 异常返回初始状态
79
+ } finally {
80
+ setLoadingAccounts(false);
81
+ }
82
+ };
83
+
84
+ // 处理所选行变化
85
+ const onSelectChange = (selectedRowKeys: React.Key[]) => {
86
+ setSelectedAccounts(selectedRowKeys as string[]);
87
+ };
88
+
89
+ // 激活单个账号
90
+ const handleActivateSingle = async (account: string) => {
91
+ if (!key.trim()) {
92
+ message.error('请输入激活密钥');
93
+ return;
94
+ }
95
+
96
+ const account_name = account.split('@')[0];
97
+
98
+ setActivatingAccount(account_name);
99
+ setLoading(true);
100
+ try {
101
+ // 使用之前的API,但只针对单个账号
102
+ const response = await activateAccounts(key, [account_name], false);
103
+ const data = response.data;
104
+
105
+ if (data.status === 'success') {
106
+ const result = data.results.find((r: any) => r.account === account);
107
+ message.success(`账号 ${account} 激活${result?.status === 'success' ? '成功' : '失败'}`);
108
+
109
+ // 更新当前结果列表中对应的账号状态
110
+ const updatedResults = [...results];
111
+ const index = updatedResults.findIndex((r: AccountResult) => r.account === account);
112
+ if (index !== -1) {
113
+ const updatedAccount = data.results.find((r: any) => r.account === account);
114
+ if (updatedAccount) {
115
+ updatedResults[index] = updatedAccount;
116
+ setResults(updatedResults);
117
+ }
118
+ }
119
+ } else {
120
+ message.error(data.message || '激活操作返回错误');
121
+ }
122
+ } catch (error: any) {
123
+ console.error('激活错误:', error);
124
+ message.error(error.message || '激活过程中发生网络或未知错误');
125
+ } finally {
126
+ setLoading(false);
127
+ setActivatingAccount('');
128
+ }
129
+ };
130
+
131
+ // 激活所有账户
132
+ const handleActivateAll = async () => {
133
+ await handleActivate(true, []);
134
+ };
135
+
136
+ // 激活选定账户
137
+ const handleActivateSelected = async () => {
138
+ if (selectedAccounts.length === 0) {
139
+ message.warning('请至少选择一个账户进行激活');
140
+ return;
141
+ }
142
+ await handleActivate(false, selectedAccounts);
143
+ };
144
+
145
+ // 激活账户通用函数
146
+ const handleActivate = async (activateAll: boolean, names: string[]) => {
147
+ if (!key.trim()) {
148
+ message.error('请输入激活密钥');
149
+ return;
150
+ }
151
+
152
+ setLoading(true);
153
+ setView('loading'); // 设置为加载中视图
154
+ setResults([]);
155
+ setSuccessMessage('');
156
+
157
+ try {
158
+ const response = await activateAccounts(key, names, activateAll);
159
+ const data = response.data;
160
+
161
+ if (data.status === 'success') {
162
+ // message.success(data.message || '激活成功完成'); // 使用下方摘要信息
163
+ setResults(data.results || []);
164
+ setSuccessMessage(data.message || '激活成功完成');
165
+ setView('success_summary'); // 显示成功摘要视图
166
+ setSelectedAccounts([]); // 清空选择,为下次做准备
167
+ } else if (data.status === 'error') {
168
+ message.error(data.message || '激活操作返回错误');
169
+ setView('accounts'); // 激活失败返回账户列表视图
170
+ } else {
171
+ message.warning('收到未知的响应状态');
172
+ setView('accounts'); // 未知状态也返回账户列表
173
+ }
174
+
175
+ } catch (error: any) {
176
+ console.error('激活错误:', error);
177
+ message.error(error.message || '激活过程中发生网络或未知错误');
178
+ setView('accounts'); // 异常返回账户列表
179
+ } finally {
180
+ setLoading(false);
181
+ }
182
+ };
183
+
184
+ // 返回账户选择视图
185
+ const handleContinueActivating = () => {
186
+ // setView('accounts'); // 先不切换视图,等待加载完成
187
+ setResults([]); // 可以选择性清空结果
188
+ setSuccessMessage('');
189
+ loadAccounts(); // 重新加载账户列表,加载函数内部会设置视图
190
+ };
191
+
192
+ // 表格列定义
193
+ const columns: ColumnsType<Account> = [
194
+ {
195
+ title: '邀请码',
196
+ dataIndex: 'invite_code',
197
+ key: 'invite_code',
198
+ width: '15%',
199
+ render: (invite_code?: string) => invite_code || '-',
200
+ ellipsis: true,
201
+ },
202
+ {
203
+ title: '名称',
204
+ dataIndex: 'name',
205
+ key: 'name',
206
+ width: '15%',
207
+ },
208
+ {
209
+ title: '邮箱',
210
+ dataIndex: 'email',
211
+ key: 'email',
212
+ render: (text) => <span style={{ fontWeight: 'bold' }}>{text}</span>,
213
+ ellipsis: true,
214
+ },
215
+ {
216
+ title: 'Device ID',
217
+ dataIndex: 'device_id',
218
+ key: 'device_id',
219
+ width: '30%',
220
+ ellipsis: true,
221
+ },
222
+ {
223
+ title: '创建时间',
224
+ dataIndex: 'timestamp',
225
+ key: 'timestamp',
226
+ width: '20%',
227
+ render: (timestamp) => formatTimestamp(timestamp),
228
+ sorter: (a, b) => parseInt(a.timestamp || '0') - parseInt(b.timestamp || '0'),
229
+ defaultSortOrder: 'descend',
230
+ }
231
+ ];
232
+
233
+ // 表格行选择配置
234
+ const rowSelection = {
235
+ selectedRowKeys: selectedAccounts,
236
+ onChange: onSelectChange,
237
+ };
238
+
239
+ // 结果表格列定义(现在用于成功摘要)
240
+ const successResultColumns: ColumnsType<AccountResult> = [
241
+ {
242
+ title: '邮箱',
243
+ dataIndex: 'account',
244
+ key: 'account',
245
+ width: '50%',
246
+ },
247
+ {
248
+ title: '状态',
249
+ key: 'status',
250
+ width: '30%',
251
+ render: (_, record) => (
252
+ record.status === 'success'
253
+ ? <Tag color="success">已激活{record.updated && ' (数据已更新)'}</Tag>
254
+ : <Tag color="error">{record.message || '激活失败'}</Tag>
255
+ ),
256
+ },
257
+ {
258
+ title: '操作',
259
+ key: 'action',
260
+ width: '20%',
261
+ render: (_, record) => (
262
+ record.status === 'error' && (
263
+ <Button
264
+ type="primary"
265
+ size="small"
266
+ onClick={() => handleActivateSingle(record.account)}
267
+ loading={loading && activatingAccount === record.account}
268
+ >
269
+ 重试
270
+ </Button>
271
+ )
272
+ ),
273
+ }
274
+ ];
275
+
276
+ return (
277
+ <div className="activate-container">
278
+ <Card title="PikPak 账号激活" className="activate-card" variant="borderless">
279
+ <div style={{ marginBottom: '20px' }}>
280
+ <Row gutter={16} align="middle">
281
+ <Col span={16}>
282
+ <Input.Password
283
+ placeholder="请输入激活密钥"
284
+ value={key}
285
+ onChange={e => setKey(e.target.value)}
286
+ style={{ width: '100%' }}
287
+ size="large"
288
+ disabled={view === 'loading'} // 加载时禁用
289
+ />
290
+ </Col>
291
+ <Col span={8}>
292
+ <Space>
293
+ <Button
294
+ type="primary"
295
+ onClick={handleActivateSelected}
296
+ loading={loading && view === 'loading'} // 仅在加载中且是当前操作时显示loading
297
+ disabled={selectedAccounts.length === 0 || view !== 'accounts'} // 仅在账户视图且有选择时可用
298
+ size="large"
299
+ >
300
+ 激活选定 ({selectedAccounts.length})
301
+ </Button>
302
+ <Button
303
+ onClick={handleActivateAll}
304
+ loading={loading && view === 'loading'} // 同上
305
+ disabled={view !== 'accounts'} // 仅在账户视图可用
306
+ size="large"
307
+ >
308
+ 激活全部
309
+ </Button>
310
+ </Space>
311
+ </Col>
312
+ </Row>
313
+ <Paragraph style={{ marginTop: '8px', color: '#888' }}>
314
+ 激活密钥在 <a href="https://kiteyuan.info" target="_blank" rel="noopener noreferrer">纸鸢佬的导航</a>
315
+ </Paragraph>
316
+ </div>
317
+
318
+ {/* 加载中提示 */}
319
+ {view === 'loading' && (
320
+ <div style={{ textAlign: 'center', padding: '40px 0' }}>
321
+ <Spin size="large" />
322
+ </div>
323
+ )}
324
+
325
+ {/* 账户列表视图 */}
326
+ {view === 'accounts' && (
327
+ <div className="accounts-container">
328
+ <Table
329
+ rowSelection={rowSelection}
330
+ columns={columns}
331
+ dataSource={accounts}
332
+ rowKey="filename"
333
+ loading={loadingAccounts} // 表格自身的加载状态
334
+ pagination={{ pageSize: 10 }}
335
+ size="middle"
336
+ locale={{ emptyText: '未找到账户数据,请先注册账户' }}
337
+ summary={() => (
338
+ <Table.Summary fixed>
339
+ <Table.Summary.Row>
340
+ <Table.Summary.Cell index={0} colSpan={columns.length + 1}>
341
+ <div style={{ textAlign: 'left', padding: '8px 0' }}>
342
+ 已选择 {selectedAccounts.length} 个账户 (共 {accounts.length} 个)
343
+ </div>
344
+ </Table.Summary.Cell>
345
+ </Table.Summary.Row>
346
+ </Table.Summary>
347
+ )}
348
+ />
349
+ </div>
350
+ )}
351
+
352
+ {/* 成功摘要视图 */}
353
+ {view === 'success_summary' && (
354
+ <div className="results-container" style={{ marginTop: '30px' }}>
355
+ <Result
356
+ status="success"
357
+ title="激活操作完成"
358
+ subTitle={successMessage}
359
+ extra={[
360
+ <Button type="primary" key="continue" onClick={handleContinueActivating}>
361
+ 继续激活
362
+ </Button>,
363
+ ]}
364
+ />
365
+ {/* 可选:显示简化的成功列表 */}
366
+ {results.length > 0 && (
367
+ <Table
368
+ columns={successResultColumns}
369
+ dataSource={results} // 显示所有结果,包括成功和失败的
370
+ rowKey="account"
371
+ pagination={{ pageSize: 5 }} // 分页显示
372
+ size="small"
373
+ style={{ marginTop: '20px' }}
374
+ />
375
+ )}
376
+ </div>
377
+ )}
378
+ </Card>
379
+ </div>
380
+ );
381
+ };
382
+
383
+ export default Activate;
frontend/src/pages/History/index.css ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .history-container {
2
+ max-width: 1000px;
3
+ margin: 0 auto;
4
+ }
5
+
6
+ .history-card {
7
+ width: 100%;
8
+
9
+ }
10
+
11
+ .history-card .ant-card-body {
12
+ padding: 0;
13
+ }
14
+
15
+ .account-details {
16
+ margin-top: 16px;
17
+ }
18
+
19
+ .token-container {
20
+ max-width: 100%;
21
+ overflow-x: auto;
22
+ overflow-y: hidden;
23
+ padding: 8px;
24
+ background-color: #f5f5f5;
25
+ border-radius: 4px;
26
+ margin-top: 4px;
27
+ word-break: break-all;
28
+ white-space: normal;
29
+ font-family: monospace;
30
+ }
frontend/src/pages/History/index.tsx ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Table, Card, Button, message, Modal, Typography, Tag, Space, Popconfirm } from 'antd';
3
+ import { ReloadOutlined, DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons';
4
+ import { fetchAccounts as apiFetchAccounts, deleteAccount, deleteAccounts } from '../../services/api';
5
+ import './index.css';
6
+
7
+ const { Text, Paragraph } = Typography;
8
+
9
+ interface AccountInfo {
10
+ name?: string;
11
+ email?: string;
12
+ password?: string;
13
+ user_id?: string;
14
+ device_id?: string;
15
+ version?: string;
16
+ access_token?: string;
17
+ refresh_token?: string;
18
+ filename: string;
19
+ captcha_token?: string;
20
+ timestamp?: number;
21
+ invite_code?: string; // 新增邀请码字段
22
+ }
23
+
24
+ const History: React.FC = () => {
25
+ const [accounts, setAccounts] = useState<AccountInfo[]>([]);
26
+ const [loading, setLoading] = useState(false);
27
+ const [visible, setVisible] = useState(false);
28
+ const [currentAccount, setCurrentAccount] = useState<AccountInfo | null>(null);
29
+ const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
30
+ const [batchDeleteVisible, setBatchDeleteVisible] = useState(false);
31
+ const [batchDeleteLoading, setBatchDeleteLoading] = useState(false);
32
+
33
+ // 修改 fetchAccounts 函数以调用 API
34
+ const fetchAccounts = async () => {
35
+ setLoading(true);
36
+ try {
37
+ const response = await apiFetchAccounts(); // Call the imported API function
38
+ if (response.data && response.data.status === 'success') {
39
+ // Map the response to ensure consistency, though AccountInfo is now optional
40
+ const fetchedAccounts = response.data.accounts.map((acc: any) => ({
41
+ ...acc,
42
+ name: acc.name || acc.filename, // Use filename as name if name is missing
43
+ }));
44
+ setAccounts(fetchedAccounts);
45
+ // 清空选择
46
+ setSelectedRowKeys([]);
47
+ } else {
48
+ message.error(response.data.message || '获取账号列表失败');
49
+ }
50
+ } catch (error: any) {
51
+ console.error('获取账号错误:', error);
52
+ message.error(`获取账号列表失败: ${error.message || '未知错误'}`);
53
+ }
54
+ setLoading(false);
55
+ };
56
+
57
+ useEffect(() => {
58
+ fetchAccounts();
59
+ }, []);
60
+
61
+ const handleDelete = async (filename: string) => {
62
+ setLoading(true);
63
+ try {
64
+ // 调用删除账号API
65
+ const response = await deleteAccount(filename);
66
+
67
+ if (response.data && response.data.status === 'success') {
68
+ // 从状态中移除账号
69
+ setAccounts(prevAccounts => prevAccounts.filter(acc => acc.filename !== filename));
70
+ message.success(response.data.message || '账号已成功删除');
71
+ } else {
72
+ // 显示API返回的错误消息
73
+ message.error(response.data.message || '删除账号失败');
74
+ }
75
+ } catch (error: any) {
76
+ console.error('删除账号错误:', error);
77
+ // 显示捕获到的错误消息
78
+ message.error(`删除账号出错: ${error.message || '未知错误'}`);
79
+ } finally {
80
+ // 确保 loading 状态在所有情况下都设置为 false
81
+ setLoading(false);
82
+ }
83
+ };
84
+
85
+ // 批量删除账号
86
+ const handleBatchDelete = async () => {
87
+ if (selectedRowKeys.length === 0) {
88
+ message.warning('请至少选择一个账号');
89
+ return;
90
+ }
91
+
92
+ setBatchDeleteLoading(true);
93
+ try {
94
+ // 从选中的键中提取文件名
95
+ const filenames = selectedRowKeys.map(key => key.toString());
96
+
97
+ // 调用批量删除API
98
+ const response = await deleteAccounts(filenames);
99
+
100
+ if (response.data && (response.data.status === 'success' || response.data.status === 'partial')) {
101
+ // 从状态中移除成功删除的账号
102
+ if (response.data.results && response.data.results.success) {
103
+ const successFilenames = response.data.results.success;
104
+ setAccounts(prevAccounts =>
105
+ prevAccounts.filter(acc => !successFilenames.includes(acc.filename))
106
+ );
107
+ }
108
+
109
+ // 显示成功消息
110
+ message.success(response.data.message || '账号已成功删除');
111
+
112
+ // 清空选择
113
+ setSelectedRowKeys([]);
114
+ } else {
115
+ // 显示API返回的错误消息
116
+ message.error(response.data.message || '批量删除账号失败');
117
+ }
118
+ } catch (error: any) {
119
+ console.error('批量删除账号错误:', error);
120
+ message.error(`批量删除账号出错: ${error.message || '未知错误'}`);
121
+ } finally {
122
+ setBatchDeleteLoading(false);
123
+ setBatchDeleteVisible(false); // 关闭确认对话框
124
+ }
125
+ };
126
+
127
+ const showAccountDetails = (account: AccountInfo) => {
128
+ setCurrentAccount(account);
129
+ setVisible(true);
130
+ };
131
+
132
+ // 表格行选择配置
133
+ const rowSelection = {
134
+ selectedRowKeys,
135
+ onChange: (newSelectedRowKeys: React.Key[]) => {
136
+ setSelectedRowKeys(newSelectedRowKeys);
137
+ }
138
+ };
139
+
140
+ const columns = [
141
+ {
142
+ title: '名称',
143
+ dataIndex: 'name',
144
+ key: 'name',
145
+ },
146
+ {
147
+ title: '邮箱',
148
+ dataIndex: 'email',
149
+ key: 'email',
150
+ },
151
+ {
152
+ title: '状态',
153
+ key: 'status',
154
+ render: (_: any, record: AccountInfo) => {
155
+ if (record.access_token) {
156
+ return <Tag color="green">已激活</Tag>;
157
+ } else if (record.email) { // Check if email exists as an indicator of more complete info
158
+ return <Tag color="orange">未激活</Tag>;
159
+ } else {
160
+ return <Tag color="default">信息不完整</Tag>; // Indicate incomplete info
161
+ }
162
+ },
163
+ },
164
+ {
165
+ title: '邀请码',
166
+ dataIndex: 'invite_code',
167
+ key: 'invite_code',
168
+ render: (invite_code?: string) => invite_code || '-',
169
+ },
170
+ {
171
+ title: '修改日期',
172
+ dataIndex: 'timestamp',
173
+ key: 'timestamp',
174
+ render: (timestamp: number) => {
175
+ // 这里需要类型转换
176
+ return (new Date(timestamp*1)).toLocaleString();
177
+ },
178
+ },
179
+ {
180
+ title: '操作',
181
+ key: 'action',
182
+ render: (_: any, record: AccountInfo) => {
183
+ const isIncomplete = !record.email; // Consider incomplete if email is missing
184
+ return (
185
+ <Space size="middle">
186
+ <Button
187
+ type="text"
188
+ icon={<InfoCircleOutlined />}
189
+ onClick={() => showAccountDetails(record)}
190
+ disabled={isIncomplete} // Disable if incomplete
191
+ >
192
+ 详情
193
+ </Button>
194
+ <Popconfirm
195
+ title="确定要删除此账号吗?"
196
+ onConfirm={() => handleDelete(record.filename)}
197
+ okText="确定"
198
+ cancelText="取消"
199
+ >
200
+ <Button type="text" danger icon={<DeleteOutlined />}>
201
+ 删除
202
+ </Button>
203
+ </Popconfirm>
204
+ </Space>
205
+ );
206
+ },
207
+ },
208
+ ];
209
+
210
+ return (
211
+ <div className="history-container">
212
+ <Card
213
+ title="PikPak 历史账号"
214
+ className="history-card"
215
+ extra={
216
+ <Space>
217
+ {selectedRowKeys.length > 0 && (
218
+ <Button
219
+ danger
220
+ icon={<DeleteOutlined />}
221
+ onClick={() => setBatchDeleteVisible(true)}
222
+ >
223
+ 批量删除 ({selectedRowKeys.length})
224
+ </Button>
225
+ )}
226
+ <Button
227
+ type="primary"
228
+ icon={<ReloadOutlined />}
229
+ onClick={fetchAccounts}
230
+ loading={loading}
231
+ >
232
+ 刷新
233
+ </Button>
234
+ </Space>
235
+ }
236
+ >
237
+ <Table
238
+ rowSelection={rowSelection}
239
+ columns={columns}
240
+ dataSource={accounts}
241
+ rowKey="filename"
242
+ loading={loading}
243
+ pagination={{ pageSize: 10 }}
244
+ />
245
+ </Card>
246
+
247
+ {/* 账号详情模态框 */}
248
+ <Modal
249
+ title="账号详情"
250
+ open={visible}
251
+ onCancel={() => setVisible(false)}
252
+ footer={[
253
+ <Button key="close" onClick={() => setVisible(false)}>
254
+ 关闭
255
+ </Button>
256
+ ]}
257
+ width={700}
258
+ >
259
+ {currentAccount && (
260
+ <div className="account-details">
261
+ <Paragraph>
262
+ <Text strong>名称:</Text> {currentAccount.name || '未提供'}
263
+ </Paragraph>
264
+ <Paragraph>
265
+ <Text strong>邮箱:</Text> {currentAccount.email || '未提供'}
266
+ </Paragraph>
267
+ <Paragraph>
268
+ <Text strong>密码:</Text> {currentAccount.password || '未提供'}
269
+ </Paragraph>
270
+ <Paragraph>
271
+ <Text strong>用户ID:</Text> {currentAccount.user_id || '未提供'}
272
+ </Paragraph>
273
+ <Paragraph>
274
+ <Text strong>设备ID:</Text> {currentAccount.device_id || '未提供'}
275
+ </Paragraph>
276
+ <Paragraph>
277
+ <Text strong>版本:</Text> {currentAccount.version || '未提供'}
278
+ </Paragraph>
279
+ <Paragraph>
280
+ <Text strong>Access Token:</Text>
281
+ <div className="token-container">
282
+ {currentAccount.access_token || '无'}
283
+ </div>
284
+ </Paragraph>
285
+ <Paragraph>
286
+ <Text strong>Refresh Token:</Text>
287
+ <div className="token-container">
288
+ {currentAccount.refresh_token || '无'}
289
+ </div>
290
+ </Paragraph>
291
+ <Paragraph>
292
+ <Text strong>邀请码:</Text> {currentAccount.invite_code || '未提供'}
293
+ </Paragraph>
294
+ <Paragraph>
295
+ <Text strong>文件名:</Text> {currentAccount.filename}
296
+ </Paragraph>
297
+ </div>
298
+ )}
299
+ </Modal>
300
+
301
+ {/* 批量删除确认对话框 */}
302
+ <Modal
303
+ title="确认批量删除"
304
+ open={batchDeleteVisible}
305
+ onCancel={() => setBatchDeleteVisible(false)}
306
+ footer={[
307
+ <Button key="cancel" onClick={() => setBatchDeleteVisible(false)}>
308
+ 取消
309
+ </Button>,
310
+ <Button
311
+ key="delete"
312
+ type="primary"
313
+ danger
314
+ loading={batchDeleteLoading}
315
+ onClick={handleBatchDelete}
316
+ >
317
+ 删除
318
+ </Button>
319
+ ]}
320
+ >
321
+ <p>确定要删除选中的 {selectedRowKeys.length} 个账号吗?此操作不可撤销。</p>
322
+ </Modal>
323
+ </div>
324
+ );
325
+ };
326
+
327
+ export default History;
frontend/src/pages/Register/index.css ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .register-container {
2
+ display: flex;
3
+ justify-content: space-between;
4
+ align-items: flex-start;
5
+ padding: 10px 20px 10px 10px;
6
+ gap: 20px;
7
+ height: calc(100vh - 50px);
8
+ background-color: #f0f2f5;
9
+
10
+ @media screen and (max-width: 1024px) {
11
+ flex-direction: column;
12
+ height: auto;
13
+ }
14
+ }
15
+
16
+ .register-card {
17
+ position: relative;
18
+ width: 100%;
19
+ max-width: 1000px;
20
+ height: 100%;
21
+ }
22
+
23
+ .register-right {
24
+ position: relative;
25
+ width: 100%;
26
+ max-width: 1000px;
27
+ height: calc(100vh - 70px);
28
+ display: grid;
29
+ grid-template-rows: 49.6% 49.6%;
30
+ gap: 5px;
31
+ }
32
+
33
+ .register-right .register-card {
34
+ height: 100%;
35
+ }
36
+
37
+ .register-card .ant-card-body > .steps-content {
38
+ margin-top: 24px;
39
+ }
40
+
41
+ .steps-action {
42
+ margin-top: 24px;
43
+ text-align: right;
44
+ position: absolute;
45
+ bottom: 10px;
46
+ right: 10px;
47
+ }
48
+
49
+ #captcha-container {
50
+ margin: 24px 0;
51
+ min-height: 150px;
52
+ border: 1px solid #eee;
53
+ padding: 12px;
54
+ display: flex;
55
+ justify-content: center;
56
+ align-items: center;
57
+ }
58
+
59
+ .right-panel-list {
60
+ max-height: 400px;
61
+ overflow-y: auto;
62
+ }
63
+
64
+ .right-panel-list-item {
65
+ margin-bottom: 10px;
66
+ padding: 8px;
67
+ border: 1px solid #eee;
68
+ border-radius: 4px;
69
+ transition: background-color 0.3s ease;
70
+ }
71
+
72
+ .right-panel-list-item.processing {
73
+ background-color: #e6f7ff;
74
+ }
75
+
76
+ .right-panel-list-item-status {
77
+ margin-top: 5px;
78
+ }
79
+
80
+ .right-panel-list-item-message {
81
+ margin-top: 5px;
82
+ font-size: 12px;
83
+ word-break: break-all;
84
+ }
85
+
86
+ .right-panel-list-item-message.error {
87
+ color: #ff4d4f;
88
+ }
89
+
90
+ .right-panel-list-item-message.success {
91
+ color: #52c41a;
92
+ }
93
+
94
+ pre {
95
+ background-color: #fafafa;
96
+ padding: 10px;
97
+ border-radius: 4px;
98
+ white-space: pre-wrap;
99
+ word-break: break-all;
100
+ }
101
+
102
+ .step-content-container {
103
+ --max-height: calc(100vh - 80px);
104
+ max-height: calc(var(--max-height) / 2);
105
+ overflow-y: auto;
106
+ }
frontend/src/pages/Register/index.tsx ADDED
@@ -0,0 +1,1586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react";
2
+ import {
3
+ Form,
4
+ Input,
5
+ Button,
6
+ Card,
7
+ message,
8
+ Switch,
9
+ Row,
10
+ Col,
11
+ Tag,
12
+ Spin,
13
+ Checkbox,
14
+ Progress,
15
+ } from "antd";
16
+ import { CheckboxChangeEvent } from "antd/es/checkbox";
17
+ import "./index.css";
18
+ import {
19
+ testProxy,
20
+ initialize,
21
+ verifyCaptha,
22
+ getEmailVerificationCode,
23
+ register,
24
+ } from "../../services/api";
25
+ import {
26
+ CheckCircleOutlined,
27
+ CloseCircleOutlined,
28
+ SyncOutlined,
29
+ SafetyCertificateOutlined,
30
+ MailOutlined,
31
+ } from "@ant-design/icons";
32
+
33
+ const { TextArea } = Input;
34
+
35
+ // 定义账号信息接口
36
+ interface AccountInfo {
37
+ id: number;
38
+ account: string;
39
+ password: string;
40
+ clientId: string;
41
+ token: string;
42
+ status:
43
+ | "pending"
44
+ | "processing"
45
+ | "initializing"
46
+ | "captcha_pending"
47
+ | "email_pending"
48
+ | "success"
49
+ | "error";
50
+ message?: string;
51
+ }
52
+
53
+ const Register: React.FC = () => {
54
+ const [current, setCurrent] = useState(0);
55
+ const [form] = Form.useForm();
56
+ const [useProxy, setUseProxy] = useState(false);
57
+ const [loading, setLoading] = useState(false); // Represents the overall batch processing state
58
+ const [accountList, setAccountList] = useState<AccountInfo[]>([]);
59
+ const [processingIndex, setProcessingIndex] = useState<number>(-1); // -1 indicates not started, >= 0 is the index being processed
60
+ const [testingProxy, setTestingProxy] = useState(false);
61
+ const [proxyTestResult, setProxyTestResult] = useState<
62
+ "idle" | "success" | "error"
63
+ >("idle");
64
+ const [isCaptchaVerified, setIsCaptchaVerified] = useState(false);
65
+ const [captchaLoading, setCaptchaLoading] = useState(false);
66
+ const [emailVerifyLoading, setEmailVerifyLoading] = useState(false); // Loading state for email verification step
67
+ const [allAccountsProcessed, setAllAccountsProcessed] = useState(false); // Checklist item 1: Add state
68
+ const [autoFetchLoading, setAutoFetchLoading] = useState(false); // 新增状态
69
+ const [saveInviteCode, setSaveInviteCode] = useState(false); // Added state for checkbox
70
+
71
+ // 添加错误跟踪和重试状态
72
+ const [captchaError, setCaptchaError] = useState<string | null>(null);
73
+ const [emailVerificationError, setEmailVerificationError] = useState<string | null>(null);
74
+ const [registrationError, setRegistrationError] = useState<string | null>(null);
75
+
76
+ // 添加重置表单函数
77
+ const resetForm = () => {
78
+ // 保留邀请码
79
+ const savedInviteCode = form.getFieldValue("invite_code");
80
+
81
+ // 重置表单
82
+ form.resetFields(["accountInfo", "verification_code"]);
83
+ if (savedInviteCode && saveInviteCode) {
84
+ form.setFieldValue("invite_code", savedInviteCode);
85
+ }
86
+
87
+ // 重置状态
88
+ setLoading(false);
89
+ setAccountList([]);
90
+ setProcessingIndex(-1);
91
+ setCurrent(0);
92
+ setIsCaptchaVerified(false);
93
+ setCaptchaLoading(false);
94
+ setEmailVerifyLoading(false);
95
+ setAllAccountsProcessed(false);
96
+ setAutoFetchLoading(false);
97
+
98
+ // 重置错误状态
99
+ setCaptchaError(null);
100
+ setEmailVerificationError(null);
101
+ setRegistrationError(null);
102
+
103
+ message.success("已重置,可以开始新一轮注册");
104
+ };
105
+
106
+ // Load saved invite code on mount
107
+ useEffect(() => {
108
+ try {
109
+ const savedCode = localStorage.getItem("savedInviteCode");
110
+ if (savedCode) {
111
+ form.setFieldsValue({ invite_code: savedCode });
112
+ setSaveInviteCode(true);
113
+ }
114
+ } catch (error) {
115
+ console.error("无法访问 localStorage:", error);
116
+ // Don't block rendering, just log error
117
+ }
118
+ }, [form]); // Run once on mount, depends on form instance
119
+
120
+ // 初始化重试的专门函数
121
+ const handleRetryInitialization = () => {
122
+ if (processingIndex >= 0 && processingIndex < accountList.length) {
123
+ handleInitializeCurrentAccount(processingIndex);
124
+ }
125
+ };
126
+
127
+ // 更新移动到下一账号或完成的函数,重置错误状态
128
+ const moveToNextAccountOrComplete = () => {
129
+ // 重置所有错误状态
130
+ setCaptchaError(null);
131
+ setEmailVerificationError(null);
132
+ setRegistrationError(null);
133
+
134
+ const nextIndex = processingIndex + 1;
135
+ if (nextIndex >= accountList.length) {
136
+ setAllAccountsProcessed(true);
137
+ setLoading(false);
138
+ message.success("所有账号均已处理完毕!");
139
+ } else {
140
+ setAllAccountsProcessed(false);
141
+ setAccountList((prev) =>
142
+ prev.map((acc) =>
143
+ acc.id === nextIndex ? { ...acc, status: "pending" } : acc
144
+ )
145
+ );
146
+ handleStartNextAccount(nextIndex);
147
+ }
148
+ setCurrent(3);
149
+ };
150
+
151
+ const handleStartNextAccount = (nextIndex: number) => {
152
+ if (nextIndex >= 0 && nextIndex < accountList.length) {
153
+ // Reset states for the next account's steps
154
+ setProcessingIndex(nextIndex);
155
+ setCurrent(0);
156
+ setIsCaptchaVerified(false);
157
+ setCaptchaLoading(false);
158
+ setEmailVerifyLoading(false);
159
+ form.setFieldsValue({ verification_code: "" }); // Clear previous code
160
+ } else {
161
+ message.info("所有账号均已处理。");
162
+ }
163
+ };
164
+
165
+ const handleInitializeCurrentAccount = async (index: number) => {
166
+ if (index < 0 || index >= accountList.length) {
167
+ console.warn(
168
+ "handleInitializeCurrentAccount called with invalid index or empty list",
169
+ index,
170
+ accountList.length
171
+ );
172
+ return;
173
+ }
174
+
175
+ const account = accountList[index];
176
+ const inviteCode = form.getFieldValue("invite_code");
177
+ const proxyUrl = form.getFieldValue("use_proxy")
178
+ ? form.getFieldValue("proxy_url")
179
+ : undefined;
180
+
181
+ setAccountList((prev) =>
182
+ prev.map((acc) =>
183
+ acc.id === account.id
184
+ ? { ...acc, status: "initializing", message: "开始初始化..." }
185
+ : acc
186
+ )
187
+ );
188
+
189
+ try {
190
+ const dataToSend: any = {
191
+ invite_code: inviteCode,
192
+ email: account.account,
193
+ use_proxy: !!proxyUrl,
194
+ };
195
+ if (proxyUrl) {
196
+ dataToSend.proxy_url = proxyUrl;
197
+ }
198
+
199
+ let formData = new FormData();
200
+ for (const key in dataToSend) {
201
+ formData.append(key, dataToSend[key]);
202
+ }
203
+
204
+ const response = await initialize(formData);
205
+ const responseData = response.data;
206
+ console.log(
207
+ `[${account.account}] Initialize API response:`,
208
+ responseData
209
+ );
210
+
211
+ if (responseData.status === "success") {
212
+ setAccountList((prev) =>
213
+ prev.map((acc) =>
214
+ acc.id === account.id
215
+ ? {
216
+ ...acc,
217
+ status: "captcha_pending",
218
+ message: responseData.message || "初始化成功, 等待验证码",
219
+ }
220
+ : acc
221
+ )
222
+ );
223
+ setCurrent(1); // Move to captcha step for this account
224
+
225
+ // 直接调用过滑块验证
226
+ setTimeout(() => {
227
+ handleCaptchaVerification();
228
+ }, 1000);
229
+ } else {
230
+ throw new Error(responseData.message || "初始化返回失败状态");
231
+ }
232
+ } catch (error: any) {
233
+ console.error(`[${account.account}] 初始化失败:`, error);
234
+ const errorMessage = error.message || "未知错误";
235
+ // Update status first
236
+ setAccountList((prev) =>
237
+ prev.map((acc) =>
238
+ acc.id === account.id
239
+ ? {
240
+ ...acc,
241
+ status: "error",
242
+ message: `初始化失败: ${errorMessage}`,
243
+ }
244
+ : acc
245
+ )
246
+ );
247
+ // 不再自动跳到下一个账号,让用户选择是重试还是跳过
248
+ // moveToNextAccountOrComplete(); // 移除这行代码
249
+ }
250
+ };
251
+
252
+ const startProcessing = async () => {
253
+ try {
254
+ const values = await form.validateFields([
255
+ "invite_code",
256
+ "accountInfo",
257
+ "use_proxy",
258
+ "proxy_url",
259
+ ]); // Validate needed fields
260
+ setLoading(true);
261
+ setAccountList([]);
262
+ setProcessingIndex(-1);
263
+ setCurrent(0);
264
+ setIsCaptchaVerified(false);
265
+ setCaptchaLoading(false);
266
+
267
+ const lines = values.accountInfo
268
+ .split("\n")
269
+ .filter((line: string) => line.trim() !== "");
270
+ const parsedAccounts: AccountInfo[] = [];
271
+ let formatError = false;
272
+ for (let i = 0; i < lines.length; i++) {
273
+ const line = lines[i];
274
+ const parts = line.split("----");
275
+ if (
276
+ parts.length < 4 ||
277
+ parts.some((part: string) => part.trim() === "")
278
+ ) {
279
+ message.error(`第 ${i + 1} 行格式错误或包含空字段,请检查!`);
280
+ formatError = true;
281
+ break; // Stop parsing on first error
282
+ }
283
+ parsedAccounts.push({
284
+ id: i,
285
+ account: parts[0].trim(),
286
+ password: parts[1].trim(),
287
+ clientId: parts[2].trim(),
288
+ token: parts[3].trim(),
289
+ status: "pending",
290
+ });
291
+ }
292
+
293
+ if (formatError) {
294
+ setLoading(false);
295
+ return;
296
+ }
297
+
298
+ if (parsedAccounts.length === 0) {
299
+ message.warning("请输入至少一条有效的账号信息。");
300
+ setLoading(false);
301
+ return;
302
+ }
303
+ // console.log(parsedAccounts, "----------------"); // Keep user's log if desired
304
+ setAccountList(parsedAccounts);
305
+ setProcessingIndex(0); // Start with the first account
306
+ setLoading(true); // Ensure loading is true when processing starts
307
+ setAllAccountsProcessed(false); // Reset completion flag
308
+ setCurrent(0); // Ensure starting at step 0
309
+ setIsCaptchaVerified(false); // Reset step-specific states
310
+ setCaptchaLoading(false);
311
+ setEmailVerifyLoading(false);
312
+ } catch (error) {
313
+ console.error("表单验证失败或解析错误:", error);
314
+ setLoading(false);
315
+ // Validation errors are handled by Antd form
316
+ }
317
+ };
318
+
319
+ const handleCaptchaVerification = async () => {
320
+ if (processingIndex < 0 || processingIndex >= accountList.length) return;
321
+ const currentAccount = accountList[processingIndex];
322
+
323
+ setCaptchaLoading(true);
324
+ setIsCaptchaVerified(false);
325
+ setCaptchaError(null); // 重置验证码错误状态
326
+
327
+ try {
328
+ console.log(`[${currentAccount.account}] 调用 verifyCaptha API...`);
329
+ let formData = new FormData();
330
+ formData.append("email", currentAccount.account); // Pass current email
331
+ const response = await verifyCaptha(formData);
332
+ const responseData = response.data;
333
+ console.log(
334
+ `[${currentAccount.account}] verifyCaptha API response:`,
335
+ responseData
336
+ );
337
+
338
+ if (responseData.status === "success") {
339
+ message.success(responseData.message || "验证成功!");
340
+ setIsCaptchaVerified(true); // Set verification success
341
+ // 可以点击下一步
342
+ setAccountList((prev) =>
343
+ prev.map((acc) =>
344
+ acc.id === currentAccount.id
345
+ ? {
346
+ ...acc,
347
+ status: "email_pending",
348
+ message: "验证码成功, 等待邮箱验证",
349
+ }
350
+ : acc
351
+ )
352
+ );
353
+ // 验证成功时自动移至下一步
354
+ setCurrent(2);
355
+ // DO NOT call next() here, user clicks the button
356
+ // 获取验证码
357
+ setTimeout(() => {
358
+ handleAutoFetchCode();
359
+ }, 200);
360
+ } else {
361
+ const errorMessage = responseData.message || "验证失败,请确保已完成滑块验证后重试";
362
+ message.error(errorMessage);
363
+ setIsCaptchaVerified(false);
364
+ setCaptchaError(errorMessage); // 设置错误信息
365
+
366
+ // 更新账号状态为错误,但保持在当前步骤
367
+ setAccountList((prev) =>
368
+ prev.map((acc) =>
369
+ acc.id === currentAccount.id
370
+ ? {
371
+ ...acc,
372
+ message: `滑块验证失败: ${errorMessage}`
373
+ }
374
+ : acc
375
+ )
376
+ );
377
+ }
378
+ } catch (error: any) {
379
+ // Added type annotation
380
+ console.error(`[${currentAccount.account}] 验证码 API 调用失败:`, error);
381
+ const errorMessage = error.message || "未知错误";
382
+ message.error(`验证码验证出错: ${errorMessage}`);
383
+ setIsCaptchaVerified(false);
384
+ setCaptchaError(errorMessage); // 设置错误信息
385
+
386
+ // 更新账号状态为错误,但保持在当前步骤
387
+ setAccountList((prev) =>
388
+ prev.map((acc) =>
389
+ acc.id === currentAccount.id
390
+ ? {
391
+ ...acc,
392
+ message: `滑块验证出错: ${errorMessage}`
393
+ }
394
+ : acc
395
+ )
396
+ );
397
+ } finally {
398
+ setCaptchaLoading(false);
399
+ // 移除这里的setCurrent(2),让步骤只在验证成功时前进
400
+ // setCurrent(2);
401
+ }
402
+ };
403
+
404
+ // 添加滑块验证重试功能
405
+ const handleRetryCaptcha = () => {
406
+ setCaptchaError(null);
407
+ handleCaptchaVerification();
408
+ };
409
+
410
+ const handleEmailVerification = async () => {
411
+ if (processingIndex < 0 || processingIndex >= accountList.length) return;
412
+ const currentAccount = accountList[processingIndex];
413
+ const verificationCode = form.getFieldValue("verification_code"); // Get code from form
414
+
415
+ if (!verificationCode) {
416
+ message.error("请输入邮箱验证码!");
417
+ return;
418
+ }
419
+
420
+ setEmailVerifyLoading(true);
421
+ setRegistrationError(null); // 重置注册错误状态
422
+ setAccountList((prev) =>
423
+ prev.map((acc) =>
424
+ acc.id === currentAccount.id
425
+ ? { ...acc, message: "正在提交注册信息..." }
426
+ : acc
427
+ )
428
+ ); // 更新提示信息
429
+
430
+ try {
431
+ console.log(`[${currentAccount.account}] 调用注册 API...`, {
432
+ code: verificationCode,
433
+ });
434
+
435
+ // --- 替换模拟代码为实际 API 调用 ---
436
+ // 准备数据
437
+ const formData = new FormData();
438
+ formData.append("email", currentAccount.account);
439
+ formData.append("verification_code", verificationCode);
440
+
441
+ const response = await register(formData);
442
+ const responseData = response.data;
443
+ console.log(
444
+ `[${currentAccount.account}] Register API response:`,
445
+ responseData
446
+ );
447
+
448
+ // 处理响应
449
+ if (responseData.status === "success") {
450
+ // 成功情况
451
+ console.log(`[${currentAccount.account}] 注册成功`);
452
+ message.success(responseData.message || "注册成功!");
453
+ // 更新状态和消息
454
+ setAccountList((prev) =>
455
+ prev.map((acc) =>
456
+ acc.id === currentAccount.id
457
+ ? {
458
+ ...acc,
459
+ status: "success",
460
+ message: responseData.message || "注册成功",
461
+ // 可选: 存储 responseData.account_info
462
+ }
463
+ : acc
464
+ )
465
+ );
466
+ // 移动到下一步或完成
467
+ moveToNextAccountOrComplete();
468
+ setLoading(false);
469
+ } else {
470
+ // 失败情况 (后端返回非成功状态)
471
+ const errorMessage = responseData.message || "注册返回失败状态";
472
+ throw new Error(errorMessage);
473
+ }
474
+ // --- 结束替换 ---
475
+ } catch (error: any) {
476
+ console.error(`[${currentAccount.account}] 注册失败:`, error);
477
+ const errorMessage = error.message || "未知错误";
478
+ message.error(`注册失败: ${errorMessage}`);
479
+ setRegistrationError(errorMessage); // 设置注册错误状态
480
+
481
+ // 更新状态和消息,但不移动到下一个账号
482
+ setAccountList((prev) =>
483
+ prev.map((acc) =>
484
+ acc.id === currentAccount.id
485
+ ? { ...acc, message: `注册失败: ${errorMessage}` }
486
+ : acc
487
+ )
488
+ );
489
+ } finally {
490
+ setEmailVerifyLoading(false);
491
+ }
492
+ };
493
+
494
+ // 添加注册重试功能
495
+ const handleRetryRegistration = () => {
496
+ setRegistrationError(null);
497
+ handleEmailVerification();
498
+ };
499
+
500
+ const getCurrentAccount = () => {
501
+ if (processingIndex >= 0 && processingIndex < accountList.length) {
502
+ return accountList[processingIndex];
503
+ }
504
+ return null;
505
+ };
506
+
507
+ const handleTestProxy = async () => {
508
+ setProxyTestResult("idle");
509
+ const proxyUrl = form.getFieldValue("proxy_url");
510
+ if (!proxyUrl || !proxyUrl.trim()) {
511
+ message.error("请输入代理地址再进行测试!");
512
+ return;
513
+ }
514
+ if (!proxyUrl.startsWith("http://") && !proxyUrl.startsWith("https://")) {
515
+ message.warning(
516
+ "代理地址格式似乎不正确,请检查 (应以 http:// 或 https:// 开头)"
517
+ );
518
+ }
519
+
520
+ setTestingProxy(true);
521
+ const formData = new FormData();
522
+ formData.append("proxy_url", proxyUrl);
523
+
524
+ try {
525
+ const response = await testProxy(formData);
526
+ const responseData = response.data;
527
+ if (responseData.status === "success") {
528
+ setProxyTestResult("success");
529
+ } else {
530
+ setProxyTestResult("error");
531
+ }
532
+ } catch (error: any) {
533
+ console.error("测试代理失败:", error);
534
+ setProxyTestResult("error");
535
+ } finally {
536
+ setTestingProxy(false);
537
+ }
538
+ };
539
+
540
+ useEffect(() => {
541
+ if (
542
+ accountList.length > 0 &&
543
+ processingIndex < accountList.length &&
544
+ accountList[processingIndex].status === "pending"
545
+ ) {
546
+ console.log("useEffect triggering initialization for index 0");
547
+ // Call the initialization function for the first account
548
+ handleInitializeCurrentAccount(processingIndex);
549
+ }
550
+ // Dependencies: run when the index changes or the list is populated
551
+ }, [processingIndex, accountList]);
552
+
553
+ const handleAutoFetchCode = async () => {
554
+ const currentAccount = getCurrentAccount();
555
+ setAutoFetchLoading(true);
556
+ setEmailVerificationError(null); // 重置验证码获取错误状态
557
+
558
+ try {
559
+ const formData = new FormData();
560
+ formData.append("email", currentAccount?.account || "");
561
+ formData.append("password", currentAccount?.password || "");
562
+ formData.append("token", currentAccount?.token || "");
563
+ formData.append("client_id", currentAccount?.clientId || "");
564
+
565
+ const response = await getEmailVerificationCode(formData);
566
+
567
+ const result = response.data;
568
+
569
+ if (result.status === "success" && result.verification_code) {
570
+ form.setFieldValue("verification_code", result.verification_code);
571
+ message.success(result.msg || "验证码已自动填入");
572
+ console.log("收到验证码:", result.verification_code);
573
+
574
+ // 验证邮箱
575
+ setTimeout(() => {
576
+ handleEmailVerification();
577
+ }, 1000);
578
+ } else {
579
+ // 显示后端返回的更具体的错误信息
580
+ const errorMessage = result.msg || "未能获取验证码,请检查邮箱和密码或手动输入";
581
+ message.error(errorMessage);
582
+ setEmailVerificationError(errorMessage); // 设置错误信息
583
+
584
+ // 更新账号状态显示错误
585
+ if (currentAccount) {
586
+ setAccountList((prev) =>
587
+ prev.map((acc) =>
588
+ acc.id === currentAccount.id
589
+ ? {
590
+ ...acc,
591
+ message: `获取验证码失败: ${errorMessage}`
592
+ }
593
+ : acc
594
+ )
595
+ );
596
+ }
597
+ }
598
+ } catch (error: any) {
599
+ const errorMessage = error.message || "未知错误";
600
+ message.error(`获取验证码时出错: ${errorMessage}`);
601
+ setEmailVerificationError(errorMessage); // 设置错误信息
602
+ console.error("Auto fetch code error:", error);
603
+
604
+ // 更新账号状态显示错误
605
+ if (currentAccount) {
606
+ setAccountList((prev) =>
607
+ prev.map((acc) =>
608
+ acc.id === currentAccount.id
609
+ ? {
610
+ ...acc,
611
+ message: `获取验证码出错: ${errorMessage}`
612
+ }
613
+ : acc
614
+ )
615
+ );
616
+ }
617
+ } finally {
618
+ setAutoFetchLoading(false);
619
+ }
620
+ };
621
+
622
+ // 添加获取验证码重试功能
623
+ const handleRetryEmailVerification = () => {
624
+ setEmailVerificationError(null);
625
+ handleAutoFetchCode();
626
+ };
627
+
628
+ const handleSaveInviteCodeChange = (e: CheckboxChangeEvent) => {
629
+ const isChecked = e.target.checked;
630
+ setSaveInviteCode(isChecked);
631
+ const currentCode = form.getFieldValue("invite_code");
632
+ try {
633
+ if (isChecked && currentCode) {
634
+ localStorage.setItem("savedInviteCode", currentCode);
635
+ } else {
636
+ localStorage.removeItem("savedInviteCode");
637
+ }
638
+ } catch (error) {
639
+ console.error("无法访问 localStorage:", error);
640
+ message.error("无法保存邀请码设置,存储不可用。");
641
+ }
642
+ };
643
+
644
+ const handleInviteCodeInputChange = (
645
+ e: React.ChangeEvent<HTMLInputElement>
646
+ ) => {
647
+ const newCode = e.target.value;
648
+ if (saveInviteCode) {
649
+ try {
650
+ if (newCode) {
651
+ localStorage.setItem("savedInviteCode", newCode);
652
+ } else {
653
+ // If code is cleared while save is checked, remove from storage
654
+ localStorage.removeItem("savedInviteCode");
655
+ }
656
+ } catch (error) {
657
+ console.error("无法访问 localStorage:", error);
658
+ // Optionally show message, but might be too noisy on every input change
659
+ }
660
+ }
661
+ };
662
+
663
+ const steps = [
664
+ {
665
+ title: "初始化",
666
+ content: (() => {
667
+ const currentAccount = getCurrentAccount();
668
+ const status = currentAccount?.status || "pending";
669
+
670
+ // 根据状态决定不同的初始化步骤样式
671
+ if (status === "pending") {
672
+ return (
673
+ <div
674
+ className="step-content-container"
675
+ style={{ textAlign: "center", padding: "12px" }}
676
+ >
677
+ <div
678
+ style={{
679
+ fontSize: "42px",
680
+ color: "#8c8c8c",
681
+ marginBottom: "10px",
682
+ }}
683
+ >
684
+ <SafetyCertificateOutlined />
685
+ </div>
686
+ <h3
687
+ style={{
688
+ marginBottom: "10px",
689
+ fontSize: "18px",
690
+ fontWeight: "bold",
691
+ }}
692
+ >
693
+ 等待初始化
694
+ </h3>
695
+ <p style={{ color: "#666", marginBottom: "10px" }}>
696
+ 系统准备就绪,正在等待开始初始化流程
697
+ </p>
698
+ <Progress percent={0} status="normal" />
699
+ </div>
700
+ );
701
+ } else if (status === "initializing") {
702
+ return (
703
+ <div
704
+ className="step-content-container"
705
+ style={{ textAlign: "center", padding: "12px" }}
706
+ >
707
+ <div
708
+ style={{
709
+ fontSize: "42px",
710
+ color: "#1890ff",
711
+ marginBottom: "10px",
712
+ }}
713
+ >
714
+ <SyncOutlined spin />
715
+ </div>
716
+ <h3
717
+ style={{
718
+ marginBottom: "10px",
719
+ fontSize: "18px",
720
+ fontWeight: "bold",
721
+ }}
722
+ >
723
+ 正在初始化
724
+ </h3>
725
+ <p style={{ color: "#666", marginBottom: "10px" }}>
726
+ 系统正在准备您的注册环境,请稍候...
727
+ </p>
728
+ <Progress percent={25} status="active" />
729
+ </div>
730
+ );
731
+ } else if (status === "error") {
732
+ return (
733
+ <div
734
+ className="step-content-container"
735
+ style={{ textAlign: "center", padding: "12px" }}
736
+ >
737
+ <div
738
+ style={{
739
+ fontSize: "42px",
740
+ color: "#ff4d4f",
741
+ marginBottom: "10px",
742
+ }}
743
+ >
744
+ <CloseCircleOutlined />
745
+ </div>
746
+ <h3
747
+ style={{
748
+ marginBottom: "10px",
749
+ fontSize: "18px",
750
+ fontWeight: "bold",
751
+ }}
752
+ >
753
+ 初始化失败
754
+ </h3>
755
+ <p style={{ color: "#666", marginBottom: "10px" }}>
756
+ {currentAccount?.message ||
757
+ "初始化过程中遇到错误,请检查网络或代理设置"}
758
+ </p>
759
+ <Progress percent={25} status="exception" />
760
+ <div style={{ marginTop: "8px" }}>
761
+ <Button
762
+ type="primary"
763
+ danger
764
+ onClick={handleRetryInitialization}
765
+ style={{ marginRight: "8px" }}
766
+ >
767
+ 重试初始化
768
+ </Button>
769
+ <Button
770
+ onClick={moveToNextAccountOrComplete}
771
+ >
772
+ 跳过此账号
773
+ </Button>
774
+ </div>
775
+ </div>
776
+ );
777
+ } else {
778
+ // 其他状态表示已初始化完���
779
+ return (
780
+ <div
781
+ className="step-content-container"
782
+ style={{ textAlign: "center", padding: "12px" }}
783
+ >
784
+ <div
785
+ style={{
786
+ fontSize: "42px",
787
+ color: "#52c41a",
788
+ marginBottom: "10px",
789
+ }}
790
+ >
791
+ <CheckCircleOutlined />
792
+ </div>
793
+ <h3
794
+ style={{
795
+ marginBottom: "10px",
796
+ fontSize: "18px",
797
+ fontWeight: "bold",
798
+ }}
799
+ >
800
+ 初始化完成
801
+ </h3>
802
+ <p style={{ color: "#666", marginBottom: "10px" }}>
803
+ 环境准备就绪,可以继续下一步操作
804
+ </p>
805
+ <Progress percent={100} status="success" />
806
+ </div>
807
+ );
808
+ }
809
+ })(),
810
+ },
811
+ {
812
+ title: "滑块验证",
813
+ content: (() => {
814
+ // 滑块验证步骤的UI
815
+ if (captchaLoading) {
816
+ return (
817
+ <div
818
+ className="step-content-container"
819
+ style={{ textAlign: "center", padding: "12px" }}
820
+ >
821
+ <div
822
+ style={{
823
+ fontSize: "42px",
824
+ color: "#1890ff",
825
+ marginBottom: "10px",
826
+ }}
827
+ >
828
+ <SyncOutlined spin />
829
+ </div>
830
+ <h3
831
+ style={{
832
+ marginBottom: "10px",
833
+ fontSize: "18px",
834
+ fontWeight: "bold",
835
+ }}
836
+ >
837
+ 正在进行滑块验证
838
+ </h3>
839
+ <p style={{ color: "#666", marginBottom: "10px" }}>
840
+ 系统正在自动完成滑块验证,请稍候...
841
+ </p>
842
+ <Progress percent={50} status="active" />
843
+ </div>
844
+ );
845
+ } else if (isCaptchaVerified) {
846
+ return (
847
+ <div
848
+ className="step-content-container"
849
+ style={{ textAlign: "center", padding: "12px" }}
850
+ >
851
+ <div
852
+ style={{
853
+ fontSize: "42px",
854
+ color: "#52c41a",
855
+ marginBottom: "10px",
856
+ }}
857
+ >
858
+ <CheckCircleOutlined />
859
+ </div>
860
+ <h3
861
+ style={{
862
+ marginBottom: "10px",
863
+ fontSize: "18px",
864
+ fontWeight: "bold",
865
+ }}
866
+ >
867
+ 滑块验证成功
868
+ </h3>
869
+ <p style={{ color: "#666", marginBottom: "10px" }}>
870
+ 成功完成滑块验证,验证码已发送至邮箱
871
+ </p>
872
+ <Progress percent={100} status="success" />
873
+ </div>
874
+ );
875
+ } else if (captchaError) {
876
+ // 滑块验证失败时显示错误和重试按钮
877
+ return (
878
+ <div
879
+ className="step-content-container"
880
+ style={{ textAlign: "center", padding: "12px" }}
881
+ >
882
+ <div
883
+ style={{
884
+ fontSize: "42px",
885
+ color: "#ff4d4f",
886
+ marginBottom: "10px",
887
+ }}
888
+ >
889
+ <CloseCircleOutlined />
890
+ </div>
891
+ <h3
892
+ style={{
893
+ marginBottom: "10px",
894
+ fontSize: "18px",
895
+ fontWeight: "bold",
896
+ }}
897
+ >
898
+ 滑块验证失败
899
+ </h3>
900
+ <p style={{ color: "#666", marginBottom: "10px" }}>
901
+ {captchaError}
902
+ </p>
903
+ <Progress percent={50} status="exception" />
904
+ <div style={{ marginTop: "8px" }}>
905
+ <Button
906
+ type="primary"
907
+ danger
908
+ onClick={handleRetryCaptcha}
909
+ style={{ marginRight: "8px" }}
910
+ >
911
+ 重试滑块验证
912
+ </Button>
913
+ <Button
914
+ onClick={moveToNextAccountOrComplete}
915
+ >
916
+ 跳过此账号
917
+ </Button>
918
+ </div>
919
+ </div>
920
+ );
921
+ } else {
922
+ return (
923
+ <div
924
+ className="step-content-container"
925
+ style={{ textAlign: "center", padding: "12px" }}
926
+ >
927
+ <div
928
+ style={{
929
+ fontSize: "42px",
930
+ color: "#8c8c8c",
931
+ marginBottom: "10px",
932
+ }}
933
+ >
934
+ <SyncOutlined />
935
+ </div>
936
+ <h3
937
+ style={{
938
+ marginBottom: "10px",
939
+ fontSize: "18px",
940
+ fontWeight: "bold",
941
+ }}
942
+ >
943
+ 等待滑块验证
944
+ </h3>
945
+ <p style={{ color: "#666", marginBottom: "10px" }}>
946
+ 点击下方的"开始验证"按钮进行滑块验证
947
+ </p>
948
+ <Progress percent={40} status="normal" />
949
+ </div>
950
+ );
951
+ }
952
+ })(),
953
+ },
954
+ {
955
+ title: "邮箱验证",
956
+ content: (() => {
957
+ const currentAccount = getCurrentAccount();
958
+
959
+ // 根据不同状态显示不同内容
960
+ if (emailVerifyLoading) {
961
+ return (
962
+ <div
963
+ className="step-content-container"
964
+ style={{ textAlign: "center", padding: "12px" }}
965
+ >
966
+ <div
967
+ style={{
968
+ fontSize: "42px",
969
+ color: "#1890ff",
970
+ marginBottom: "10px",
971
+ }}
972
+ >
973
+ <SyncOutlined spin />
974
+ </div>
975
+ <h3
976
+ style={{
977
+ marginBottom: "10px",
978
+ fontSize: "18px",
979
+ fontWeight: "bold",
980
+ }}
981
+ >
982
+ 正在验证
983
+ </h3>
984
+ <p style={{ color: "#666", marginBottom: "10px" }}>
985
+ 正在验证您的邮箱验证码,请稍候...
986
+ </p>
987
+ <Progress percent={75} status="active" />
988
+ </div>
989
+ );
990
+ } else if (registrationError) {
991
+ // 注册失败时显示
992
+ return (
993
+ <div
994
+ className="step-content-container"
995
+ style={{ textAlign: "center", padding: "12px" }}
996
+ >
997
+ <div
998
+ style={{
999
+ fontSize: "42px",
1000
+ color: "#ff4d4f",
1001
+ marginBottom: "10px",
1002
+ }}
1003
+ >
1004
+ <CloseCircleOutlined />
1005
+ </div>
1006
+ <h3
1007
+ style={{
1008
+ marginBottom: "10px",
1009
+ fontSize: "18px",
1010
+ fontWeight: "bold",
1011
+ }}
1012
+ >
1013
+ 注册失败
1014
+ </h3>
1015
+ <p style={{ color: "#666", marginBottom: "10px" }}>
1016
+ {registrationError}
1017
+ </p>
1018
+ <Form form={form}>
1019
+ <Form.Item
1020
+ name="verification_code"
1021
+ style={{ maxWidth: "300px", margin: "0 auto 10px" }}
1022
+ >
1023
+ <Input placeholder="输入邮箱验证码" />
1024
+ </Form.Item>
1025
+ </Form>
1026
+ <div style={{ marginTop: "8px" }}>
1027
+ <Button
1028
+ type="primary"
1029
+ danger
1030
+ onClick={handleRetryRegistration}
1031
+ style={{ marginRight: "8px" }}
1032
+ >
1033
+ 重试注册
1034
+ </Button>
1035
+ <Button
1036
+ onClick={moveToNextAccountOrComplete}
1037
+ >
1038
+ 跳过此账号
1039
+ </Button>
1040
+ </div>
1041
+ </div>
1042
+ );
1043
+ } else if (emailVerificationError) {
1044
+ // 获取验证码失败时显示
1045
+ return (
1046
+ <div
1047
+ className="step-content-container"
1048
+ style={{ textAlign: "center", padding: "12px" }}
1049
+ >
1050
+ <div
1051
+ style={{
1052
+ fontSize: "42px",
1053
+ color: "#ff4d4f",
1054
+ marginBottom: "10px",
1055
+ }}
1056
+ >
1057
+ <CloseCircleOutlined />
1058
+ </div>
1059
+ <h3
1060
+ style={{
1061
+ marginBottom: "10px",
1062
+ fontSize: "18px",
1063
+ fontWeight: "bold",
1064
+ }}
1065
+ >
1066
+ 获取验证码失败
1067
+ </h3>
1068
+ <p style={{ color: "#666", marginBottom: "10px" }}>
1069
+ {emailVerificationError}
1070
+ </p>
1071
+ <div style={{ marginTop: "8px" }}>
1072
+ <Button
1073
+ type="primary"
1074
+ danger
1075
+ onClick={handleRetryEmailVerification}
1076
+ style={{ marginRight: "8px" }}
1077
+ >
1078
+ 重试获取验证码
1079
+ </Button>
1080
+ <Button
1081
+ onClick={moveToNextAccountOrComplete}
1082
+ >
1083
+ 跳过此账号
1084
+ </Button>
1085
+ </div>
1086
+ </div>
1087
+ );
1088
+ } else {
1089
+ // 默认显示内容
1090
+ return (
1091
+ <div
1092
+ className="step-content-container"
1093
+ style={{ textAlign: "center", padding: "12px" }}
1094
+ >
1095
+ <div
1096
+ style={{
1097
+ fontSize: "42px",
1098
+ color: "#1890ff",
1099
+ marginBottom: "10px",
1100
+ }}
1101
+ >
1102
+ <MailOutlined />
1103
+ </div>
1104
+ <h3
1105
+ style={{
1106
+ marginBottom: "10px",
1107
+ fontSize: "18px",
1108
+ fontWeight: "bold",
1109
+ }}
1110
+ >
1111
+ 邮箱验证
1112
+ </h3>
1113
+ <p style={{ color: "#666", marginBottom: "10px" }}>
1114
+ {currentAccount ? `验证码已发送至 ${currentAccount.account}` : "验证码已发送至邮箱"}
1115
+ </p>
1116
+ <Form.Item
1117
+ label="验证码"
1118
+ name="verification_code"
1119
+ style={{ maxWidth: "300px", margin: "0 auto 10px" }}
1120
+ >
1121
+ <Input placeholder="输入邮箱验证码" />
1122
+ </Form.Item>
1123
+ <Button
1124
+ type="default"
1125
+ onClick={handleAutoFetchCode}
1126
+ loading={autoFetchLoading}
1127
+ style={{ marginRight: "8px" }}
1128
+ >
1129
+ 自动获取验证码
1130
+ </Button>
1131
+ <Progress percent={60} status="active" style={{ marginTop: "10px" }} />
1132
+ </div>
1133
+ );
1134
+ }
1135
+ })(),
1136
+ },
1137
+ {
1138
+ title: "结果",
1139
+ content: (() => {
1140
+ if (allAccountsProcessed) {
1141
+ return (
1142
+ <div
1143
+ className="step-content-container"
1144
+ style={{ textAlign: "center", padding: "12px" }}
1145
+ >
1146
+ <div
1147
+ style={{
1148
+ fontSize: "42px",
1149
+ color: "#52c41a",
1150
+ marginBottom: "10px",
1151
+ }}
1152
+ >
1153
+ <CheckCircleOutlined />
1154
+ </div>
1155
+ <h3
1156
+ style={{
1157
+ marginBottom: "10px",
1158
+ fontSize: "18px",
1159
+ fontWeight: "bold",
1160
+ }}
1161
+ >
1162
+ 所有账号处理完成!
1163
+ </h3>
1164
+ <p style={{ color: "#666", marginBottom: "10px" }}>
1165
+ 所有账号已处理完毕,可以在右侧查看处理结果
1166
+ </p>
1167
+ <Progress percent={100} status="success" />
1168
+ </div>
1169
+ );
1170
+ } else {
1171
+ return (
1172
+ <div
1173
+ className="step-content-container"
1174
+ style={{ textAlign: "center", padding: "12px" }}
1175
+ >
1176
+ <div
1177
+ style={{
1178
+ fontSize: "42px",
1179
+ color: "#8c8c8c",
1180
+ marginBottom: "10px",
1181
+ }}
1182
+ >
1183
+ <SyncOutlined />
1184
+ </div>
1185
+ <h3
1186
+ style={{
1187
+ marginBottom: "10px",
1188
+ fontSize: "18px",
1189
+ fontWeight: "bold",
1190
+ }}
1191
+ >
1192
+ 等待完成注册
1193
+ </h3>
1194
+ <p style={{ color: "#666", marginBottom: "10px" }}>
1195
+ 等待完成前面的步骤
1196
+ </p>
1197
+ <Progress percent={0} status="normal" />
1198
+ </div>
1199
+ );
1200
+ }
1201
+ })(),
1202
+ },
1203
+ ];
1204
+
1205
+ const next = () => {
1206
+ if (current < steps.length - 1) {
1207
+ setCurrent(current + 1);
1208
+ }
1209
+ };
1210
+
1211
+ const handleMainButtonClick = async () => {
1212
+ if (current === 0) {
1213
+ // 初始化页面
1214
+ const currentAccount = getCurrentAccount();
1215
+
1216
+ // 如果当前有错误状态的账号,提供重试选项
1217
+ if (currentAccount && currentAccount.status === "error") {
1218
+ handleRetryInitialization();
1219
+ } else {
1220
+ await startProcessing();
1221
+ }
1222
+ } else if (current === 1) {
1223
+ // 滑块验证页面
1224
+ if (captchaError) {
1225
+ // 如果有错误,重试滑块验证
1226
+ handleRetryCaptcha();
1227
+ } else if (isCaptchaVerified) {
1228
+ next(); // 验证已通过,移至下一步
1229
+ } else {
1230
+ // 未开始验证,显示警告
1231
+ console.log("请先完成滑块验证!");
1232
+ message.warning("请先完成滑块验证!");
1233
+ }
1234
+ } else if (current === 2) {
1235
+ // 邮箱验证页面
1236
+ if (registrationError) {
1237
+ // 注册失败,重试注册
1238
+ handleRetryRegistration();
1239
+ } else if (emailVerificationError) {
1240
+ // 获取验证码失败,重试获取
1241
+ handleRetryEmailVerification();
1242
+ } else {
1243
+ // 正常验证
1244
+ await handleEmailVerification();
1245
+ }
1246
+ } else if (current === 3) {
1247
+ // 结果页面
1248
+ if (allAccountsProcessed) {
1249
+ // 如果所有账号都已处理完成,重置表单
1250
+ resetForm();
1251
+ } else {
1252
+ // 否则,开始处理下一个账号
1253
+ handleStartNextAccount(processingIndex + 1);
1254
+ }
1255
+ }
1256
+ };
1257
+
1258
+ let mainButtonText = "开始处理";
1259
+ let mainButtonLoading = loading;
1260
+ let mainButtonDisabled = false;
1261
+
1262
+ if (current === 0) {
1263
+ const currentAccount = getCurrentAccount();
1264
+ if (currentAccount && currentAccount.status === "error") {
1265
+ mainButtonText = "重试初��化";
1266
+ mainButtonDisabled = false;
1267
+ } else {
1268
+ mainButtonText = "开始处理";
1269
+ mainButtonDisabled = loading || processingIndex !== -1; // Disable if already processing
1270
+ }
1271
+ } else if (current === 1) {
1272
+ // 滑块验证页面
1273
+ if (captchaError) {
1274
+ mainButtonText = "重试滑块验证";
1275
+ mainButtonLoading = captchaLoading;
1276
+ } else if (isCaptchaVerified) {
1277
+ mainButtonText = "下一步";
1278
+ mainButtonDisabled = false;
1279
+ } else {
1280
+ mainButtonText = "开始验证";
1281
+ mainButtonLoading = captchaLoading;
1282
+ }
1283
+ } else if (current === 2) {
1284
+ // 邮箱验证页面
1285
+ if (registrationError) {
1286
+ mainButtonText = "重试注册";
1287
+ mainButtonLoading = emailVerifyLoading;
1288
+ } else if (emailVerificationError) {
1289
+ mainButtonText = "重试获取验证码";
1290
+ mainButtonLoading = autoFetchLoading;
1291
+ } else {
1292
+ mainButtonText = "验证邮箱";
1293
+ mainButtonLoading = emailVerifyLoading;
1294
+ mainButtonDisabled = form.getFieldValue("verification_code") === "";
1295
+ }
1296
+ } else if (current === 3) {
1297
+ if (allAccountsProcessed) {
1298
+ mainButtonText = "完成并重置";
1299
+ } else if (processingIndex === accountList.length - 1) {
1300
+ mainButtonText = "完成";
1301
+ } else {
1302
+ mainButtonText = "开始下一个账号";
1303
+ }
1304
+ }
1305
+
1306
+ return (
1307
+ <div className="register-container">
1308
+ <Card title="PikPak 账号注册" variant="borderless" className="register-card">
1309
+ <Row gutter={24}>
1310
+ <Col xs={24} md={24}>
1311
+ {" "}
1312
+ {/* 左侧表单 */}
1313
+ <Form
1314
+ form={form}
1315
+ layout="vertical"
1316
+ initialValues={{
1317
+ accountInfo: "",
1318
+ use_proxy: false,
1319
+ proxy_url: "http://127.0.0.1:7890",
1320
+ }}
1321
+ >
1322
+ <Form.Item label="邀请码" required>
1323
+ <Row align="middle" gutter={8}>
1324
+ <Col flex="auto">
1325
+ <Form.Item
1326
+ name="invite_code"
1327
+ noStyle
1328
+ rules={[{ required: true, message: "请输入邀请码" }]}
1329
+ >
1330
+ <Input
1331
+ placeholder="请输入邀请码"
1332
+ disabled={loading}
1333
+ onChange={handleInviteCodeInputChange}
1334
+ />
1335
+ </Form.Item>
1336
+ </Col>
1337
+ <Col>
1338
+ <Checkbox
1339
+ checked={saveInviteCode}
1340
+ onChange={handleSaveInviteCodeChange}
1341
+ disabled={loading}
1342
+ >
1343
+ 保存
1344
+ </Checkbox>
1345
+ </Col>
1346
+ </Row>
1347
+ </Form.Item>
1348
+ <Form.Item
1349
+ label="微软邮箱信息 (每行一条)"
1350
+ name="accountInfo"
1351
+ rules={[
1352
+ { required: true, message: "请输入账号信息" },
1353
+ // 可选:添加更复杂的自定义验证器
1354
+ ]}
1355
+ >
1356
+ <TextArea
1357
+ rows={10}
1358
+ placeholder="格式: 账号----密码----clientId----授权令牌"
1359
+ disabled={loading}
1360
+ />
1361
+ </Form.Item>
1362
+ <Form.Item
1363
+ label="使用代理"
1364
+ name="use_proxy"
1365
+ valuePropName="checked"
1366
+ >
1367
+ <Switch
1368
+ onChange={(checked) => setUseProxy(checked)}
1369
+ disabled={loading}
1370
+ />
1371
+ </Form.Item>
1372
+ {useProxy && (
1373
+ <Form.Item label="代理地址" required={useProxy}>
1374
+ <div
1375
+ style={{
1376
+ display: "flex",
1377
+ gap: "8px",
1378
+ alignItems: "center",
1379
+ }}
1380
+ >
1381
+ <Form.Item
1382
+ name="proxy_url"
1383
+ rules={[
1384
+ { required: useProxy, message: "请输入代理地址" },
1385
+ ]}
1386
+ style={{ flexGrow: 1, marginBottom: 0 }}
1387
+ >
1388
+ <Input
1389
+ placeholder="例如: http://127.0.0.1:7890"
1390
+ disabled={loading}
1391
+ onChange={() => setProxyTestResult("idle")}
1392
+ />
1393
+ </Form.Item>
1394
+ <Button
1395
+ onClick={handleTestProxy}
1396
+ loading={testingProxy}
1397
+ disabled={loading}
1398
+ >
1399
+ 测试代理
1400
+ </Button>
1401
+ {proxyTestResult === "success" && (
1402
+ <CheckCircleOutlined
1403
+ style={{ color: "#52c41a", fontSize: "18px" }}
1404
+ />
1405
+ )}
1406
+ {proxyTestResult === "error" && (
1407
+ <CloseCircleOutlined
1408
+ style={{ color: "#ff4d4f", fontSize: "18px" }}
1409
+ />
1410
+ )}
1411
+ </div>
1412
+ </Form.Item>
1413
+ )}
1414
+ </Form>
1415
+ </Col>
1416
+ </Row>{" "}
1417
+ {/* Add current to key */}
1418
+ <div className="steps-action">
1419
+ {current <= 3 && (
1420
+ <Button
1421
+ type="primary"
1422
+ onClick={handleMainButtonClick}
1423
+ loading={mainButtonLoading}
1424
+ disabled={mainButtonDisabled}
1425
+ >
1426
+ {mainButtonText}
1427
+ </Button>
1428
+ )}
1429
+ </div>
1430
+ </Card>
1431
+ <div className="register-right">
1432
+ {/* 右侧状态/说明 */}
1433
+ <Row gutter={24}>
1434
+ <Card
1435
+ className="register-card"
1436
+ title={`处理状态 ${
1437
+ processingIndex >= 0
1438
+ ? `(账号 ${processingIndex + 1} / ${accountList.length})`
1439
+ : ""
1440
+ }`}
1441
+ >
1442
+ {processingIndex === -1 && accountList.length === 0 && (
1443
+ <div>
1444
+ <p>请在左侧输入微软邮箱信息,每行一条,格式如下:</p>
1445
+ <pre>
1446
+ <code>邮箱----密码----clientId----授权令牌</code>
1447
+ </pre>
1448
+ <p>例如:</p>
1449
+ <pre>
1450
+ <code>
1451
+ [email protected]_abc----token_xyz
1452
+ </code>
1453
+ </pre>
1454
+ <p>输入完成后,点击"开始处理"开始处理。</p>
1455
+ </div>
1456
+ )}
1457
+ {(processingIndex !== -1 || accountList.length > 0) && (
1458
+ <div style={{ maxHeight: "calc(50vh - 127px)", overflowY: "auto" }}>
1459
+ {accountList.map((acc, index) => (
1460
+ <div
1461
+ key={acc.id}
1462
+ style={{
1463
+ marginBottom: "10px",
1464
+ padding: "8px",
1465
+ border:
1466
+ index === processingIndex
1467
+ ? "2px solid #1890ff"
1468
+ : "1px solid #eee",
1469
+ borderRadius: "4px",
1470
+ background:
1471
+ index === processingIndex ? "#e6f7ff" : "transparent",
1472
+ opacity:
1473
+ acc.status === "success" || acc.status === "error"
1474
+ ? 0.7
1475
+ : 1,
1476
+ }}
1477
+ >
1478
+ <Spin
1479
+ spinning={
1480
+ acc.status === "processing" ||
1481
+ acc.status === "initializing"
1482
+ }
1483
+ size="small"
1484
+ style={{ marginRight: "8px" }}
1485
+ >
1486
+ <strong>账号:</strong> {acc.account}
1487
+ </Spin>
1488
+ <div style={{ marginTop: "5px" }}>
1489
+ <strong>状态:</strong>{" "}
1490
+ {acc.status === "pending" && <Tag>待处理</Tag>}
1491
+ {acc.status === "processing" && (
1492
+ <Tag color="processing">处理中</Tag>
1493
+ )}
1494
+ {acc.status === "initializing" && (
1495
+ <Tag color="processing">初始化中</Tag>
1496
+ )}
1497
+ {acc.status === "captcha_pending" && (
1498
+ <Tag color="warning">等待验证码</Tag>
1499
+ )}
1500
+ {acc.status === "email_pending" && (
1501
+ <Tag color="warning">等待邮箱验证</Tag>
1502
+ )}
1503
+ {acc.status === "success" && (
1504
+ <Tag color="success">注册成功</Tag>
1505
+ )}
1506
+ {acc.status === "error" && (
1507
+ <Tag color="error">失败</Tag>
1508
+ )}
1509
+ </div>
1510
+ {acc.message && (
1511
+ <div style={{ marginTop: "5px", fontSize: "12px" }}>
1512
+ <strong>消息:</strong>{" "}
1513
+ <span style={{
1514
+ color: acc.message && (acc.message.includes("失败") || acc.message.includes("错误"))
1515
+ ? "#ff4d4f"
1516
+ : acc.message && acc.message.includes("成功")
1517
+ ? "#52c41a"
1518
+ : "#666"
1519
+ }}>
1520
+ {acc.message}
1521
+ </span>
1522
+ {acc.message && (acc.message.includes("失败") || acc.message.includes("错误")) && (
1523
+ <span>
1524
+ <Button
1525
+ size="small"
1526
+ type="text"
1527
+ danger
1528
+ style={{ marginLeft: "4px" }}
1529
+ onClick={() => {
1530
+ // 根据消息类型确定重试哪个步骤
1531
+ if (index === processingIndex) {
1532
+ if (acc.message && acc.message.includes("初始化失败")) {
1533
+ handleRetryInitialization();
1534
+ } else if (acc.message && acc.message.includes("滑块验证")) {
1535
+ handleRetryCaptcha();
1536
+ } else if (acc.message && acc.message.includes("获取验证码")) {
1537
+ handleRetryEmailVerification();
1538
+ } else if (acc.message && acc.message.includes("注册失败")) {
1539
+ handleRetryRegistration();
1540
+ }
1541
+ } else {
1542
+ // 如果不是当前处理的账号,先切换到它
1543
+ setProcessingIndex(index);
1544
+ message.info(`已切换到账号 ${acc.account}`);
1545
+ }
1546
+ }}
1547
+ >
1548
+ 重试
1549
+ </Button>
1550
+ <Button
1551
+ size="small"
1552
+ type="text"
1553
+ style={{ marginLeft: "4px" }}
1554
+ onClick={() => {
1555
+ if (index === processingIndex) {
1556
+ // 如果是当前处理的账号,直接跳过
1557
+ moveToNextAccountOrComplete();
1558
+ } else {
1559
+ // 如果不是当前处理的账号,提示不能跳过
1560
+ message.info(`只能跳过当前正在处理的账号`);
1561
+ }
1562
+ }}
1563
+ >
1564
+ 跳过
1565
+ </Button>
1566
+ </span>
1567
+ )}
1568
+ </div>
1569
+ )}
1570
+ </div>
1571
+ ))}
1572
+ </div>
1573
+ )}
1574
+ </Card>
1575
+ </Row>
1576
+ <Row gutter={24}>
1577
+ <Card title={steps[current].title} className="register-card">
1578
+ {steps[current].content}
1579
+ </Card>
1580
+ </Row>
1581
+ </div>
1582
+ </div>
1583
+ );
1584
+ };
1585
+
1586
+ export default Register;
frontend/src/services/api.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+
3
+ const api = axios.create({
4
+ baseURL: '/api',
5
+ timeout: 100000,
6
+ headers: {
7
+ 'Content-Type': 'application/json',
8
+ },
9
+ });
10
+
11
+
12
+ // 测试代理
13
+ export const testProxy = async (data: any) => {
14
+ return api.post('/test_proxy', data);
15
+ };
16
+
17
+ // 初始化注册
18
+ export const initialize = async (data: any) => {
19
+ return api.post('/initialize', data, {
20
+ headers: {
21
+ 'Content-Type': 'multipart/form-data',
22
+ },
23
+ });
24
+ };
25
+
26
+ // 验证验证码
27
+ export const verifyCaptha = async (data:any) => {
28
+ return api.post('/verify_captcha',data, {
29
+ headers: {
30
+ 'Content-Type': 'multipart/form-data',
31
+ },
32
+ });
33
+ };
34
+
35
+ // 注册账号
36
+ export const register = async (data: any) => {
37
+ return api.post('/register', data, {
38
+ headers: {
39
+ 'Content-Type': 'multipart/form-data',
40
+ },
41
+ });
42
+ };
43
+
44
+ // 获取邮箱验证码
45
+ export const getEmailVerificationCode = async (data: any) => {
46
+ return api.post('/get_email_verification_code', data);
47
+ };
48
+
49
+ // 激活账号
50
+ export const activateAccounts = async (key: string, names: string[], all: boolean=false) => {
51
+ return api.post('/activate_account_with_names', { key, names, all });
52
+ };
53
+
54
+ // 获取账号列表
55
+ export const fetchAccounts = async () => {
56
+ return api.get('/fetch_accounts');
57
+ };
58
+
59
+ // 删除账号
60
+ export const deleteAccount = async (filename: string) => {
61
+ const formData = new FormData();
62
+ formData.append('filename', filename);
63
+ return api.post('/delete_account', formData, {
64
+ headers: {
65
+ 'Content-Type': 'multipart/form-data',
66
+ },
67
+ });
68
+ };
69
+
70
+ // 批量删除账号
71
+ export const deleteAccounts = async (filenames: string[]) => {
72
+ const formData = new FormData();
73
+
74
+ // 将多个文件名添加到 FormData
75
+ filenames.forEach(filename => {
76
+ formData.append('filenames', filename);
77
+ });
78
+
79
+ return api.post('/delete_account', formData, {
80
+ headers: {
81
+ 'Content-Type': 'multipart/form-data',
82
+ },
83
+ });
84
+ };
85
+
86
+ // 更新账号
87
+ export const updateAccount = async (filename: string, accountData: any) => {
88
+ return api.post('/update_account', {
89
+ filename,
90
+ account_data: accountData,
91
+ });
92
+ };
93
+
94
+ export default api;
frontend/src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
frontend/tsconfig.app.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "isolatedModules": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["src"]
26
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "isolatedModules": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedSideEffectImports": true
22
+ },
23
+ "include": ["vite.config.ts"]
24
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ server: {
8
+ port: 5678,
9
+ proxy: {
10
+ '/api': {
11
+ target: 'http://localhost:5000',
12
+ changeOrigin: true,
13
+ }
14
+ },
15
+ },
16
+ resolve: {
17
+ alias: {
18
+ '@': '/src',
19
+ },
20
+ },
21
+ })
requirements.txt ADDED
Binary file (124 Bytes). View file
 
run.py ADDED
@@ -0,0 +1,1102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import time
4
+ import uuid
5
+ import argparse # 导入参数解析库
6
+ import random
7
+ import string
8
+ import logging
9
+
10
+ import requests
11
+ from flask import Flask, render_template, request, jsonify, send_from_directory
12
+ from flask_cors import CORS
13
+
14
+
15
+ # 导入 pikpak.py 中的函数
16
+ from utils.pk_email import connect_imap
17
+ from utils.pikpak import (
18
+ sign_encrypt,
19
+ captcha_image_parse,
20
+ ramdom_version,
21
+ random_rtc_token,
22
+ PikPak,
23
+ save_account_info,
24
+ test_proxy,
25
+ )
26
+
27
+ # 导入 email_client
28
+ from utils.email_client import EmailClient
29
+
30
+ # 重试参数
31
+ max_retries = 3
32
+ retry_delay = 1.0
33
+
34
+ # 设置日志
35
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # 定义一个retry函数,用于重试指定的函数
39
+ def retry_function(func, *args, max_retries=3, delay=1, **kwargs):
40
+ """
41
+ 对指定函数进行重试
42
+
43
+ Args:
44
+ func: 要重试的函数
45
+ *args: 传递给函数的位置参数
46
+ max_retries: 最大重试次数,默认为3
47
+ delay: 每次重试之间的延迟(秒),默认为1
48
+ **kwargs: 传递给函数的关键字参数
49
+
50
+ Returns:
51
+ 函数的返回值,如果所有重试都失败则返回None
52
+ """
53
+ retries = 0
54
+ result = None
55
+
56
+ while retries < max_retries:
57
+ if retries > 0:
58
+ logger.info(f"第 {retries} 次重试函数 {func.__name__}...")
59
+
60
+ result = func(*args, **kwargs)
61
+
62
+ # 如果函数返回非None结果,视为成功
63
+ if result is not None:
64
+ if retries > 0:
65
+ logger.info(f"在第 {retries} 次重试后成功")
66
+ return result
67
+
68
+ # 如果达到最大重试次数,返回最后一次结果
69
+ if retries >= max_retries - 1:
70
+ logger.warning(f"函数 {func.__name__} 在 {max_retries} 次尝试后失败")
71
+ break
72
+
73
+ # 等待指定的延迟时间
74
+ time.sleep(delay)
75
+ retries += 1
76
+
77
+ return result
78
+
79
+ # 解析命令行参数
80
+ parser = argparse.ArgumentParser(description="PikPak 自动邀请注册系统")
81
+ args = parser.parse_args()
82
+
83
+ app = Flask(__name__, static_url_path='/assets')
84
+ # cors
85
+ CORS(app, resources={r"/*": {"origins": "*"}})
86
+ app.secret_key = os.urandom(24)
87
+
88
+ # 全局字典用于存储用户处理过程中的数据,以 email 为键
89
+ user_process_data = {}
90
+
91
+ @app.route("/api/health")
92
+ def health_check():
93
+
94
+ in_huggingface = (
95
+ os.environ.get("SPACE_ID") is not None or os.environ.get("SYSTEM") == "spaces"
96
+ )
97
+
98
+ return jsonify(
99
+ {
100
+ "status": "OK",
101
+ }
102
+ )
103
+
104
+
105
+ @app.route("/api/initialize", methods=["POST"])
106
+ def initialize():
107
+ # 获取用户表单输入
108
+ use_proxy = request.form.get("use_proxy") == "true"
109
+ proxy_url = request.form.get("proxy_url", "")
110
+ invite_code = request.form.get("invite_code", "")
111
+ email = request.form.get("email", "")
112
+
113
+ # 初始化参数
114
+ current_version = ramdom_version()
115
+ version = current_version["v"]
116
+ algorithms = current_version["algorithms"]
117
+ client_id = "YNxT9w7GMdWvEOKa"
118
+ client_secret = "dbw2OtmVEeuUvIptb1Coyg"
119
+ package_name = "com.pikcloud.pikpak"
120
+ device_id = str(uuid.uuid4()).replace("-", "")
121
+ rtc_token = random_rtc_token()
122
+
123
+ # 将这些参数存储到会话中以便后续使用
124
+ # session["use_proxy"] = use_proxy
125
+ # session["proxy_url"] = proxy_url
126
+ # session["invite_code"] = invite_code
127
+ # session["email"] = email
128
+ # session["version"] = version
129
+ # session["algorithms"] = algorithms
130
+ # session["client_id"] = client_id
131
+ # session["client_secret"] = client_secret
132
+ # session["package_name"] = package_name
133
+ # session["device_id"] = device_id
134
+ # session["rtc_token"] = rtc_token
135
+
136
+ # 如果启用代理,则测试代理
137
+ proxy_status = None
138
+ if use_proxy:
139
+ proxy_status = test_proxy(proxy_url)
140
+
141
+ # 创建PikPak实例
142
+ pikpak = PikPak(
143
+ invite_code,
144
+ client_id,
145
+ device_id,
146
+ version,
147
+ algorithms,
148
+ email,
149
+ rtc_token,
150
+ client_secret,
151
+ package_name,
152
+ use_proxy=use_proxy,
153
+ proxy_http=proxy_url,
154
+ proxy_https=proxy_url,
155
+ )
156
+
157
+ # 初始化验证码
158
+ init_result = pikpak.init("POST:/v1/auth/verification")
159
+ if (
160
+ not init_result
161
+ or not isinstance(init_result, dict)
162
+ or "captcha_token" not in init_result
163
+ ):
164
+ return jsonify(
165
+ {"status": "error", "message": "初始化失败,请检查网络连接或代理设置"}
166
+ )
167
+
168
+ # 将用户数据存储在全局字典中
169
+ user_data = {
170
+ "use_proxy": use_proxy,
171
+ "proxy_url": proxy_url,
172
+ "invite_code": invite_code,
173
+ "email": email,
174
+ "version": version,
175
+ "algorithms": algorithms,
176
+ "client_id": client_id,
177
+ "client_secret": client_secret,
178
+ "package_name": package_name,
179
+ "device_id": device_id,
180
+ "rtc_token": rtc_token,
181
+ "captcha_token": pikpak.captcha_token, # Store captcha_token here
182
+ }
183
+ user_process_data[email] = user_data
184
+
185
+ # 将验证码令牌保存到会话中 - REMOVED
186
+ # session["captcha_token"] = pikpak.captcha_token
187
+
188
+ return jsonify(
189
+ {
190
+ "status": "success",
191
+ "message": "初始化成功,请进行滑块验证",
192
+ "email": email, # Return email to client
193
+ "proxy_status": proxy_status,
194
+ "version": version,
195
+ "device_id": device_id,
196
+ "rtc_token": rtc_token,
197
+ }
198
+ )
199
+
200
+
201
+ @app.route("/api/verify_captcha", methods=["POST"])
202
+ def verify_captcha():
203
+ # 尝试从表单或JSON获取email
204
+ email = request.form.get('email')
205
+ if not email and request.is_json:
206
+ data = request.get_json()
207
+ email = data.get('email')
208
+
209
+ if not email:
210
+ return jsonify({"status": "error", "message": "请求中未提供Email"})
211
+
212
+ # 从全局字典获取用户数据
213
+ user_data = user_process_data.get(email)
214
+ if not user_data:
215
+ return jsonify({"status": "error", "message": "会话数据不存在或已过期,请重新初始化"})
216
+
217
+ # 从 user_data 中获取存储的数据
218
+ device_id = user_data.get("device_id")
219
+ # email = user_data.get("email") # Email is now the key, already have it
220
+ invite_code = user_data.get("invite_code")
221
+ client_id = user_data.get("client_id")
222
+ version = user_data.get("version")
223
+ algorithms = user_data.get("algorithms")
224
+ rtc_token = user_data.get("rtc_token")
225
+ client_secret = user_data.get("client_secret")
226
+ package_name = user_data.get("package_name")
227
+ use_proxy = user_data.get("use_proxy")
228
+ proxy_url = user_data.get("proxy_url")
229
+ captcha_token = user_data.get("captcha_token", "") # Use get with default
230
+
231
+ # Check if essential data is present (device_id is checked as example)
232
+ if not device_id:
233
+ return jsonify({"status": "error", "message": "必要的会话数据丢失,请重新初始化"})
234
+
235
+
236
+ # 创建PikPak实例 (使用从 user_data 获取的数据)
237
+ pikpak = PikPak(
238
+ invite_code,
239
+ client_id,
240
+ device_id,
241
+ version,
242
+ algorithms,
243
+ email,
244
+ rtc_token,
245
+ client_secret,
246
+ package_name,
247
+ use_proxy=use_proxy,
248
+ proxy_http=proxy_url,
249
+ proxy_https=proxy_url,
250
+ )
251
+
252
+ # 从 user_data 设置验证码令牌
253
+ pikpak.captcha_token = captcha_token
254
+
255
+ # 尝试验证码验证
256
+ max_attempts = 5
257
+ captcha_result = None
258
+
259
+ for attempt in range(max_attempts):
260
+ try:
261
+ captcha_result = captcha_image_parse(pikpak, device_id)
262
+ if (
263
+ captcha_result
264
+ and "response_data" in captcha_result
265
+ and captcha_result["response_data"].get("result") == "accept"
266
+ ):
267
+ break
268
+ time.sleep(2)
269
+ except Exception as e:
270
+ time.sleep(2)
271
+
272
+ if (
273
+ not captcha_result
274
+ or "response_data" not in captcha_result
275
+ or captcha_result["response_data"].get("result") != "accept"
276
+ ):
277
+ return jsonify({"status": "error", "message": "滑块验证失败,请重试"})
278
+
279
+ # 滑块验证加密
280
+ try:
281
+ executor_info = pikpak.executor()
282
+ if not executor_info:
283
+ return jsonify({"status": "error", "message": "获取executor信息失败"})
284
+
285
+ sign_encrypt_info = sign_encrypt(
286
+ executor_info,
287
+ pikpak.captcha_token,
288
+ rtc_token,
289
+ pikpak.use_proxy,
290
+ pikpak.proxies,
291
+ )
292
+ if (
293
+ not sign_encrypt_info
294
+ or "request_id" not in sign_encrypt_info
295
+ or "sign" not in sign_encrypt_info
296
+ ):
297
+ return jsonify({"status": "error", "message": "签名加密失败"})
298
+
299
+ # 更新验证码令牌
300
+ report_result = pikpak.report(
301
+ sign_encrypt_info["request_id"],
302
+ sign_encrypt_info["sign"],
303
+ captcha_result["pid"],
304
+ captcha_result["traceid"],
305
+ )
306
+
307
+ # 请求邮箱验证码
308
+ verification_result = pikpak.verification()
309
+ if (
310
+ not verification_result
311
+ or not isinstance(verification_result, dict)
312
+ or "verification_id" not in verification_result
313
+ ):
314
+ return jsonify({"status": "error", "message": "请求验证码失败"})
315
+
316
+ # 将更新的数据保存到 user_data 中
317
+ user_data["captcha_token"] = pikpak.captcha_token
318
+ user_data["verification_id"] = pikpak.verification_id
319
+
320
+ return jsonify({"status": "success", "message": "验证码已发送到邮箱,请查收"})
321
+
322
+ except Exception as e:
323
+ import traceback
324
+
325
+ error_trace = traceback.format_exc()
326
+ return jsonify(
327
+ {
328
+ "status": "error",
329
+ "message": f"验证过程出错: {str(e)}",
330
+ "trace": error_trace,
331
+ }
332
+ )
333
+
334
+ def gen_password():
335
+ # 生成12位密码
336
+ return "".join(random.choices(string.ascii_letters + string.digits, k=12))
337
+
338
+ @app.route("/api/register", methods=["POST"])
339
+ def register():
340
+ # 从表单获取验证码和email
341
+ verification_code = request.form.get("verification_code")
342
+ email = request.form.get('email') # Get email from form
343
+
344
+ if not email:
345
+ return jsonify({"status": "error", "message": "请求中未提供Email"})
346
+
347
+ if not verification_code:
348
+ return jsonify({"status": "error", "message": "验证码不能为空"})
349
+
350
+ # 从全局字典获取用户数据
351
+ user_data = user_process_data.get(email)
352
+ if not user_data:
353
+ return jsonify({"status": "error", "message": "会话数据不存在或已过期,请重新初始化"})
354
+
355
+
356
+ # 从 user_data 中获取存储的数据
357
+ device_id = user_data.get("device_id")
358
+ # email = user_data.get("email") # Already have email
359
+ invite_code = user_data.get("invite_code")
360
+ client_id = user_data.get("client_id")
361
+ version = user_data.get("version")
362
+ algorithms = user_data.get("algorithms")
363
+ rtc_token = user_data.get("rtc_token")
364
+ client_secret = user_data.get("client_secret")
365
+ package_name = user_data.get("package_name")
366
+ use_proxy = user_data.get("use_proxy")
367
+ proxy_url = user_data.get("proxy_url")
368
+ verification_id = user_data.get("verification_id")
369
+ captcha_token = user_data.get("captcha_token", "")
370
+
371
+ # Check if essential data is present
372
+ if not device_id or not verification_id:
373
+ return jsonify({"status": "error", "message": "必要的会话数据丢失,请重新初始化"})
374
+
375
+ # 创建PikPak实例
376
+ pikpak = PikPak(
377
+ invite_code,
378
+ client_id,
379
+ device_id,
380
+ version,
381
+ algorithms,
382
+ email,
383
+ rtc_token,
384
+ client_secret,
385
+ package_name,
386
+ use_proxy=use_proxy,
387
+ proxy_http=proxy_url,
388
+ proxy_https=proxy_url,
389
+ )
390
+
391
+ # 从 user_data 中设置验证码令牌和验证ID
392
+ pikpak.captcha_token = captcha_token
393
+ pikpak.verification_id = verification_id
394
+
395
+ # 验证验证码
396
+ pikpak.verify_post(verification_code)
397
+
398
+ # 刷新时间戳并加密签名值
399
+ pikpak.init("POST:/v1/auth/signup")
400
+
401
+ # 注册并登录
402
+ name = email.split("@")[0]
403
+ password = gen_password() # 默认密码
404
+ signup_result = pikpak.signup(name, password, verification_code)
405
+
406
+ # 填写邀请码
407
+ pikpak.activation_code()
408
+
409
+ if (
410
+ not signup_result
411
+ or not isinstance(signup_result, dict)
412
+ or "access_token" not in signup_result
413
+ ):
414
+ return jsonify({"status": "error", "message": "注册失败,请检查验证码或重试"})
415
+
416
+ # 保存账号信息到JSON文件
417
+ account_info = {
418
+ "captcha_token": pikpak.captcha_token,
419
+ "timestamp": pikpak.timestamp,
420
+ "name": name,
421
+ "email": email,
422
+ "password": password,
423
+ "device_id": device_id,
424
+ "version": version,
425
+ "user_id": signup_result.get("sub", ""),
426
+ "access_token": signup_result.get("access_token", ""),
427
+ "refresh_token": signup_result.get("refresh_token", ""),
428
+ "invite_code": invite_code,
429
+ }
430
+
431
+ # 保存账号信息
432
+ account_file = save_account_info(name, account_info)
433
+
434
+ return jsonify(
435
+ {
436
+ "status": "success",
437
+ "message": "注册成功!账号已保存。",
438
+ "account_info": account_info,
439
+ }
440
+ )
441
+
442
+
443
+ @app.route("/api/test_proxy", methods=["POST"])
444
+ def test_proxy_route():
445
+ proxy_url = request.form.get("proxy_url", "http://127.0.0.1:7890")
446
+ result = test_proxy(proxy_url)
447
+
448
+ return jsonify(
449
+ {
450
+ "status": "success" if result else "error",
451
+ "message": "代理连接测试成功" if result else "代理连接测试失败",
452
+ }
453
+ )
454
+
455
+
456
+ @app.route("/api/get_verification", methods=["POST"])
457
+ def get_verification():
458
+ """
459
+ 处理获取验证码的请求
460
+ """
461
+ email_user = request.form["email"]
462
+ email_password = request.form["password"]
463
+
464
+ # 先尝试从收件箱获取验证码
465
+ result = connect_imap(email_user, email_password, "INBOX")
466
+
467
+ # 如果收件箱没有找到验证码,则尝试从垃圾邮件中查找
468
+ if result["code"] == 0:
469
+ result = connect_imap(email_user, email_password, "Junk")
470
+
471
+ return jsonify(result)
472
+
473
+
474
+ @app.route("/api/fetch_accounts", methods=["GET"])
475
+ def fetch_accounts():
476
+ # 获取account文件夹中的所有JSON文件
477
+ account_files = []
478
+ for file in os.listdir("account"):
479
+ if file.endswith(".json"):
480
+ file_path = os.path.join("account", file)
481
+ try:
482
+ with open(file_path, "r", encoding="utf-8") as f:
483
+ account_data = json.load(f)
484
+ if isinstance(account_data, dict):
485
+ # 添加文件名属性,用于后续操作
486
+ account_data["filename"] = file
487
+ account_files.append(account_data)
488
+ except Exception as e:
489
+ logger.error(f"Error reading {file}: {str(e)}")
490
+
491
+ if not account_files:
492
+ return jsonify(
493
+ {"status": "info", "message": "没有找到保存的账号", "accounts": []}
494
+ )
495
+ account_files.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
496
+
497
+ return jsonify(
498
+ {
499
+ "status": "success",
500
+ "message": f"找到 {len(account_files)} 个账号",
501
+ "accounts": account_files,
502
+ }
503
+ )
504
+
505
+
506
+ @app.route("/api/update_account", methods=["POST"])
507
+ def update_account():
508
+ data = request.json
509
+ if not data or "filename" not in data or "account_data" not in data:
510
+ return jsonify({"status": "error", "message": "请求数据不完整"})
511
+
512
+ filename = data.get("filename")
513
+ account_data = data.get("account_data")
514
+
515
+ # 安全检查文件名
516
+ if not filename or ".." in filename or not filename.endswith(".json"):
517
+ return jsonify({"status": "error", "message": "无效的文件名"})
518
+
519
+ file_path = os.path.join("account", filename)
520
+
521
+ try:
522
+ with open(file_path, "w", encoding="utf-8") as f:
523
+ json.dump(account_data, f, indent=4, ensure_ascii=False)
524
+
525
+ return jsonify({"status": "success", "message": "账号已成功更新"})
526
+ except Exception as e:
527
+ return jsonify({"status": "error", "message": f"更新账号时出错: {str(e)}"})
528
+
529
+
530
+ @app.route("/api/delete_account", methods=["POST"])
531
+ def delete_account():
532
+ # 检查是否是单个文件名还是文件名列表
533
+ if 'filenames' in request.form:
534
+ # 批量删除模式
535
+ filenames = request.form.getlist('filenames')
536
+ if not filenames:
537
+ return jsonify({"status": "error", "message": "未提供文件名"})
538
+
539
+ results = {
540
+ "success": [],
541
+ "failed": []
542
+ }
543
+
544
+ for filename in filenames:
545
+ # 安全检查文件名
546
+ if ".." in filename or not filename.endswith(".json"):
547
+ results["failed"].append({"filename": filename, "reason": "无效的文件名"})
548
+ continue
549
+
550
+ file_path = os.path.join("account", filename)
551
+
552
+ try:
553
+ # 检查文件是否存在
554
+ if not os.path.exists(file_path):
555
+ results["failed"].append({"filename": filename, "reason": "账号文件不存在"})
556
+ continue
557
+
558
+ # 删除文件
559
+ os.remove(file_path)
560
+ results["success"].append(filename)
561
+ except Exception as e:
562
+ results["failed"].append({"filename": filename, "reason": str(e)})
563
+
564
+ # 返回批量删除结果
565
+ if len(results["success"]) > 0:
566
+ if len(results["failed"]) > 0:
567
+ message = f"成功删除 {len(results['success'])} 个账号,{len(results['failed'])} 个账号删除失败"
568
+ status = "partial"
569
+ else:
570
+ message = f"成功删除 {len(results['success'])} 个账号"
571
+ status = "success"
572
+ else:
573
+ message = "所有账号删除失败"
574
+ status = "error"
575
+
576
+ return jsonify({
577
+ "status": status,
578
+ "message": message,
579
+ "results": results
580
+ })
581
+ else:
582
+ # 保持向后兼容 - 单个文件删除模式
583
+ filename = request.form.get("filename")
584
+
585
+ if not filename:
586
+ return jsonify({"status": "error", "message": "未提供文件名"})
587
+
588
+ # 安全检查文件名
589
+ if ".." in filename or not filename.endswith(".json"):
590
+ return jsonify({"status": "error", "message": "无效的文件名"})
591
+
592
+ file_path = os.path.join("account", filename)
593
+
594
+ try:
595
+ # 检查文件是否存在
596
+ if not os.path.exists(file_path):
597
+ return jsonify({"status": "error", "message": "账号文件不存在"})
598
+
599
+ # 删除文件
600
+ os.remove(file_path)
601
+
602
+ return jsonify({"status": "success", "message": "账号已成功删除"})
603
+ except Exception as e:
604
+ return jsonify({"status": "error", "message": f"删除账号时出错: {str(e)}"})
605
+
606
+ @app.route("/api/activate_account_with_names", methods=["POST"])
607
+ def activate_account_with_names():
608
+ try:
609
+ data = request.json
610
+ key = data.get("key")
611
+ names = data.get("names", []) # 获取指定的账户名称列表
612
+ all_accounts = data.get("all", False) # 获取是否处理所有账户的标志
613
+
614
+ if not key:
615
+ return jsonify({"status": "error", "message": "密钥不能为空"})
616
+
617
+ if not all_accounts and (not names or not isinstance(names, list)):
618
+ return jsonify({"status": "error", "message": "请提供有效的账户名称列表或设置 all=true"})
619
+
620
+ # 存储账号数据及其文件路径
621
+ accounts_with_paths = []
622
+ for file in os.listdir("account"):
623
+ if file.endswith(".json"):
624
+ # 如果 all=true 或者文件名在指定的names列表中,则处理该文件
625
+ file_name_without_ext = os.path.splitext(file)[0]
626
+ if all_accounts or file_name_without_ext in names or file in names:
627
+ file_path = os.path.join("account", file)
628
+ with open(file_path, "r", encoding="utf-8") as f:
629
+ account_data = json.load(f)
630
+ # 保存文件路径以便后续更新
631
+ accounts_with_paths.append(
632
+ {"path": file_path, "data": account_data}
633
+ )
634
+
635
+ if not accounts_with_paths:
636
+ return jsonify({"status": "error", "message": "未找到指定的账号数据"})
637
+
638
+ # 使用多线程处理每个账号
639
+ import threading
640
+ import queue
641
+
642
+ # 创建结果队列
643
+ result_queue = queue.Queue()
644
+
645
+ # 定义线程处理函数
646
+ def process_account(account_with_path, account_key, result_q):
647
+ try:
648
+ file_path = account_with_path["path"]
649
+ single_account = account_with_path["data"]
650
+
651
+ response = requests.post(
652
+ headers={
653
+ "Content-Type": "application/json",
654
+ "referer": "https://inject.kiteyuan.info/",
655
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0",
656
+ },
657
+ url="https://inject.kiteyuan.info/infoInject",
658
+ json={"info": single_account, "key": account_key},
659
+ timeout=30,
660
+ )
661
+
662
+ # 将结果放入队列
663
+ if response.status_code == 200:
664
+ api_result = response.json()
665
+
666
+ # 检查API是否返回了正确的数据格式
667
+ if isinstance(api_result, dict) and api_result.get("code") == 200 and "data" in api_result:
668
+ # 获取返回的数据对象
669
+ account_data = api_result.get("data", {})
670
+
671
+ if account_data and isinstance(account_data, dict):
672
+ # 更新账号信息
673
+ updated_account = single_account.copy()
674
+
675
+ # 更新令牌信息 (从data子对象中提取)
676
+ for key in ["access_token", "refresh_token", "captcha_token", "timestamp", "device_id", "user_id"]:
677
+ if key in account_data:
678
+ updated_account[key] = account_data[key]
679
+
680
+ # 保存更新后的账号数据
681
+ with open(file_path, "w", encoding="utf-8") as f:
682
+ json.dump(updated_account, f, indent=4, ensure_ascii=False)
683
+
684
+ # 将更新后的数据放入结果队列
685
+ result_q.put(
686
+ {
687
+ "status": "success",
688
+ "account": single_account.get("email", "未知邮箱"),
689
+ "result": account_data,
690
+ "updated": True,
691
+ }
692
+ )
693
+ else:
694
+ # 返回的data不是字典类型
695
+ result_q.put(
696
+ {
697
+ "status": "error",
698
+ "account": single_account.get("email", "未知邮箱"),
699
+ "message": "返回的数据格式不符合预期",
700
+ "result": api_result,
701
+ }
702
+ )
703
+ else:
704
+ # API返回错误码或格式不符合预期
705
+ error_msg = api_result.get("msg", "未知错误")
706
+ result_q.put(
707
+ {
708
+ "status": "error",
709
+ "account": single_account.get("email", "未知邮箱"),
710
+ "message": f"激活失败: {error_msg}",
711
+ "result": api_result,
712
+ }
713
+ )
714
+ else:
715
+ result_q.put(
716
+ {
717
+ "status": "error",
718
+ "account": single_account.get("email", "未知邮箱"),
719
+ "message": f"激活失败: HTTP {response.status_code}-{response.json().get('detail', '未知错误')}",
720
+ "result": response.text,
721
+ }
722
+ )
723
+ except Exception as e:
724
+ result_q.put(
725
+ {
726
+ "status": "error",
727
+ "account": single_account.get("email", "未知邮箱"),
728
+ "message": f"处理失败: {str(e)}",
729
+ }
730
+ )
731
+
732
+ # 创建并启动线程
733
+ threads = []
734
+ for account_with_path in accounts_with_paths:
735
+ thread = threading.Thread(
736
+ target=process_account, args=(account_with_path, key, result_queue)
737
+ )
738
+ threads.append(thread)
739
+ thread.start()
740
+
741
+ # 等待所有线程完成
742
+ for thread in threads:
743
+ thread.join()
744
+
745
+ # 收集所有结果
746
+ results = []
747
+ while not result_queue.empty():
748
+ results.append(result_queue.get())
749
+
750
+ # 统计成功和失败的数量
751
+ success_count = sum(1 for r in results if r["status"] == "success")
752
+ updated_count = sum(
753
+ 1
754
+ for r in results
755
+ if r.get("status") == "success" and r.get("updated", False)
756
+ )
757
+
758
+ return jsonify(
759
+ {
760
+ "status": "success",
761
+ "message": f"账号激活完成: {success_count}/{len(accounts_with_paths)}个成功, {updated_count}个已更新数据",
762
+ "results": results,
763
+ }
764
+ )
765
+
766
+ except Exception as e:
767
+ return jsonify({"status": "error", "message": f"操作失败: {str(e)}"})
768
+
769
+
770
+ @app.route("/api/check_email_inventory", methods=["GET"])
771
+ def check_email_inventory():
772
+ try:
773
+ # 发送请求到库存API
774
+ response = requests.get(
775
+ url="https://zizhu.shanyouxiang.com/kucun",
776
+ headers={
777
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
778
+ },
779
+ timeout=10,
780
+ )
781
+
782
+ if response.status_code == 200:
783
+ return jsonify({"status": "success", "inventory": response.json()})
784
+ else:
785
+ return jsonify(
786
+ {
787
+ "status": "error",
788
+ "message": f"获取库存失败: HTTP {response.status_code}",
789
+ }
790
+ )
791
+
792
+ except Exception as e:
793
+ return jsonify({"status": "error", "message": f"获取库存时出错: {str(e)}"})
794
+
795
+
796
+ @app.route("/api/check_balance", methods=["GET"])
797
+ def check_balance():
798
+ try:
799
+ # 从请求参数中获取卡号
800
+ card = request.args.get("card")
801
+
802
+ if not card:
803
+ return jsonify({"status": "error", "message": "未提供卡号参数"})
804
+
805
+ # 发送请求到余额查询API
806
+ response = requests.get(
807
+ url="https://zizhu.shanyouxiang.com/yue",
808
+ params={"card": card},
809
+ headers={
810
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
811
+ },
812
+ timeout=10,
813
+ )
814
+
815
+ if response.status_code == 200:
816
+ return jsonify({"status": "success", "balance": response.json()})
817
+ else:
818
+ return jsonify(
819
+ {
820
+ "status": "error",
821
+ "message": f"查询余额失败: HTTP {response.status_code}",
822
+ }
823
+ )
824
+
825
+ except Exception as e:
826
+ return jsonify({"status": "error", "message": f"查询余额时出错: {str(e)}"})
827
+
828
+
829
+ @app.route("/api/extract_emails", methods=["GET"])
830
+ def extract_emails():
831
+ try:
832
+ # 从请求参数中获取必需的参数
833
+ card = request.args.get("card")
834
+ shuliang = request.args.get("shuliang")
835
+ leixing = request.args.get("leixing")
836
+ # 获取前端传递的重试次数计数器,如果没有则初始化为0
837
+ frontend_retry_count = int(request.args.get("retry_count", "0"))
838
+
839
+ # 验证必需的参数
840
+ if not card:
841
+ return jsonify({"status": "error", "message": "未提供卡号参数"})
842
+
843
+ if not shuliang:
844
+ return jsonify({"status": "error", "message": "未提供提取数量参数"})
845
+
846
+ if not leixing or leixing not in ["outlook", "hotmail"]:
847
+ return jsonify(
848
+ {
849
+ "status": "error",
850
+ "message": "提取类型参数无效,必须为 outlook 或 hotmail",
851
+ }
852
+ )
853
+
854
+ # 尝试将数量转换为整数
855
+ try:
856
+ shuliang_int = int(shuliang)
857
+ if shuliang_int < 1 or shuliang_int > 2000:
858
+ return jsonify(
859
+ {"status": "error", "message": "提取数量必须在1到2000之间"}
860
+ )
861
+ except ValueError:
862
+ return jsonify({"status": "error", "message": "提取数量必须为整数"})
863
+
864
+ # 后端重试计数器
865
+ retry_count = 0
866
+ max_retries = 20 # 单次后端请求的最大重试次数
867
+ retry_delay = 0 # 每次重试间隔秒数
868
+
869
+ # 记录总的前端+后端重试次数,用于展示给用户
870
+ total_retry_count = frontend_retry_count
871
+
872
+ while retry_count < max_retries:
873
+ # 发送请求到邮箱提取API
874
+ response = requests.get(
875
+ url="https://zizhu.shanyouxiang.com/huoqu",
876
+ params={"card": card, "shuliang": shuliang, "leixing": leixing},
877
+ headers={
878
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
879
+ },
880
+ timeout=10, # 降低单次请求的超时时间,以便更快地进行重试
881
+ )
882
+
883
+ if response.status_code == 200:
884
+ # 检查响应是否为JSON格式,如果是,通常表示没有库存
885
+ try:
886
+ json_response = response.json()
887
+ if isinstance(json_response, dict) and "msg" in json_response:
888
+ # 没有库存,需要重试
889
+ retry_count += 1
890
+ total_retry_count += 1
891
+
892
+ # 如果达到后端最大重试次数,返回特殊状态让前端继续重试
893
+ if retry_count >= max_retries:
894
+ return jsonify(
895
+ {
896
+ "status": "retry",
897
+ "message": f"暂无库存: {json_response['msg']},已重试{total_retry_count}次,继续尝试中...",
898
+ "retry_count": total_retry_count,
899
+ }
900
+ )
901
+
902
+ # 等待一段时间后重试
903
+ time.sleep(retry_delay)
904
+ continue
905
+ except ValueError:
906
+ # 不是JSON格式,可能是成功的文本列表响应
907
+ pass
908
+
909
+ # 处理文本响应
910
+ response_text = response.text.strip()
911
+
912
+ # 解析响应文本为邮箱列表
913
+ emails = []
914
+ if response_text:
915
+ for line in response_text.split("\n"):
916
+ if line.strip():
917
+ emails.append(line.strip())
918
+
919
+ # 如果没有实际提取到邮箱(可能是空文本响应),继续重试
920
+ if not emails:
921
+ retry_count += 1
922
+ total_retry_count += 1
923
+
924
+ if retry_count >= max_retries:
925
+ return jsonify(
926
+ {
927
+ "status": "retry",
928
+ "message": f"未能获取到邮箱,已重试{total_retry_count}次,继续尝试中...",
929
+ "retry_count": total_retry_count,
930
+ }
931
+ )
932
+
933
+ time.sleep(retry_delay)
934
+ continue
935
+
936
+ # 成功获取到邮箱,返回结果
937
+ return jsonify(
938
+ {
939
+ "status": "success",
940
+ "emails": emails,
941
+ "count": len(emails),
942
+ "retries": total_retry_count,
943
+ "message": f"成功获取{len(emails)}个邮箱,总共重试{total_retry_count}次",
944
+ }
945
+ )
946
+ else:
947
+ # 请求失败,返回错误
948
+ return jsonify(
949
+ {
950
+ "status": "error",
951
+ "message": f"提取邮箱失败: HTTP {response.status_code}",
952
+ "response": response.text,
953
+ }
954
+ )
955
+
956
+ # 如果执行到这里,说明超过了最大重试次数
957
+ return jsonify(
958
+ {
959
+ "status": "retry",
960
+ "message": f"暂无邮箱库存,已重试{total_retry_count}次,继续尝试中...",
961
+ "retry_count": total_retry_count,
962
+ }
963
+ )
964
+
965
+ except Exception as e:
966
+ return jsonify({"status": "error", "message": f"提取邮箱时出错: {str(e)}"})
967
+
968
+
969
+ # --- 新增API:通过EmailClient获取验证码 ---
970
+ @app.route('/api/get_email_verification_code', methods=['POST'])
971
+ def get_email_verification_code_api():
972
+ """
973
+ 通过 EmailClient (通常是基于HTTP API的邮件服务) 获取验证码。
974
+ 接收 JSON 或 Form data。
975
+ 必需参数: email, token, client_id
976
+ 可选参数: password, api_base_url, mailbox, code_regex, max_retries, retry_delay
977
+
978
+ 如���EmailClient方法失败,将尝试使用connect_imap作为备用方法。
979
+ 如果用户之前已配置代理,也会使用相同的代理设置。
980
+ """
981
+ global max_retries, retry_delay
982
+ if request.is_json:
983
+ data = request.get_json()
984
+ else:
985
+ data = request.form
986
+
987
+ email = data.get('email')
988
+ token = data.get('token') # 对应 EmailClient 的 refresh_token
989
+ client_id = data.get('client_id')
990
+
991
+ if not all([email, token, client_id]):
992
+ return jsonify({"status": "error", "message": "缺少必需参数: email, token, client_id"}), 400
993
+
994
+ # 获取可选参数
995
+ password = data.get('password')
996
+ api_base_url = data.get('api_base_url') # 如果提供,将覆盖 EmailClient 的默认设置
997
+ mailbox = data.get('mailbox', "INBOX")
998
+ code_regex = data.get('code_regex', r'\b\d{6}\b') # 默认匹配6位数字
999
+
1000
+ # 检查是否在用户处理数据中有该邮箱,并提取代理设置
1001
+ use_proxy = False
1002
+ proxy_url = None
1003
+ if email in user_process_data:
1004
+ user_data = user_process_data.get(email, {})
1005
+ use_proxy = user_data.get("use_proxy", False)
1006
+ proxy_url = user_data.get("proxy_url", "") if use_proxy else None
1007
+ logger.info(f"为邮箱 {email} 使用代理设置: {use_proxy}, {proxy_url}")
1008
+
1009
+ try:
1010
+ # 实例化 EmailClient,传入代理设置
1011
+ email_client = EmailClient(api_base_url=api_base_url)
1012
+
1013
+ # 设置代理(如果 EmailClient 类支持代理配置)
1014
+ if use_proxy and proxy_url and hasattr(email_client, 'set_proxy'):
1015
+ email_client.set_proxy(proxy_url)
1016
+ elif use_proxy and proxy_url:
1017
+ logger.warning("EmailClient 类不支持设置代理")
1018
+
1019
+ # 使用重试机制调用获取验证码的方法
1020
+ verification_code = retry_function(
1021
+ email_client.get_verification_code,
1022
+ token=token,
1023
+ client_id=client_id,
1024
+ email=email,
1025
+ password=password,
1026
+ mailbox=mailbox,
1027
+ code_regex=code_regex,
1028
+ max_retries=max_retries,
1029
+ delay=retry_delay
1030
+ )
1031
+
1032
+ if verification_code:
1033
+ return jsonify({"status": "success", "verification_code": verification_code})
1034
+ else:
1035
+ # EmailClient 失败,尝试使用connect_imap作为备用方法
1036
+ logger.info(f"EmailClient在{max_retries}次尝试后未能找到验证码,尝试使用connect_imap作为备用方法")
1037
+
1038
+ # 检查是否有password参数
1039
+ if not password:
1040
+ return jsonify({"status": "error", "msg": "EmailClient失败,且未提供password参数,无法使用备用方法"}), 200
1041
+
1042
+ # 先尝试从收件箱获取验证码,传入代理设置
1043
+ result = connect_imap(email, password, "INBOX", use_proxy=use_proxy, proxy_url=proxy_url)
1044
+
1045
+ # 如果收件箱没有找到验证码,则尝试从垃圾邮件中查找
1046
+ if result["code"] == 0:
1047
+ result = connect_imap(email, password, "Junk", use_proxy=use_proxy, proxy_url=proxy_url)
1048
+
1049
+ logger.info(f"catch 当前Oauth登录失败,IMAP结果如下:{result['msg']}")
1050
+ result["msg"] = f"当前Oauth登录失败,IMAP结果如下:{result['msg']}"
1051
+ if result["code"] == 0:
1052
+ return jsonify({"status": "error", "msg": "收件箱和垃圾邮件中均未找到验证码"}), 200
1053
+ elif result["code"] == 200:
1054
+ return jsonify({"status": "success", "verification_code": result["verification_code"], "msg": result["msg"]})
1055
+ else:
1056
+ return jsonify({"status": "error", "msg": result["msg"]}), 200
1057
+
1058
+ except Exception as e:
1059
+ # 捕获实例化或调用过程中的其他潜在错误
1060
+ logger.error(f"处理 /api/get_email_verification_code 时出错: {str(e)}")
1061
+ import traceback
1062
+ logger.error(traceback.format_exc())
1063
+
1064
+ # 如果有password参数,尝试使用connect_imap作为备用方法
1065
+ if password:
1066
+ logger.info(f"EmailClient出现异常,尝试使用connect_imap作为备用方法")
1067
+ try:
1068
+ # 先尝试从收件箱获取验证码,传入代理设置
1069
+ result = connect_imap(email, password, "INBOX", use_proxy=use_proxy, proxy_url=proxy_url)
1070
+
1071
+ # 如果收件箱没有找到验证码,则尝试从垃圾邮件中查找
1072
+ if result["code"] == 0:
1073
+ result = connect_imap(email, password, "Junk", use_proxy=use_proxy, proxy_url=proxy_url)
1074
+
1075
+ logger.info(f"catch 当前Oauth登录失败,IMAP结果如下:{result['msg']}")
1076
+ result["msg"] = f"当前Oauth登录失败,IMAP结果如下:{result['msg']}"
1077
+ if result["code"] == 0:
1078
+ return jsonify({"status": "error", "msg": "收件箱和垃圾邮件中均未找到验证���"}), 200
1079
+ elif result["code"] == 200:
1080
+ return jsonify({"status": "success", "verification_code": result["verification_code"], "msg": result["msg"]})
1081
+ else:
1082
+ return jsonify({"status": "error", "msg": result["msg"]}), 200
1083
+ except Exception as backup_error:
1084
+ logger.error(f"备用方法connect_imap也失败: {str(backup_error)}")
1085
+ return jsonify({"status": "error", "message": f"主要和备用验证码获取方法均出现错误"}), 500
1086
+
1087
+ return jsonify({"status": "error", "message": f"处理请求时发生内部错误"}), 500
1088
+
1089
+
1090
+
1091
+ # 处理所有前端路由
1092
+ @app.route('/', defaults={'path': ''})
1093
+ @app.route('/<path:path>')
1094
+ def serve(path):
1095
+ #favicon vite.svg
1096
+ if path == 'favicon.ico' or path == 'vite.svg':
1097
+ return send_from_directory("static", path)
1098
+ # 对于所有其他请求 - 返回index.html (SPA入口点)
1099
+ return render_template('index.html')
1100
+
1101
+ if __name__ == "__main__":
1102
+ app.run(debug=False, host="0.0.0.0", port=5000)
utils/__pycache__/email_client.cpython-311.pyc ADDED
Binary file (16 kB). View file
 
utils/__pycache__/pikpak.cpython-311.pyc ADDED
Binary file (39.9 kB). View file
 
utils/__pycache__/pk_email.cpython-311.pyc ADDED
Binary file (5.22 kB). View file
 
utils/email_client.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import re
3
+ import json
4
+ import logging
5
+ import os
6
+ from typing import Dict, List, Optional, Any
7
+ from dotenv import load_dotenv
8
+
9
+ # 加载环境变量,强制覆盖已存在的环境变量
10
+ load_dotenv(override=True)
11
+
12
+ # 配置日志
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
16
+ )
17
+ logger = logging.getLogger('email_client')
18
+
19
+ # 添加一条日志,显示加载的环境变量值(如果存在)
20
+ mail_api_url = os.getenv('MAIL_POINT_API_URL', '')
21
+ logger.info(f"加载的MAIL_POINT_API_URL环境变量值: {mail_api_url}")
22
+
23
+ class EmailClient:
24
+ """邮件客户端类,封装邮件API操作"""
25
+
26
+ def __init__(self, api_base_url: Optional[str] = None, use_proxy: bool = False, proxy_url: Optional[str] = None):
27
+ """
28
+ 初始化邮件客户端
29
+
30
+ Args:
31
+ api_base_url: API基础URL,如不提供则从环境变量MAIL_POINT_API_URL读取
32
+ use_proxy: 是否使用代理
33
+ proxy_url: 代理服务器URL (例如 "http://127.0.0.1:7890")
34
+ """
35
+ if api_base_url is None:
36
+ # 添加调试信息,查看API_URL是否正确加载
37
+ api_base_url = os.getenv('MAIL_POINT_API_URL', '')
38
+ logger.info(f"使用的MAIL_POINT_API_URL环境变量值: {api_base_url}")
39
+
40
+ self.api_base_url = api_base_url.rstrip('/')
41
+ self.session = requests.Session()
42
+
43
+ # 初始化代理设置
44
+ self.use_proxy = use_proxy
45
+ self.proxy_url = proxy_url
46
+
47
+ # 如果启用代理,设置代理
48
+ if self.use_proxy and self.proxy_url:
49
+ self.set_proxy(self.proxy_url)
50
+
51
+ def set_proxy(self, proxy_url: str) -> None:
52
+ """
53
+ 设置代理服务器
54
+
55
+ Args:
56
+ proxy_url: 代理服务器URL (例如 "http://127.0.0.1:7890")
57
+ """
58
+ if not proxy_url:
59
+ logger.warning("代理URL为空,不设置代理")
60
+ return
61
+
62
+ # 为会话设置代理
63
+ self.proxy_url = proxy_url
64
+ self.use_proxy = True
65
+
66
+ # 设置代理,支持HTTP和HTTPS
67
+ proxies = {
68
+ "http": proxy_url,
69
+ "https": proxy_url
70
+ }
71
+ self.session.proxies.update(proxies)
72
+ logger.info(f"已设置代理: {proxy_url}")
73
+
74
+ def _make_request(self, endpoint: str, method: str = "POST", **params) -> Dict[str, Any]:
75
+ """
76
+ 发送API请求
77
+
78
+ Args:
79
+ endpoint: API端点
80
+ method: 请求方法,GET或POST
81
+ **params: 请求参数
82
+
83
+ Returns:
84
+ API响应的JSON数据
85
+ """
86
+ url = f"{self.api_base_url}{endpoint}"
87
+
88
+ try:
89
+ if method.upper() == "GET":
90
+ response = self.session.get(url, params=params)
91
+ else: # POST
92
+ response = self.session.post(url, json=params)
93
+
94
+ response.raise_for_status()
95
+ return response.json()
96
+ except requests.RequestException as e:
97
+ logger.error(f"API请求失败: {str(e)}")
98
+ return {"error": str(e), "status": "failed"}
99
+
100
+ def get_latest_email(self, refresh_token: str, client_id: str, email: str,
101
+ mailbox: str = "INBOX", response_type: str = "json",
102
+ password: Optional[str] = None) -> Dict[str, Any]:
103
+ """
104
+ 获取最新一封邮件
105
+
106
+ Args:
107
+ refresh_token: 刷新令牌
108
+ client_id: 客户端ID
109
+ email: 邮箱地址
110
+ mailbox: 邮箱文件夹,INBOX或Junk
111
+ response_type: 返回格式,json或html
112
+ password: 可选密码
113
+
114
+ Returns:
115
+ 包含最新邮件信息的字典
116
+ """
117
+ params = {
118
+ 'refresh_token': refresh_token,
119
+ 'client_id': client_id,
120
+ 'email': email,
121
+ 'mailbox': mailbox,
122
+ 'response_type': response_type
123
+ }
124
+
125
+ if password:
126
+ params['password'] = password
127
+
128
+ return self._make_request('/api/mail-new', **params)
129
+
130
+ def get_all_emails(self, refresh_token: str, client_id: str, email: str,
131
+ mailbox: str = "INBOX", password: Optional[str] = None) -> Dict[str, Any]:
132
+ """
133
+ 获取全部邮件
134
+
135
+ Args:
136
+ refresh_token: 刷新令牌
137
+ client_id: 客户端ID
138
+ email: 邮箱地址
139
+ mailbox: 邮箱文件夹,INBOX或Junk
140
+ password: 可选密码
141
+
142
+ Returns:
143
+ 包含所有邮件信息的字典
144
+ """
145
+ params = {
146
+ 'refresh_token': refresh_token,
147
+ 'client_id': client_id,
148
+ 'email': email,
149
+ 'mailbox': mailbox
150
+ }
151
+
152
+ if password:
153
+ params['password'] = password
154
+
155
+ return self._make_request('/api/mail-all', **params)
156
+
157
+ def process_inbox(self, refresh_token: str, client_id: str, email: str,
158
+ password: Optional[str] = None) -> Dict[str, Any]:
159
+ """
160
+ 清空收件箱
161
+
162
+ Args:
163
+ refresh_token: 刷新令牌
164
+ client_id: 客户端ID
165
+ email: 邮箱地址
166
+ password: 可选密码
167
+
168
+ Returns:
169
+ 操作结果字典
170
+ """
171
+ params = {
172
+ 'refresh_token': refresh_token,
173
+ 'client_id': client_id,
174
+ 'email': email
175
+ }
176
+
177
+ if password:
178
+ params['password'] = password
179
+
180
+ return self._make_request('/api/process-inbox', **params)
181
+
182
+ def process_junk(self, refresh_token: str, client_id: str, email: str,
183
+ password: Optional[str] = None) -> Dict[str, Any]:
184
+ """
185
+ 清空垃圾箱
186
+
187
+ Args:
188
+ refresh_token: 刷新令牌
189
+ client_id: 客户端ID
190
+ email: 邮箱地址
191
+ password: 可选密码
192
+
193
+ Returns:
194
+ 操作结果字典
195
+ """
196
+ params = {
197
+ 'refresh_token': refresh_token,
198
+ 'client_id': client_id,
199
+ 'email': email
200
+ }
201
+
202
+ if password:
203
+ params['password'] = password
204
+
205
+ return self._make_request('/api/process-junk', **params)
206
+
207
+ def send_email(self, refresh_token: str, client_id: str, email: str, to: str,
208
+ subject: str, text: Optional[str] = None, html: Optional[str] = None,
209
+ send_password: Optional[str] = None) -> Dict[str, Any]:
210
+ """
211
+ 发送邮件
212
+
213
+ Args:
214
+ refresh_token: 刷新令牌
215
+ client_id: 客户端ID
216
+ email: 发件人邮箱地址
217
+ to: 收件人邮箱地址
218
+ subject: 邮件主题
219
+ text: 邮件的纯文本内容(与html二选一)
220
+ html: 邮件的HTML内容(与text二选一)
221
+ send_password: 可选发送密码
222
+
223
+ Returns:
224
+ 操作结果字典
225
+ """
226
+ if not text and not html:
227
+ raise ValueError("必须提供text或html参数")
228
+
229
+ params = {
230
+ 'refresh_token': refresh_token,
231
+ 'client_id': client_id,
232
+ 'email': email,
233
+ 'to': to,
234
+ 'subject': subject
235
+ }
236
+
237
+ if text:
238
+ params['text'] = text
239
+ if html:
240
+ params['html'] = html
241
+ if send_password:
242
+ params['send_password'] = send_password
243
+
244
+ return self._make_request('/api/send-mail', **params)
245
+
246
+ def get_verification_code(self, token: str, client_id: str, email: str,
247
+ password: Optional[str] = None, mailbox: str = "INBOX",
248
+ code_regex: str = r'\\b\\d{6}\\b') -> Optional[str]:
249
+ """
250
+ 获取最新邮件中的验证码
251
+
252
+ Args:
253
+ token: 刷新令牌 (对应API的refresh_token)
254
+ client_id: 客户端ID
255
+ email: 邮箱地址
256
+ password: 可选密码
257
+ mailbox: 邮箱文件夹,INBOX或Junk (默认为INBOX)
258
+ code_regex: 用于匹配验证码的正则表达式 (默认为匹配6位数字)
259
+
260
+ Returns:
261
+ 找到的验证码字符串,如果未找到或出错则返回None
262
+ """
263
+ logger.info(f"尝试从邮箱 {email} 的 {mailbox} 获取验证码")
264
+
265
+ # 调用 get_latest_email 获取邮件内容, 先从INBOX获取
266
+ latest_email_data = self.get_latest_email(
267
+ refresh_token=token,
268
+ client_id=client_id,
269
+ email=email,
270
+ mailbox="INBOX",
271
+ response_type='json', # 需要JSON格式来解析内容
272
+ password=password
273
+ )
274
+
275
+ if not latest_email_data or (latest_email_data.get('send') is not None and isinstance(latest_email_data.get('send'), str) and 'PikPak' not in latest_email_data.get('send')):
276
+ logger.error(f"在 INBOX 获取邮箱 {email} 最新邮件失败,尝试从Junk获取")
277
+ latest_email_data = self.get_latest_email(
278
+ refresh_token=token,
279
+ client_id=client_id,
280
+ email=email,
281
+ mailbox="Junk",
282
+ )
283
+
284
+ logger.info(f"Junk latest_email_data: {latest_email_data.get('send')}")
285
+ if not latest_email_data or (latest_email_data.get('send') is not None and isinstance(latest_email_data.get('send'), str) and 'PikPak' not in latest_email_data.get('send')):
286
+ logger.error(f"在 Junk 获取邮箱 {email} 最新邮件失败")
287
+ return None
288
+
289
+ # 假设邮件正文在 'text' 或 'body' 字段
290
+ email_content = latest_email_data.get('text') or latest_email_data.get('body')
291
+
292
+ if not email_content:
293
+ logger.warning(f"邮箱 {email} 的最新邮件数据中未找到 'text' 或 'body' 字段")
294
+ return None
295
+
296
+ # 使用正则表达式搜索验证码
297
+ try:
298
+ match = re.search(code_regex, email_content)
299
+ if match:
300
+ verification_code = match.group(0) # 通常验证码是整个匹配项
301
+ logger.info(f"在邮箱 {email} 的邮件中成功找到验证码: {verification_code}")
302
+ return verification_code
303
+ else:
304
+ logger.info(f"在邮箱 {email} 的最新邮件中未找到符合模式 {code_regex} 的验证码")
305
+ return None
306
+ except re.error as e:
307
+ logger.error(f"提供的正则表达式 '{code_regex}' 无效: {e}")
308
+ return None
309
+ except Exception as e:
310
+ logger.error(f"解析邮件内容或匹配验证码时发生未知错误: {e}")
311
+ return None
312
+
313
+ def parse_email_credentials(credentials_str: str) -> List[Dict[str, str]]:
314
+ """
315
+ 解析邮箱凭证字符串,提取邮箱、密码、Client ID和Token
316
+
317
+ Args:
318
+ credentials_str: 包含凭证信息的字符串
319
+
320
+ Returns:
321
+ 凭证列表,每个凭证为一个字典
322
+ """
323
+ credentials_list = []
324
+ pattern = r'(.+?)----(.+?)----(.+?)----(.+?)(?:\n|$)'
325
+ matches = re.finditer(pattern, credentials_str.strip())
326
+
327
+ for match in matches:
328
+ if len(match.groups()) == 4:
329
+ email, password, client_id, token = match.groups()
330
+ credentials_list.append({
331
+ 'email': email.strip(),
332
+ 'password': password.strip(),
333
+ 'client_id': client_id.strip(),
334
+ 'token': token.strip()
335
+ })
336
+
337
+ return credentials_list
338
+
339
+ def load_credentials_from_file(file_path: str) -> str:
340
+ """
341
+ 从文件加载凭证信息
342
+
343
+ Args:
344
+ file_path: 文件路径
345
+
346
+ Returns:
347
+ 包含凭证的字符串
348
+ """
349
+ try:
350
+ with open(file_path, 'r', encoding='utf-8') as file:
351
+ content = file.read()
352
+ # 提取多行字符串v的内容
353
+ match = re.search(r'v\s*=\s*"""(.*?)"""', content, re.DOTALL)
354
+ if match:
355
+ return match.group(1)
356
+ return ""
357
+ except Exception as e:
358
+ logger.error(f"加载凭证文件失败: {str(e)}")
359
+ return ""
360
+
361
+ def format_json_output(json_data: Dict) -> str:
362
+ """
363
+ 格式化JSON输出
364
+
365
+ Args:
366
+ json_data: JSON数据
367
+
368
+ Returns:
369
+ 格式化后的字符串
370
+ """
371
+ return json.dumps(json_data, ensure_ascii=False, indent=2)
utils/pikpak.py ADDED
@@ -0,0 +1,876 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding:utf-8
2
+ import hashlib
3
+ import json
4
+ import random
5
+ import time
6
+ import uuid
7
+ import requests
8
+ from PIL import Image
9
+ from io import BytesIO
10
+ import base64
11
+ import os
12
+
13
+
14
+ def ca_f_encrypt(frames, index, pid, use_proxy=False, proxies=None):
15
+ url = "https://api.kiteyuan.info/cafEncrypt"
16
+
17
+ payload = json.dumps({
18
+ "frames": frames,
19
+ "index": index,
20
+ "pid": pid
21
+ })
22
+ headers = {
23
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
24
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
25
+ "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
26
+ "Connection": "keep-alive",
27
+ 'Content-Type': 'application/json',
28
+ "Upgrade-Insecure-Requests": "1",
29
+ "Sec-Fetch-Dest": "document",
30
+ "Sec-Fetch-Mode": "navigate",
31
+ "Sec-Fetch-Site": "none",
32
+ "Sec-Fetch-User": "?1"
33
+ }
34
+
35
+ try:
36
+ # response = requests.request("POST", url, headers=headers, data=payload, proxies=proxies if use_proxy else None)
37
+ response = requests.request("POST", url, headers=headers, data=payload, proxies=None)
38
+ response.raise_for_status()
39
+ if not response.text:
40
+ print(f"API响应为空: {url}")
41
+ return {"f": "", "ca": ["", "", "", ""]}
42
+
43
+ # 解析响应以确保与 text.py 行为一致
44
+ result = json.loads(response.text)
45
+ if "f" not in result or "ca" not in result:
46
+ print(f"API响应缺少关键字段: {result}")
47
+ return {"f": "", "ca": ["", "", "", ""]}
48
+ return result
49
+ except requests.exceptions.RequestException as e:
50
+ print(f"API请求失败: {e}")
51
+ if use_proxy:
52
+ print(f"当前使用的代理: {proxies}")
53
+ return {"f": "", "ca": ["", "", "", ""]}
54
+ except json.JSONDecodeError as e:
55
+ print(f"JSON解析错误: {e}, 响应内容: {response.text}")
56
+ return {"f": "", "ca": ["", "", "", ""]}
57
+
58
+
59
+ def image_parse(image, frames, use_proxy=False, proxies=None):
60
+ url = "https://api.kiteyuan.info/imageParse"
61
+
62
+ payload = json.dumps({
63
+ "image": image,
64
+ "frames": frames
65
+ })
66
+ headers = {
67
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
68
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
69
+ "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
70
+ "Connection": "keep-alive",
71
+ 'Content-Type': 'application/json',
72
+ "Upgrade-Insecure-Requests": "1",
73
+ "Sec-Fetch-Dest": "document",
74
+ "Sec-Fetch-Mode": "navigate",
75
+ "Sec-Fetch-Site": "none",
76
+ "Sec-Fetch-User": "?1"
77
+ }
78
+
79
+ try:
80
+ # response = requests.request("POST", url, headers=headers, data=payload, proxies=proxies if use_proxy else None)
81
+ response = requests.request("POST", url, headers=headers, data=payload, proxies=None)
82
+ response.raise_for_status() # 检查HTTP状态码
83
+ if not response.text:
84
+ print(f"API响应为空: {url}")
85
+ return {"best_index": 0} # 返回一个默认值
86
+
87
+ # 解析响应以确保与 text.py 行为一致
88
+ result = json.loads(response.text)
89
+ if "best_index" not in result:
90
+ print(f"API响应缺少best_index字段: {result}")
91
+ return {"best_index": 0}
92
+ return result
93
+ except requests.exceptions.RequestException as e:
94
+ print(f"API请求失败: {e}")
95
+ if use_proxy:
96
+ print(f"当前使用的代理: {proxies}")
97
+ return {"best_index": 0} # 返回一个默认值
98
+ except json.JSONDecodeError as e:
99
+ print(f"JSON解析错误: {e}, 响应内容: {response.text}")
100
+ return {"best_index": 0} # 返回一个默认值
101
+
102
+
103
+ def sign_encrypt(code, captcha_token, rtc_token, use_proxy=False, proxies=None):
104
+ url = "https://api.kiteyuan.info/signEncrypt"
105
+
106
+ # 检查 code 是否为空或 None
107
+ if not code:
108
+ print("code 参数为空,无法进行加密")
109
+ return {"request_id": "", "sign": ""}
110
+
111
+ # 如果 code 是字符串而不是对象,则直接使用
112
+ if isinstance(code, str):
113
+ payload_data = code
114
+ else:
115
+ try:
116
+ payload_data = json.dumps(code)
117
+ except (TypeError, ValueError) as e:
118
+ print(f"code 参数序列化失败: {e}")
119
+ return {"request_id": "", "sign": ""}
120
+
121
+ try:
122
+ payload = json.dumps({
123
+ "code": payload_data,
124
+ "captcha_token": captcha_token,
125
+ "rtc_token": rtc_token
126
+ })
127
+ headers = {
128
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
129
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
130
+ "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
131
+ "Connection": "keep-alive",
132
+ 'Content-Type': 'application/json',
133
+ "Upgrade-Insecure-Requests": "1",
134
+ "Sec-Fetch-Dest": "document",
135
+ "Sec-Fetch-Mode": "navigate",
136
+ "Sec-Fetch-Site": "none",
137
+ "Sec-Fetch-User": "?1"
138
+ }
139
+
140
+ # response = requests.request("POST", url, headers=headers, data=payload, proxies=proxies if use_proxy else None, timeout=30)
141
+ response = requests.request("POST", url, headers=headers, data=payload, proxies=None, timeout=30)
142
+ response.raise_for_status()
143
+ if not response.text:
144
+ print(f"API响应为空: {url}")
145
+ return {"request_id": "", "sign": ""}
146
+
147
+ # 解析响应以确保与 text.py 行为一致
148
+ result = json.loads(response.text)
149
+ if "request_id" not in result or "sign" not in result:
150
+ print(f"API响应缺少关键字段: {result}")
151
+ return {"request_id": "", "sign": ""}
152
+ return result
153
+ except requests.exceptions.RequestException as e:
154
+ print(f"API请求失败: {e}")
155
+ if use_proxy:
156
+ print(f"当前使用的代理: {proxies}")
157
+ return {"request_id": "", "sign": ""}
158
+ except json.JSONDecodeError as e:
159
+ print(f"JSON解析错误: {e}, 响应内容: {response.text}")
160
+ return {"request_id": "", "sign": ""}
161
+ except Exception as e:
162
+ print(f"未知错误: {e}")
163
+ return {"request_id": "", "sign": ""}
164
+
165
+
166
+ def d_encrypt(pid, device_id, f, use_proxy=False, proxies=None):
167
+ url = "https://api.kiteyuan.info/dEncrypt"
168
+
169
+ payload = json.dumps({
170
+ "pid": pid,
171
+ "device_id": device_id,
172
+ "f": f
173
+ })
174
+ headers = {
175
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
176
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
177
+ "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
178
+ "Connection": "keep-alive",
179
+ 'Content-Type': 'application/json',
180
+ "Upgrade-Insecure-Requests": "1",
181
+ "Sec-Fetch-Dest": "document",
182
+ "Sec-Fetch-Mode": "navigate",
183
+ "Sec-Fetch-Site": "none",
184
+ "Sec-Fetch-User": "?1"
185
+ }
186
+
187
+ try:
188
+ # response = requests.request("POST", url, headers=headers, data=payload, proxies=proxies if use_proxy else None)
189
+ response = requests.request("POST", url, headers=headers, data=payload, proxies=None)
190
+ response.raise_for_status()
191
+ if not response.text:
192
+ print(f"API响应为空: {url}")
193
+ return ""
194
+ return response.text
195
+ except requests.exceptions.RequestException as e:
196
+ print(f"API请求失败: {e}")
197
+ if use_proxy:
198
+ print(f"当前使用的代理: {proxies}")
199
+ return ""
200
+
201
+
202
+ # md5加密算法
203
+ def captcha_sign_encrypt(encrypt_string, salts):
204
+ for salt in salts:
205
+ encrypt_string = hashlib.md5((encrypt_string + salt["salt"]).encode("utf-8")).hexdigest()
206
+ return encrypt_string
207
+
208
+
209
+ def captcha_image_parse(pikpak, device_id):
210
+ try:
211
+ # 获取frames信息
212
+ frames_info = pikpak.gen()
213
+ if not frames_info or not isinstance(frames_info, dict) or "pid" not in frames_info or "frames" not in frames_info:
214
+ print("获取frames_info失败,返回内容:", frames_info)
215
+ return {"response_data": {"result": "reject"}, "pid": "", "traceid": ""}
216
+
217
+ if "traceid" not in frames_info:
218
+ frames_info["traceid"] = ""
219
+
220
+ # 下载验证码图片
221
+ captcha_image = image_download(device_id, frames_info["pid"], frames_info["traceid"], pikpak.use_proxy, pikpak.proxies)
222
+ if not captcha_image:
223
+ print("图片下载失败")
224
+ return {"response_data": {"result": "reject"}, "pid": frames_info["pid"], "traceid": frames_info["traceid"]}
225
+
226
+ # 读取图片数据并转换为 PIL.Image
227
+ img = Image.open(BytesIO(captcha_image))
228
+
229
+ # 将图片转换为 Base64 编码
230
+ buffered = BytesIO()
231
+ img.save(buffered, format="PNG") # 可根据图片格式调整 format
232
+ base64_image = base64.b64encode(buffered.getvalue()).decode()
233
+
234
+ # 获取最佳滑块位置
235
+ best_index = image_parse(base64_image, frames_info["frames"], pikpak.use_proxy, pikpak.proxies)
236
+ if "best_index" not in best_index:
237
+ print("图片分析失败, 返回内容:", best_index)
238
+ return {"response_data": {"result": "reject"}, "pid": frames_info["pid"], "traceid": frames_info["traceid"]}
239
+
240
+ # 滑块加密
241
+ json_data = ca_f_encrypt(frames_info["frames"], best_index["best_index"], frames_info["pid"], pikpak.use_proxy, pikpak.proxies)
242
+ if "f" not in json_data or "ca" not in json_data:
243
+ print("加密计算失败, 返回内容:", json_data)
244
+ return {"response_data": {"result": "reject"}, "pid": frames_info["pid"], "traceid": frames_info["traceid"]}
245
+
246
+ f = json_data['f']
247
+ npac = json_data['ca']
248
+
249
+ # d加密
250
+ d = d_encrypt(frames_info["pid"], device_id, f, pikpak.use_proxy, pikpak.proxies)
251
+ if not d:
252
+ print("d_encrypt失败")
253
+ return {"response_data": {"result": "reject"}, "pid": frames_info["pid"], "traceid": frames_info["traceid"]}
254
+
255
+ # 验证
256
+ verify2 = pikpak.image_verify(frames_info["pid"], frames_info["traceid"], f, npac[0], npac[1], npac[2], npac[3], d)
257
+
258
+ return {
259
+ "response_data": verify2,
260
+ "pid": frames_info["pid"],
261
+ "traceid": frames_info["traceid"],
262
+ }
263
+ except Exception as e:
264
+ print(f"滑块验证过程中出错: {e}")
265
+ import traceback
266
+ traceback.print_exc()
267
+ return {"response_data": {"result": "reject"}, "pid": "", "traceid": ""}
268
+
269
+
270
+ def image_download(device_id, pid, traceid, use_proxy=False, proxies=None):
271
+ url = f"https://user.mypikpak.com/pzzl/image?deviceid={device_id}&pid={pid}&traceid={traceid}"
272
+
273
+ headers = {
274
+ 'pragma': 'no-cache',
275
+ 'priority': 'u=1, i'
276
+ }
277
+
278
+ try:
279
+ response = requests.get(url, headers=headers, proxies=proxies if use_proxy else None)
280
+ response.raise_for_status()
281
+ if response.status_code == 200:
282
+ return response.content # 直接返回图片的二进制数据
283
+ else:
284
+ print(f"下载失败,状态码: {response.status_code}")
285
+ return None
286
+ except requests.exceptions.RequestException as e:
287
+ print(f"图片下载失败: {e}")
288
+ if use_proxy:
289
+ print(f"当前使用的代理: {proxies}")
290
+ return None
291
+
292
+
293
+ def ramdom_version():
294
+ version_list = [
295
+ {
296
+ "v": "1.42.6",
297
+ "algorithms": [{"alg": "md5", "salt": "frupTFdxwcJ5mcL3R8"},
298
+ {"alg": "md5", "salt": "jB496fSFfbWLhWyqV"},
299
+ {"alg": "md5", "salt": "xYLtzn8LT5h3KbAalCjc/Wf"},
300
+ {"alg": "md5", "salt": "PSHSbm1SlxbvkwNk4mZrJhBZ1vsHCtEdm3tsRiy1IPUnqi1FNB5a2F"},
301
+ {"alg": "md5", "salt": "SX/WvPCRzgkLIp99gDnLaCs0jGn2+urx7vz/"},
302
+ {"alg": "md5", "salt": "OGdm+dgLk5EpK4O1nDB+Z4l"},
303
+ {"alg": "md5", "salt": "nwtOQpz2xFLIE3EmrDwMKe/Vlw2ubhRcnS2R23bwx9wMh+C3Sg"},
304
+ {"alg": "md5", "salt": "FI/9X9jbnTLa61RHprndT0GkVs18Chd"}]
305
+
306
+ },
307
+ {
308
+ "v": "1.47.1",
309
+ "algorithms": [{'alg': 'md5', 'salt': 'Gez0T9ijiI9WCeTsKSg3SMlx'}, {'alg': 'md5', 'salt': 'zQdbalsolyb1R/'},
310
+ {'alg': 'md5', 'salt': 'ftOjr52zt51JD68C3s'},
311
+ {'alg': 'md5', 'salt': 'yeOBMH0JkbQdEFNNwQ0RI9T3wU/v'},
312
+ {'alg': 'md5', 'salt': 'BRJrQZiTQ65WtMvwO'},
313
+ {'alg': 'md5', 'salt': 'je8fqxKPdQVJiy1DM6Bc9Nb1'},
314
+ {'alg': 'md5', 'salt': 'niV'}, {'alg': 'md5', 'salt': '9hFCW2R1'},
315
+ {'alg': 'md5', 'salt': 'sHKHpe2i96'},
316
+ {'alg': 'md5', 'salt': 'p7c5E6AcXQ/IJUuAEC9W6'}, {'alg': 'md5', 'salt': ''},
317
+ {'alg': 'md5', 'salt': 'aRv9hjc9P+Pbn+u3krN6'},
318
+ {'alg': 'md5', 'salt': 'BzStcgE8qVdqjEH16l4'},
319
+ {'alg': 'md5', 'salt': 'SqgeZvL5j9zoHP95xWHt'},
320
+ {'alg': 'md5', 'salt': 'zVof5yaJkPe3VFpadPof'}]
321
+ },
322
+ {
323
+ "v": "1.48.3",
324
+ "algorithms": [{'alg': 'md5', 'salt': 'aDhgaSE3MsjROCmpmsWqP1sJdFJ'},
325
+ {'alg': 'md5', 'salt': '+oaVkqdd8MJuKT+uMr2AYKcd9tdWge3XPEPR2hcePUknd'},
326
+ {'alg': 'md5', 'salt': 'u/sd2GgT2fTytRcKzGicHodhvIltMntA3xKw2SRv7S48OdnaQIS5mn'},
327
+ {'alg': 'md5', 'salt': '2WZiae2QuqTOxBKaaqCNHCW3olu2UImelkDzBn'},
328
+ {'alg': 'md5', 'salt': '/vJ3upic39lgmrkX855Qx'},
329
+ {'alg': 'md5', 'salt': 'yNc9ruCVMV7pGV7XvFeuLMOcy1'},
330
+ {'alg': 'md5', 'salt': '4FPq8mT3JQ1jzcVxMVfwFftLQm33M7i'},
331
+ {'alg': 'md5', 'salt': 'xozoy5e3Ea'}]
332
+ },
333
+ {
334
+ "v": "1.49.3",
335
+ "algorithms": [{'alg': 'md5', 'salt': '7xOq4Z8s'}, {'alg': 'md5', 'salt': 'QE9/9+IQco'},
336
+ {'alg': 'md5', 'salt': 'WdX5J9CPLZp'}, {'alg': 'md5', 'salt': 'NmQ5qFAXqH3w984cYhMeC5TJR8j'},
337
+ {'alg': 'md5', 'salt': 'cc44M+l7GDhav'}, {'alg': 'md5', 'salt': 'KxGjo/wHB+Yx8Lf7kMP+/m9I+'},
338
+ {'alg': 'md5', 'salt': 'wla81BUVSmDkctHDpUT'},
339
+ {'alg': 'md5', 'salt': 'c6wMr1sm1WxiR3i8LDAm3W'},
340
+ {'alg': 'md5', 'salt': 'hRLrEQCFNYi0PFPV'},
341
+ {'alg': 'md5', 'salt': 'o1J41zIraDtJPNuhBu7Ifb/q3'},
342
+ {'alg': 'md5', 'salt': 'U'}, {'alg': 'md5', 'salt': 'RrbZvV0CTu3gaZJ56PVKki4IeP'},
343
+ {'alg': 'md5', 'salt': 'NNuRbLckJqUp1Do0YlrKCUP'},
344
+ {'alg': 'md5', 'salt': 'UUwnBbipMTvInA0U0E9'},
345
+ {'alg': 'md5', 'salt': 'VzGc'}]
346
+ },
347
+ {
348
+ "v": "1.51.2",
349
+ "algorithms": [{'alg': 'md5', 'salt': 'vPjelkvqcWoCsQO1CnkVod8j2GbcE0yEHEwJ3PKSKW'},
350
+ {'alg': 'md5', 'salt': 'Rw5aO9MHuhY'}, {'alg': 'md5', 'salt': 'Gk111qdZkPw/xgj'},
351
+ {'alg': 'md5', 'salt': '/aaQ4/f8HNpyzPOtIF3rG/UEENiRRvpIXku3WDWZHuaIq+0EOF'},
352
+ {'alg': 'md5', 'salt': '6p1gxZhV0CNuKV2QO5vpibkR8IJeFURvqNIKXWOIyv1A'},
353
+ {'alg': 'md5', 'salt': 'gWR'},
354
+ {'alg': 'md5', 'salt': 'iPD'}, {'alg': 'md5', 'salt': 'ASEm+P75YfKzQRW6eRDNNTd'},
355
+ {'alg': 'md5', 'salt': '2fauuwVCxLCpL/FQ/iJ5NpOPb7gRZs0EWJwe/2YNPQr3ore+ZiIri6s/tYayG'}]
356
+ }
357
+ ]
358
+ return version_list[0]
359
+ # return random.choice(version_list)
360
+
361
+
362
+ def random_rtc_token():
363
+ # 生成 8 组 16 进制数,每组 4 位,使用冒号分隔
364
+ ipv6_parts = ["{:04x}".format(random.randint(0, 0xFFFF)) for _ in range(8)]
365
+ ipv6_address = ":".join(ipv6_parts)
366
+ return ipv6_address
367
+
368
+
369
+ class PikPak:
370
+ def __init__(self, invite_code, client_id, device_id, version, algorithms, email, rtc_token,
371
+ client_secret, package_name, use_proxy=False, proxy_http=None, proxy_https=None):
372
+ # 初始化实例属性
373
+ self.invite_code = invite_code # 邀请码
374
+ self.client_id = client_id # 客户端ID
375
+ self.device_id = device_id # 设备ID
376
+ self.timestamp = 0 # 时间戳
377
+ self.algorithms = algorithms # 版本盐值
378
+ self.version = version # 版本
379
+ self.email = email # 邮箱
380
+ self.rtc_token = rtc_token # RTC Token
381
+ self.captcha_token = "" # Captcha Token
382
+ self.client_secret = client_secret # Client Secret
383
+ self.user_id = "" # 用户ID
384
+ self.access_token = "" # 登录令牌
385
+ self.refresh_token = "" # 刷新令牌
386
+ self.verification_token = "" # Verification Token
387
+ self.captcha_sign = "" # Captcha Sign
388
+ self.verification_id = "" # Verification ID
389
+ self.package_name = package_name # 客户端包名
390
+ self.use_proxy = use_proxy # 是否使用代理
391
+
392
+ # 代理配置
393
+ if use_proxy:
394
+ self.proxies = {
395
+ "http": proxy_http or "http://127.0.0.1:7890",
396
+ "https": proxy_https or "http://127.0.0.1:7890",
397
+ }
398
+ else:
399
+ self.proxies = None
400
+
401
+ def send_request(self, method, url, headers=None, params=None, json_data=None, data=None, use_proxy=None):
402
+ headers = headers or {}
403
+ # 如果未指定use_proxy,则使用类的全局设置
404
+ use_proxy = self.use_proxy if use_proxy is None else use_proxy
405
+
406
+ # 确保当use_proxy为True时,有可用的代理配置
407
+ if use_proxy and not self.proxies:
408
+ # 如果类的use_proxy为True但proxies未设置,使用默认代理
409
+ proxies = {
410
+ "http": "http://127.0.0.1:7890",
411
+ "https": "http://127.0.0.1:7890"
412
+ }
413
+ else:
414
+ proxies = self.proxies if use_proxy else None
415
+
416
+ try:
417
+ response = requests.request(
418
+ method=method,
419
+ url=url,
420
+ headers=headers,
421
+ params=params,
422
+ json=json_data,
423
+ data=data,
424
+ proxies=proxies,
425
+ timeout=30 # 添加超时设置
426
+ )
427
+ response.raise_for_status() # 检查HTTP状态码
428
+
429
+ print(response.text)
430
+ try:
431
+ return response.json()
432
+ except json.JSONDecodeError:
433
+ return response.text
434
+ except requests.exceptions.RequestException as e:
435
+ print(f"请求失败: {url}, 错误: {e}")
436
+ if use_proxy:
437
+ print(f"当前使用的代理: {proxies}")
438
+ # 返回一个空的响应对象
439
+ return {}
440
+
441
+ def gen(self):
442
+ url = "https://user.mypikpak.com/pzzl/gen"
443
+ params = {"deviceid": self.device_id, "traceid": ""}
444
+ headers = {"Host": "user.mypikpak.com", "accept": "application/json, text/plain, */*"}
445
+ response = self.send_request("GET", url, headers=headers, params=params)
446
+ # 检查响应是否有效
447
+ if not response or not isinstance(response, dict) or "pid" not in response or "frames" not in response:
448
+ print(f"gen请求返回无效响应: {response}")
449
+ return response
450
+
451
+ def image_verify(self, pid, trace_id, f, n, p, a, c, d):
452
+ url = "https://user.mypikpak.com/pzzl/verify"
453
+ params = {"pid": pid, "deviceid": self.device_id, "traceid": trace_id, "f": f, "n": n, "p": p, "a": a, "c": c,
454
+ "d": d}
455
+ headers = {"Host": "user.mypikpak.com", "accept": "application/json, text/plain, */*"}
456
+ response = self.send_request("GET", url, headers=headers, params=params)
457
+ # 检查响应是否有效
458
+ if not response or not isinstance(response, dict) or "result" not in response:
459
+ print(f"image_verify请求返回无效响应: {response}")
460
+ return {"result": "reject"}
461
+ return response
462
+
463
+ def executor(self):
464
+ url = "https://api-drive.mypikpak.com/captcha-jsonp/v2/executor?callback=handleJsonpResult_" + str(int(time.time() * 1000))
465
+ headers = {'pragma': 'no-cache', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'}
466
+
467
+ try:
468
+ # 使用普通 requests 而不是 self.send_request 以获取原始响应
469
+ response = requests.get(url, headers=headers, proxies=self.proxies if self.use_proxy else None, timeout=30)
470
+ response.raise_for_status() # 检查 HTTP 状态码
471
+
472
+ content = response.text
473
+ print(f"executor 原始响应: {content}")
474
+
475
+ # 如果内容为空,直接返回空字符串
476
+ if not content:
477
+ print("executor 响应内容为空")
478
+ return ""
479
+
480
+ # 处理 JSONP 响应格式
481
+ if "handleJsonpResult" in content:
482
+ # 提取 JSON 部分,JSONP 格式通常是 callback(json数据)
483
+ start_index = content.find('(')
484
+ end_index = content.rfind(')')
485
+
486
+ if start_index != -1 and end_index != -1:
487
+ json_str = content[start_index + 1:end_index]
488
+ # 有时 JSONP 响应中包含反引号,需要去除
489
+ if json_str.startswith('`') and json_str.endswith('`'):
490
+ json_str = json_str[1:-1]
491
+
492
+ return json_str
493
+ else:
494
+ print(f"无法从JSONP响应中提取有效内容: {content}")
495
+ return ""
496
+ elif isinstance(content, str) and (content.startswith('{') or content.startswith('[')):
497
+ # 可能是直接返回的 JSON 字符串
498
+ return content
499
+ else:
500
+ print(f"未知的响应格式: {content}")
501
+ return ""
502
+ except requests.exceptions.RequestException as e:
503
+ print(f"执行 executor 请求失败: {e}")
504
+ return ""
505
+ except Exception as e:
506
+ print(f"解析 executor 响应失败: {e}")
507
+ return ""
508
+
509
+ def report(self, request_id, sign, pid, trace_id):
510
+ url = "https://user.mypikpak.com/credit/v1/report"
511
+ params = {
512
+ "deviceid": self.device_id,
513
+ "captcha_token": self.captcha_token,
514
+ "request_id": request_id,
515
+ "sign": sign,
516
+ "type": "pzzlSlider",
517
+ "result": 0,
518
+ "data": pid,
519
+ "traceid": trace_id,
520
+ "rtc_token": self.rtc_token
521
+ }
522
+ headers = {'pragma': 'no-cache', 'priority': 'u=1, i'}
523
+ response = self.send_request("GET", url, params=params, headers=headers)
524
+ # 检查响应是否有效
525
+ if not response or not isinstance(response, dict) or "captcha_token" not in response:
526
+ print(f"report请求返回无效响应: {response}")
527
+ else:
528
+ self.captcha_token = response.get('captcha_token')
529
+ return response
530
+
531
+ def verification(self):
532
+ url = 'https://user.mypikpak.com/v1/auth/verification'
533
+ params = {"email": self.email, "target": "ANY", "usage": "REGISTER", "locale": "zh-CN",
534
+ "client_id": self.client_id}
535
+ headers = {'host': 'user.mypikpak.com', 'x-captcha-token': self.captcha_token, 'x-device-id': self.device_id,
536
+ "x-client-id": self.client_id}
537
+ response = self.send_request("POST", url, headers=headers, data=params)
538
+ # 检查响应是否有效
539
+ if not response or not isinstance(response, dict) or "verification_id" not in response:
540
+ print(f"verification请求返回无效响应: {response}")
541
+ else:
542
+ self.verification_id = response.get('verification_id')
543
+ return response
544
+
545
+ def verify_post(self, verification_code):
546
+ url = "https://user.mypikpak.com/v1/auth/verification/verify"
547
+ params = {"client_id": self.client_id}
548
+ payload = {"client_id": self.client_id, "verification_id": self.verification_id,
549
+ "verification_code": verification_code}
550
+ headers = {"X-Device-Id": self.device_id}
551
+ response = self.send_request("POST", url, headers=headers, json_data=payload, params=params)
552
+ # 检查响应是否有效
553
+ if not response or not isinstance(response, dict) or "verification_token" not in response:
554
+ print(f"verify_post请求返回无效响应: {response}")
555
+ else:
556
+ self.verification_token = response.get('verification_token')
557
+ return response
558
+
559
+ def init(self, action):
560
+ self.refresh_captcha_sign()
561
+ url = "https://user.mypikpak.com/v1/shield/captcha/init"
562
+ params = {"client_id": self.client_id}
563
+ payload = {
564
+ "action": action,
565
+ "captcha_token": self.captcha_token,
566
+ "client_id": self.client_id,
567
+ "device_id": self.device_id,
568
+ "meta": {
569
+ "captcha_sign": "1." + self.captcha_sign,
570
+ "user_id": self.user_id,
571
+ "package_name": self.package_name,
572
+ "client_version": self.version,
573
+ "email": self.email,
574
+ "timestamp": self.timestamp
575
+ }
576
+ }
577
+ headers = {"x-device-id": self.device_id}
578
+ response = self.send_request("POST", url, headers=headers, json_data=payload, params=params)
579
+ # 检查响应是否有效
580
+ if not response or not isinstance(response, dict) or "captcha_token" not in response:
581
+ print(f"init请求返回无效响应: {response}")
582
+ else:
583
+ self.captcha_token = response.get('captcha_token')
584
+ return response
585
+
586
+ def signup(self, name, password, verification_code):
587
+ url = "https://user.mypikpak.com/v1/auth/signup"
588
+ params = {"client_id": self.client_id}
589
+ payload = {
590
+ "captcha_token": self.captcha_token,
591
+ "client_id": self.client_id,
592
+ "client_secret": self.client_secret,
593
+ "email": self.email,
594
+ "name": name,
595
+ "password": password,
596
+ "verification_code": verification_code,
597
+ "verification_token": self.verification_token
598
+ }
599
+ headers = {"X-Device-Id": self.device_id}
600
+ response = self.send_request("POST", url, headers=headers, json_data=payload, params=params)
601
+ # 检查响应是否有效
602
+ if not response or not isinstance(response, dict):
603
+ print(f"signup请求返回无效响应: {response}")
604
+ else:
605
+ self.access_token = response.get('access_token', '')
606
+ self.refresh_token = response.get('refresh_token', '')
607
+ self.user_id = response.get('sub', '')
608
+ return response
609
+
610
+ def activation_code(self):
611
+ url = "https://api-drive.mypikpak.com/vip/v1/order/activation-code"
612
+ payload = {"activation_code": self.invite_code, "data": {}}
613
+ headers = {
614
+ "Host": "api-drive.mypikpak.com",
615
+ "authorization": "Bearer " + self.access_token,
616
+ "x-captcha-token": self.captcha_token,
617
+ "x-device-id": self.device_id,
618
+ 'x-system-language': "ko",
619
+ 'content-type': 'application/json'
620
+ }
621
+ response = self.send_request("POST", url, headers=headers, json_data=payload)
622
+ # 检查响应是否有效
623
+ if not response or not isinstance(response, dict):
624
+ print(f"activation_code请求返回无效响应: {response}")
625
+ return response
626
+
627
+ def files_task(self, task_link):
628
+ url = "https://api-drive.mypikpak.com/drive/v1/files"
629
+ payload = {
630
+ "kind": "drive#file",
631
+ "folder_type": "DOWNLOAD",
632
+ "upload_type": "UPLOAD_TYPE_URL",
633
+ "url": {"url": task_link},
634
+ "params": {"with_thumbnail": "true", "from": "manual"}
635
+ }
636
+ headers = {
637
+ "Authorization": "Bearer " + self.access_token,
638
+ "x-device-id": self.device_id,
639
+ "x-captcha-token": self.captcha_token,
640
+ "Content-Type": "application/json"
641
+ }
642
+ response = self.send_request("POST", url, headers=headers, json_data=payload)
643
+ # 检查响应是否有效
644
+ if not response or not isinstance(response, dict):
645
+ print(f"files_task请求返回无效响应: {response}")
646
+ return response
647
+
648
+ def refresh_captcha_sign(self):
649
+ self.timestamp = str(int(time.time()) * 1000)
650
+ encrypt_string = self.client_id + self.version + self.package_name + self.device_id + self.timestamp
651
+ self.captcha_sign = captcha_sign_encrypt(encrypt_string, self.algorithms)
652
+
653
+
654
+ def save_account_info(name, account_info):
655
+ # 保证account目录存在
656
+ if not os.path.exists("./account"):
657
+ os.makedirs("./account")
658
+ with open("./account/" + name + ".json", "w", encoding="utf-8") as f:
659
+ json.dump(account_info, f, ensure_ascii=False, indent=4)
660
+
661
+
662
+ def test_proxy(proxy_url):
663
+ """测试代理连接是否可用"""
664
+ test_url = "https://mypikpak.com" # 改为 PikPak 的网站,更可能连通
665
+ proxies = {
666
+ "http": proxy_url,
667
+ "https": proxy_url
668
+ }
669
+
670
+ try:
671
+ response = requests.get(test_url, proxies=proxies, timeout=10) # 增加超时时间
672
+ response.raise_for_status()
673
+ print(f"代理连接测试成功: {proxy_url}")
674
+ return True
675
+ except Exception as e:
676
+ print(f"代理连接测试失败: {proxy_url}, 错误: {e}")
677
+ return False
678
+
679
+
680
+ # 程序运行主函数
681
+ def main():
682
+ try:
683
+ # 1、初始化参数
684
+ current_version = ramdom_version()
685
+ version = current_version['v']
686
+ algorithms = current_version['algorithms']
687
+ client_id = "YNxT9w7GMdWvEOKa"
688
+ client_secret = "dbw2OtmVEeuUvIptb1Coyg"
689
+ package_name = "com.pikcloud.pikpak"
690
+ device_id = str(uuid.uuid4()).replace("-", "")
691
+ rtc_token = random_rtc_token()
692
+ print(f"当前版本:{version} 设备号:{device_id} 令牌:{rtc_token}")
693
+
694
+ # 询问用户是否使用代理
695
+ use_proxy_input = input('是否启用代理(y/n):').strip().lower()
696
+ use_proxy = use_proxy_input == 'y' or use_proxy_input == 'yes'
697
+
698
+ proxy_http = None
699
+ proxy_https = None
700
+
701
+ if use_proxy:
702
+ # 询问用户是否使用默认代理
703
+ default_proxy = input('是否使用默认代理地址 http://127.0.0.1:7890 (y/n):').strip().lower()
704
+ if default_proxy == 'y' or default_proxy == 'yes':
705
+ proxy_url = "http://127.0.0.1:7890"
706
+ print("已启用代理,使用默认地址:", proxy_url)
707
+
708
+ # 测试默认代理连接
709
+ if not test_proxy(proxy_url):
710
+ retry = input("默认代理连接测试失败,是否继续使用(y/n):").strip().lower()
711
+ if retry != 'y' and retry != 'yes':
712
+ print("已取消代理设置,将直接连接")
713
+ use_proxy = False
714
+ proxy_url = None
715
+
716
+ if use_proxy:
717
+ proxy_http = proxy_url
718
+ proxy_https = proxy_url
719
+ else:
720
+ # 用户自定义代理地址和端口
721
+ proxy_host = input('请输入代理主机地址 (默认127.0.0.1): ').strip()
722
+ proxy_host = proxy_host if proxy_host else '127.0.0.1'
723
+
724
+ proxy_port = input('请输入代理端口 (默认7890): ').strip()
725
+ proxy_port = proxy_port if proxy_port else '7890'
726
+
727
+ proxy_protocol = input('请输入代理协议 (http/https/socks5,默认http): ').strip().lower()
728
+ proxy_protocol = proxy_protocol if proxy_protocol in ['http', 'https', 'socks5'] else 'http'
729
+
730
+ proxy_url = f"{proxy_protocol}://{proxy_host}:{proxy_port}"
731
+ print(f"已设置代理地址: {proxy_url}")
732
+
733
+ # 测试自定义代理连接
734
+ if not test_proxy(proxy_url):
735
+ retry = input("自定义代理连接测试失败,是否继续使用(y/n):").strip().lower()
736
+ if retry != 'y' and retry != 'yes':
737
+ print("已取消代理设置,将直接连接")
738
+ use_proxy = False
739
+ proxy_url = None
740
+
741
+ if use_proxy:
742
+ proxy_http = proxy_url
743
+ proxy_https = proxy_url
744
+ else:
745
+ print("未启用代理,直接连接")
746
+
747
+ invite_code = input('请输入你的邀请码:')
748
+ email = input("请输入注册用的邮箱:")
749
+ # 2、实例化PikPak类,传入代理设置
750
+ pikpak = PikPak(invite_code, client_id, device_id, version, algorithms, email, rtc_token, client_secret,
751
+ package_name, use_proxy=use_proxy, proxy_http=proxy_http, proxy_https=proxy_https)
752
+
753
+ # 3、刷新timestamp,加密sign值。
754
+ init_result = pikpak.init("POST:/v1/auth/verification")
755
+ if not init_result or not isinstance(init_result, dict) or "captcha_token" not in init_result:
756
+ print("初始化失败,请检查网络连接或代理设置")
757
+ input("按任意键退出程序")
758
+ return
759
+
760
+ # 4、图片滑块分析
761
+ max_attempts = 5 # 最大尝试次数
762
+ captcha_result = None
763
+
764
+ for attempt in range(max_attempts):
765
+ print(f"尝试滑块验证 ({attempt+1}/{max_attempts})...")
766
+ try:
767
+ captcha_result = captcha_image_parse(pikpak, device_id)
768
+ print(captcha_result)
769
+
770
+ if captcha_result and "response_data" in captcha_result and captcha_result['response_data'].get('result') == 'accept':
771
+ print("滑块验证成功!")
772
+ break
773
+ else:
774
+ print('滑块验证失败, 正在重新尝试...')
775
+ time.sleep(2) # 延迟2秒再次尝试
776
+ except Exception as e:
777
+ print(f"滑块验证过程出错: {e}")
778
+ import traceback
779
+ traceback.print_exc()
780
+ time.sleep(2) # 出错后延迟2秒再次尝试
781
+
782
+ if not captcha_result or "response_data" not in captcha_result or captcha_result['response_data'].get('result') != 'accept':
783
+ print("滑块验证失败,达到最大尝试次数")
784
+ input("按任意键退出程序")
785
+ return
786
+
787
+ # 5、滑块验证加密
788
+ try:
789
+ executor_info = pikpak.executor()
790
+ if not executor_info:
791
+ print("获取executor信息失败")
792
+ input("按任意键退出程序")
793
+ return
794
+
795
+ sign_encrypt_info = sign_encrypt(executor_info, pikpak.captcha_token, rtc_token, pikpak.use_proxy, pikpak.proxies)
796
+ if not sign_encrypt_info or "request_id" not in sign_encrypt_info or "sign" not in sign_encrypt_info:
797
+ print("签名加密失败")
798
+ print(f"executor_info: {executor_info}")
799
+ print(f"captcha_token: {pikpak.captcha_token}")
800
+ print(f"rtc_token: {rtc_token}")
801
+ input("按任意键退出程序")
802
+ return
803
+
804
+ # 更新 captcha_token
805
+ pikpak.report(sign_encrypt_info['request_id'], sign_encrypt_info['sign'], captcha_result['pid'],
806
+ captcha_result['traceid'])
807
+
808
+ # 发送邮箱验证码
809
+ verification_result = pikpak.verification()
810
+ if not verification_result or not isinstance(verification_result, dict) or "verification_id" not in verification_result:
811
+ print("请求验证码失败")
812
+ input("按任意键退出程序")
813
+ return
814
+ except Exception as e:
815
+ print(f"验证过程出错: {e}")
816
+ import traceback
817
+ traceback.print_exc()
818
+ input("按任意键退出程序")
819
+ return
820
+
821
+ # 6、提交验证码
822
+ verification_code = input("请输入接收到的验证码:")
823
+ pikpak.verify_post(verification_code)
824
+
825
+ # 7、刷新timestamp,加密sign值
826
+ pikpak.init("POST:/v1/auth/signup")
827
+
828
+ # 8、注册登录
829
+ name = email.split("@")[0]
830
+ password = "zhiyuan233"
831
+ pikpak.signup(name, password, verification_code)
832
+
833
+ # 9、填写邀请码
834
+ pikpak.activation_code()
835
+
836
+ # 准备账号信息
837
+ account_info = {
838
+ "version": pikpak.version,
839
+ "device_id": pikpak.device_id,
840
+ "email": pikpak.email,
841
+ "captcha_token": pikpak.captcha_token,
842
+ "access_token": pikpak.access_token,
843
+ "refresh_token": pikpak.refresh_token,
844
+ "user_id": pikpak.user_id,
845
+ "timestamp": pikpak.timestamp,
846
+ "password": password,
847
+ "name": name
848
+ }
849
+
850
+ print("请保存好账号信息备用:", json.dumps(account_info, indent=4, ensure_ascii=False))
851
+
852
+ # 确认是否保存账号信息
853
+ save_info = input("是否保存账号信息到文件(y/n):").strip().lower()
854
+ if save_info == 'y' or save_info == 'yes':
855
+ try:
856
+ # 创建account目录(如果不存在)
857
+ if not os.path.exists("./account"):
858
+ os.makedirs("./account")
859
+ save_account_info(name, account_info)
860
+ print(f"账号信息已保存到 ./account/{name}.json")
861
+ except Exception as e:
862
+ print(f"保存账号信息失败: {e}")
863
+
864
+ input("运行完成,回车结束程序:")
865
+ except KeyboardInterrupt:
866
+ print("\n程序被用户中断")
867
+ except Exception as e:
868
+ print(f"程序运行出错: {e}")
869
+ import traceback
870
+ traceback.print_exc()
871
+ input("按任意键退出程序")
872
+
873
+
874
+ if __name__ == "__main__":
875
+ print("开发者声明:免费转载需标注出处:B站-纸鸢花的花语,此工具仅供交流学习和技术分析,严禁用于任何商业牟利行为。(包括但不限于倒卖、二改倒卖、引流、冒充作者、广告植入...)")
876
+ main()
utils/pk_email.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import imaplib
2
+ import re
3
+ import email
4
+ import socket
5
+ import socks # 增加 socks 库支持
6
+
7
+ # IMAP 服务器信息
8
+ IMAP_SERVER = 'imap.shanyouxiang.com'
9
+ IMAP_PORT = 993 # IMAP SSL 端口
10
+
11
+ # 邮件发送者列表(用于查找验证码)
12
+ VERIFICATION_SENDERS = ['[email protected]']
13
+
14
+
15
+ # --------------------------- IMAP 获取验证码 ---------------------------
16
+
17
+ def connect_imap(email_user, email_password, folder='INBOX', use_proxy=False, proxy_url=None):
18
+ """
19
+ 使用 IMAP 连接并检查指定文件夹中的验证码邮件
20
+ 支持通过代理连接
21
+
22
+ 参数:
23
+ email_user: 邮箱用户名
24
+ email_password: 邮箱密码
25
+ folder: 要检查的文件夹
26
+ use_proxy: 是否使用代理
27
+ proxy_url: 代理服务器URL (例如 "http://127.0.0.1:7890")
28
+ """
29
+ original_socket = None
30
+
31
+ try:
32
+ # 如果启用代理,设置SOCKS代理
33
+ if use_proxy and proxy_url:
34
+ # 解析代理URL
35
+ if proxy_url.startswith(('http://', 'https://')):
36
+ # 从HTTP代理URL提取主机和端口
37
+ from urllib.parse import urlparse
38
+ parsed = urlparse(proxy_url)
39
+ proxy_host = parsed.hostname
40
+ proxy_port = parsed.port or 80
41
+
42
+ # 保存原始socket
43
+ original_socket = socket.socket
44
+
45
+ # 设置socks代理
46
+ socks.set_default_proxy(socks.PROXY_TYPE_HTTP, proxy_host, proxy_port)
47
+ socket.socket = socks.socksocket
48
+
49
+ print(f"使用代理连接IMAP服务器: {proxy_url}")
50
+
51
+ # 连接 IMAP 服务器
52
+ mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
53
+ mail.login(email_user, email_password) # 直接使用邮箱密码登录
54
+
55
+ # 选择文件夹
56
+ status, _ = mail.select(folder)
57
+ if status != 'OK':
58
+ return {"code": 0, "msg": f"无法访问 {folder} 文件夹"}
59
+
60
+ # 搜索邮件
61
+ status, messages = mail.search(None, 'ALL')
62
+ if status != 'OK' or not messages[0]:
63
+ return {"code": 0, "msg": f"{folder} 文件夹为空"}
64
+
65
+ message_ids = messages[0].split()
66
+ verification_code = None
67
+ timestamp = None
68
+
69
+ for msg_id in message_ids[::-1]: # 从最新邮件开始查找
70
+ status, msg_data = mail.fetch(msg_id, '(RFC822)')
71
+ if status != 'OK':
72
+ continue
73
+
74
+ for response_part in msg_data:
75
+ if isinstance(response_part, tuple):
76
+ msg = email.message_from_bytes(response_part[1])
77
+ from_email = msg['From']
78
+
79
+ if any(sender in from_email for sender in VERIFICATION_SENDERS):
80
+ timestamp = msg['Date']
81
+
82
+ # 解析邮件正文
83
+ if msg.is_multipart():
84
+ for part in msg.walk():
85
+ if part.get_content_type() == 'text/html':
86
+ body = part.get_payload(decode=True).decode('utf-8')
87
+ break
88
+ else:
89
+ body = msg.get_payload(decode=True).decode('utf-8')
90
+
91
+ # 提取验证码
92
+ match = re.search(r'\b(\d{6})\b', body)
93
+ if match:
94
+ verification_code = match.group(1)
95
+ break
96
+
97
+ if verification_code:
98
+ break
99
+
100
+ mail.logout()
101
+
102
+ if verification_code:
103
+ return {"code": 200, "verification_code": verification_code, "time": timestamp,
104
+ "msg": f"成功获取验证码 ({folder})"}
105
+ else:
106
+ return {"code": 0, "msg": f"{folder} 中未找到验证码"}
107
+
108
+ except imaplib.IMAP4.error as e:
109
+ return {"code": 401, "msg": "IMAP 认证失败,请检查邮箱和密码是否正确,或者邮箱是否支持IMAP登录"}
110
+ except Exception as e:
111
+ return {"code": 500, "msg": f"错误: {str(e)}"}
112
+ finally:
113
+ # 恢复原始socket
114
+ if original_socket:
115
+ socket.socket = original_socket
116
+