Spaces:
Paused
Paused
Upload 41 files
Browse files- .dockerignore +35 -0
- .env.example +1 -0
- .gitignore +3 -0
- Dockerfile +35 -0
- LICENSE +201 -0
- README.md +135 -10
- docker-compose.yml +13 -0
- frontend/.gitignore +24 -0
- frontend/README.md +54 -0
- frontend/eslint.config.js +28 -0
- frontend/index.html +13 -0
- frontend/package.json +34 -0
- frontend/pnpm-lock.yaml +0 -0
- frontend/public/vite.svg +1 -0
- frontend/src/App.css +102 -0
- frontend/src/App.tsx +88 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/components/Header/index.css +32 -0
- frontend/src/components/Header/index.tsx +41 -0
- frontend/src/index.css +68 -0
- frontend/src/main.tsx +11 -0
- frontend/src/pages/Activate/index.css +25 -0
- frontend/src/pages/Activate/index.tsx +383 -0
- frontend/src/pages/History/index.css +30 -0
- frontend/src/pages/History/index.tsx +327 -0
- frontend/src/pages/Register/index.css +106 -0
- frontend/src/pages/Register/index.tsx +1586 -0
- frontend/src/services/api.ts +94 -0
- frontend/src/vite-env.d.ts +1 -0
- frontend/tsconfig.app.json +26 -0
- frontend/tsconfig.json +7 -0
- frontend/tsconfig.node.json +24 -0
- frontend/vite.config.ts +21 -0
- requirements.txt +0 -0
- run.py +1102 -0
- utils/__pycache__/email_client.cpython-311.pyc +0 -0
- utils/__pycache__/pikpak.cpython-311.pyc +0 -0
- utils/__pycache__/pk_email.cpython-311.pyc +0 -0
- utils/email_client.py +371 -0
- utils/pikpak.py +876 -0
- utils/pk_email.py +116 -0
.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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
user@example.com----password123----client_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 = ['noreply@accounts.mypikpak.com']
|
| 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 |
+
|