├── .dockerignore
├── .gitignore
├── .replit
├── Dockerfile
├── LICENSE
├── backend
├── .gitignore
├── .replit
├── api.js
├── app.js
├── bin
│ └── www
├── netlify.toml
├── package-lock.json
├── package.json
├── public
│ └── .gitignore
├── replit.nix
├── routes
│ ├── api.js
│ └── redbook.js
└── utils
│ ├── douyinService.js
│ ├── redbookService.js
│ ├── x-bogus.js
│ └── xhs-sign.js
├── frontend
├── .cache
│ ├── replit
│ │ ├── __replit_disk_meta.json
│ │ ├── modules.stamp
│ │ └── nix
│ │ │ └── env.json
│ └── typescript
│ │ └── 4.4
│ │ ├── package-lock.json
│ │ └── package.json
├── .gitignore
├── .replit
├── .upm
│ └── store.json
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── help.svg
│ ├── logo.png
│ └── logo.svg
├── replit.nix
├── replit_zip_error_log.txt
├── src
│ ├── App.vue
│ ├── assets
│ │ └── main.css
│ ├── components
│ │ └── Home.vue
│ ├── locales
│ │ ├── en.json
│ │ ├── i18n.js
│ │ └── zh.json
│ ├── main.js
│ └── utils
│ │ ├── config.js
│ │ └── tableUtils.js
└── vite.config.js
├── package-lock.json
├── package.json
├── readme.md
└── use-doc
├── 使用截图.png
├── 使用示例.gif
├── 填写自定义插件.png
├── 新增自定义插件.png
└── 获取cookie.png
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | backend/node_modules
3 | npm-debug.log
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node.js
2 | node_modules
3 | bower_components
4 | nodemon
5 |
6 | # Byte-compiled / optimized / DLL files
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 |
11 | # C extensions
12 | *.so
13 |
14 | # Distribution / packaging
15 | .Python
16 | build/
17 | develop-eggs/
18 | dist/
19 | downloads/
20 | eggs/
21 | .eggs/
22 | lib/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | wheels/
28 | pip-wheel-metadata/
29 | share/python-wheels/
30 | *.egg-info/
31 | .installed.cfg
32 | *.egg
33 | MANIFEST
34 |
35 | # PyInstaller
36 | # Usually these files are written by a python script from a template
37 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
38 | *.manifest
39 | *.spec
40 |
41 | # Installer logs
42 | pip-log.txt
43 | pip-delete-this-directory.txt
44 |
45 | # Unit test / coverage reports
46 | htmlcov/
47 | .tox/
48 | .nox/
49 | .coverage
50 | .coverage.*
51 | .cache
52 | nosetests.xml
53 | coverage.xml
54 | *.cover
55 | *.py,cover
56 | .hypothesis/
57 | .pytest_cache/
58 |
59 | # Translations
60 | *.mo
61 | *.pot
62 |
63 | # Django stuff:
64 | *.log
65 | local_settings.py
66 | db.sqlite3
67 | db.sqlite3-journal
68 |
69 | # Flask stuff:
70 | instance/
71 | .webassets-cache
72 |
73 | # Scrapy stuff:
74 | .scrapy
75 |
76 | # Sphinx documentation
77 | docs/_build/
78 |
79 | # PyBuilder
80 | target/
81 |
82 | # Jupyter Notebook
83 | .ipynb_checkpoints
84 |
85 | # IPython
86 | profile_default/
87 | ipython_config.py
88 |
89 | # pyenv
90 | .python-version
91 |
92 | # pipenv
93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
96 | # install all needed dependencies.
97 | #Pipfile.lock
98 |
99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
100 | __pypackages__/
101 |
102 | # Celery stuff
103 | celerybeat-schedule
104 | celerybeat.pid
105 |
106 | # SageMath parsed files
107 | *.sage.py
108 |
109 | # Environments
110 | .env
111 | .venv
112 | env/
113 | venv/
114 | ENV/
115 | env.bak/
116 | venv.bak/
117 |
118 | # Spyder project settings
119 | .spyderproject
120 | .spyproject
121 |
122 | # Rope project settings
123 | .ropeproject
124 |
125 | # mkdocs documentation
126 | /site
127 |
128 | # mypy
129 | .mypy_cache/
130 | .dmypy.json
131 | dmypy.json
132 |
133 | # Pyre type checker
134 | .pyre/
135 |
--------------------------------------------------------------------------------
/.replit:
--------------------------------------------------------------------------------
1 | modules = ["nodejs-20:v8-20230920-bd784b9"]
2 | hidden = [".config", "package-lock.json"]
3 | run = "npm run build && npm run start"
4 |
5 | [nix]
6 | channel = "stable-23_05"
7 |
8 | [unitTest]
9 | language = "nodejs"
10 |
11 | [deployment]
12 | run = ["sh", "-c", "npm run build && npm run start"]
13 | deploymentTarget = "cloudrun"
14 | ignorePorts = false
15 | build = ["sh", "-c", "npm install"]
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18
2 |
3 | # Create app directory
4 | WORKDIR /usr/src/feishudouyin
5 |
6 | # Install app dependencies
7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied
8 | # where available (npm@5+)
9 | COPY package*.json ./
10 |
11 | RUN npm install
12 | RUN npm build
13 | # If you are building your code for production
14 | # RUN npm ci --omit=dev
15 |
16 | # Bundle app source
17 | COPY ./backend .
18 |
19 | EXPOSE 4000
20 |
21 | CMD [ "npm", "start" ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
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.
202 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | # Node.js
2 | node_modules
3 | bower_components
4 | nodemon
5 |
6 | # Byte-compiled / optimized / DLL files
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 |
11 | # C extensions
12 | *.so
13 |
14 | # Distribution / packaging
15 | .Python
16 | build/
17 | develop-eggs/
18 | dist/
19 | downloads/
20 | eggs/
21 | .eggs/
22 | lib/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | wheels/
28 | pip-wheel-metadata/
29 | share/python-wheels/
30 | *.egg-info/
31 | .installed.cfg
32 | *.egg
33 | MANIFEST
34 |
35 | # PyInstaller
36 | # Usually these files are written by a python script from a template
37 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
38 | *.manifest
39 | *.spec
40 |
41 | # Installer logs
42 | pip-log.txt
43 | pip-delete-this-directory.txt
44 |
45 | # Unit test / coverage reports
46 | htmlcov/
47 | .tox/
48 | .nox/
49 | .coverage
50 | .coverage.*
51 | .cache
52 | nosetests.xml
53 | coverage.xml
54 | *.cover
55 | *.py,cover
56 | .hypothesis/
57 | .pytest_cache/
58 |
59 | # Translations
60 | *.mo
61 | *.pot
62 |
63 | # Django stuff:
64 | *.log
65 | local_settings.py
66 | db.sqlite3
67 | db.sqlite3-journal
68 |
69 | # Flask stuff:
70 | instance/
71 | .webassets-cache
72 |
73 | # Scrapy stuff:
74 | .scrapy
75 |
76 | # Sphinx documentation
77 | docs/_build/
78 |
79 | # PyBuilder
80 | target/
81 |
82 | # Jupyter Notebook
83 | .ipynb_checkpoints
84 |
85 | # IPython
86 | profile_default/
87 | ipython_config.py
88 |
89 | # pyenv
90 | .python-version
91 |
92 | # pipenv
93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
96 | # install all needed dependencies.
97 | #Pipfile.lock
98 |
99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
100 | __pypackages__/
101 |
102 | # Celery stuff
103 | celerybeat-schedule
104 | celerybeat.pid
105 |
106 | # SageMath parsed files
107 | *.sage.py
108 |
109 | # Environments
110 | .env
111 | .venv
112 | env/
113 | venv/
114 | ENV/
115 | env.bak/
116 | venv.bak/
117 |
118 | # Spyder project settings
119 | .spyderproject
120 | .spyproject
121 |
122 | # Rope project settings
123 | .ropeproject
124 |
125 | # mkdocs documentation
126 | /site
127 |
128 | # mypy
129 | .mypy_cache/
130 | .dmypy.json
131 | dmypy.json
132 |
133 | # Pyre type checker
134 | .pyre/
135 |
--------------------------------------------------------------------------------
/backend/.replit:
--------------------------------------------------------------------------------
1 | modules = ["nodejs-20:v8-20230920-bd784b9"]
2 | hidden = [".config", "package-lock.json"]
3 | run = "npm run start"
4 |
5 | [nix]
6 | channel = "stable-23_05"
7 |
8 | [unitTest]
9 | language = "nodejs"
10 |
11 | [deployment]
12 | run = ["sh", "-c", "npm run start"]
13 | deploymentTarget = "cloudrun"
14 | ignorePorts = false
15 |
--------------------------------------------------------------------------------
/backend/api.js:
--------------------------------------------------------------------------------
1 | import serverless from "serverless-http";
2 |
3 | const app = require('./app');
4 |
5 | export const handler = serverless(app);
--------------------------------------------------------------------------------
/backend/app.js:
--------------------------------------------------------------------------------
1 | var createError = require('http-errors');
2 | var express = require('express');
3 | var path = require('path');
4 | var cookieParser = require('cookie-parser');
5 | var logger = require('morgan');
6 | var favicon = require('serve-favicon')
7 |
8 | var apiRouter = require('./routes/api');
9 | var redbookRouter = require('./routes/redbook');
10 |
11 | var app = express();
12 |
13 |
14 | // view engine setup
15 | app.engine('html', require('ejs').renderFile);
16 |
17 | app.set('view engine', 'html');
18 | app.use(logger('dev'));
19 | app.use(express.json());
20 | app.use(express.urlencoded({ extended: false }));
21 | app.use(cookieParser());
22 | app.use(favicon(path.join(__dirname, 'public/favicon.ico')));
23 |
24 | // CROS
25 | app.all('*', function (req, res, next) {
26 | res.header('Access-Control-Allow-Origin', '*')
27 | res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length, Authorization, Accept,X-Requested-With')
28 | res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS')
29 | res.header('X-Powered-By', ' 3.2.1')
30 | req.method == "OPTIONS" ? res.send(200) : next()
31 | })
32 |
33 | // 主页
34 | app.use(express.static(path.join(__dirname, 'public')));
35 | // 调用api
36 | app.use('/api', apiRouter);
37 | app.use('/redbook', redbookRouter);
38 |
39 | // catch 404 and forward to error handler
40 | app.use(function(req, res, next) {
41 | next(createError(404));
42 | });
43 |
44 | // error handler
45 | app.use(function(err, req, res, next) {
46 | // set locals, only providing error in development
47 | res.locals.message = err.message;
48 | res.locals.error = req.app.get('env') === 'development' ? err : {};
49 |
50 | // render the error page
51 | console.log(err)
52 | res.status(err.status || 500);
53 | res.render('error');
54 | });
55 |
56 |
57 | module.exports = app;
--------------------------------------------------------------------------------
/backend/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('feishudouyin:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '4000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/backend/netlify.toml:
--------------------------------------------------------------------------------
1 | [functions]
2 | external_node_modules = ["express"]
3 | node_bundler = "esbuild"
4 | [[redirects]]
5 | force = true
6 | from = "/*"
7 | status = 200
8 | to = "/.netlify/functions/:splat"
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "feishu-douyin-tool-be",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "node ./bin/www"
7 | },
8 | "dependencies": {
9 | "@netlify/functions": "^2.5.1",
10 | "axios": "^0.26.1",
11 | "cookie-parser": "~1.4.4",
12 | "debug": "~2.6.9",
13 | "ejs": "^3.1.6",
14 | "express": "^4.18.2",
15 | "express-handlebars": "^6.0.5",
16 | "file-saver": "^2.0.5",
17 | "http-errors": "~1.6.3",
18 | "jade": "^1.9.2",
19 | "jsdom": "^24.0.0",
20 | "jszip": "^3.10.1",
21 | "md5": "^2.3.0",
22 | "morgan": "~1.9.1",
23 | "nodemon": "^2.0.15",
24 | "serve-favicon": "^2.5.0",
25 | "serverless-http": "^3.2.0"
26 | },
27 | "description": "
",
28 | "main": "app.js",
29 | "repository": {
30 | "type": "git",
31 | "url": "git+https://github.com/happyVee/feishu-douyin-tool.git"
32 | },
33 | "keywords": [],
34 | "author": "",
35 | "license": "ISC",
36 | "bugs": {
37 | "url": "https://github.com/happyVee/feishu-douyin-tool/issues"
38 | },
39 | "homepage": "https://github.com/happyVee/feishu-douyin-tool#readme"
40 | }
41 |
--------------------------------------------------------------------------------
/backend/public/.gitignore:
--------------------------------------------------------------------------------
1 | *
--------------------------------------------------------------------------------
/backend/replit.nix:
--------------------------------------------------------------------------------
1 | {pkgs}: {
2 | deps = [ ];
3 | }
4 |
--------------------------------------------------------------------------------
/backend/routes/api.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { GetID, GetInfo } = require('../utils/douyinService');
3 | const getXB = require('../utils/x-bogus.js')
4 |
5 | let router = express.Router();
6 |
7 | /* 获取API */
8 | router.post('/', async function(req, res, next) {
9 | try {
10 | // 尝试从cookies中获取并解析dycookie
11 | let dyCookie;
12 | try {
13 | dyCookie = req.body.dyCookie;
14 | } catch {
15 | dyCookie = null;
16 | }
17 |
18 |
19 | // 如果dycookie不存在或无法解析rs
20 | const allEmpty = Object.values(dyCookie).every(value => value === '');
21 | if (allEmpty) {
22 | res.render('index', { videoData: {work:false} });
23 | return;
24 | }
25 |
26 | // 如果未提供URL,则使用默认视频
27 | let videoUrl = req.body.url;
28 | if (!videoUrl) {
29 | throw new Error('视频地址为空');
30 | }
31 |
32 | // 检查URL的有效性(简单的)
33 | if (!videoUrl.startsWith('https://v.douyin.com/') && !videoUrl.startsWith('https://www.douyin.com/')) {
34 | throw new Error('无效的URL地址');
35 | }
36 |
37 | // 根据提供的URL获取视频ID
38 | const videoId = await GetID(videoUrl);
39 |
40 | // 使用视频ID和cookie获取视频详情
41 | const videoData = await GetInfo(videoId, dyCookie, getXB);
42 | res.send({code:0, data: videoData, msg: '解析成功'})
43 | } catch (error) {
44 | next(error); // 转发错误到错误处理中间件
45 | }
46 | });
47 |
48 | // 错误处理中间件
49 | router.use((err, req, res, next) => {
50 | console.error(err.stack)
51 | // 不要尝试嗅探并覆盖响应的MIME类型
52 | res.setHeader('X-Content-Type-Options', 'nosniff');
53 | // 根据错误类型或消息为用户提供不同的反馈
54 | if (err.message.includes('无效的URL地址')) {
55 | res.status(200).send({code:400, data: null, msg: '提供的URL无效'})
56 | } else {
57 | res.status(200).send({code:400, data: null, msg: '服务器内部错误'})
58 | }
59 | });
60 |
61 | module.exports = router;
--------------------------------------------------------------------------------
/backend/routes/redbook.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { getAllNoteList, getProfileInfo, getNoteInfo } = require('../utils/redbookService.js');
3 |
4 | let router = express.Router();
5 |
6 | /* 获取全部笔记API */
7 | router.post('/getNoteList', async function(req, res, next) {
8 | try {
9 | // 尝试从cookies中获取并解析dycookie
10 | let xhsCookies;
11 | try {
12 | xhsCookies = req.body.xhsCookies;
13 | } catch {
14 | xhsCookies = null;
15 | }
16 |
17 |
18 | // 如果dycookie不存在或无法解析rs
19 | const allEmpty = Object.values(xhsCookies).every(value => value === '');
20 | if (allEmpty) {
21 | res.send({code:400, data: null, msg: '提供cookies参数缺失'})
22 | return;
23 | }
24 |
25 | // 如果未提供URL,则使用默认视频
26 | let userUrl = req.body.url;
27 | if (!userUrl) {
28 | throw new Error('用户主页地址为空');
29 | }
30 |
31 | // 检查URL的有效性(简单的)
32 | if (!userUrl.startsWith('https://www.xiaohongshu.com/')) {
33 | throw new Error('无效的URL地址');
34 | }
35 |
36 | // 根据提供的URL获取视频ID
37 | const parts = userUrl.split("/");
38 | const userId = parts[parts.length - 1];
39 | const regex = /^[0-9a-zA-Z]{24}$/;
40 | if(!regex.test(userId)) {
41 | throw new Error('用户主页地址错误');
42 | }
43 |
44 | // 使用视频ID和cookie获取视频详情
45 | const noteList = await getAllNoteList(userId, xhsCookies);
46 | res.send({code:0, data: noteList, msg: '成功'})
47 | } catch (error) {
48 | console.log("error==========>", error);
49 | next(error); // 转发错误到错误处理中间件
50 | }
51 | });
52 |
53 | /* 获取用户信息API */
54 | router.post('/getProfileInfo', async function(req, res, next) {
55 | try {
56 | // 尝试从cookies中获取并解析dycookie
57 | let xhsCookies;
58 | try {
59 | xhsCookies = req.body.xhsCookies;
60 | } catch {
61 | xhsCookies = null;
62 | }
63 |
64 |
65 | // 如果dycookie不存在或无法解析rs
66 | const allEmpty = Object.values(xhsCookies).every(value => value === '');
67 | if (allEmpty) {
68 | res.send({code:400, data: null, msg: '提供cookies参数缺失'})
69 | return;
70 | }
71 |
72 | // 如果未提供URL
73 | let userUrl = req.body.url;
74 | if (!userUrl) {
75 | throw new Error('用户主页地址为空');
76 | }
77 |
78 | // 检查URL的有效性(简单的)
79 | if (!userUrl.startsWith('https://www.xiaohongshu.com/')) {
80 | throw new Error('无效的URL地址');
81 | }
82 |
83 | // 根据提供的URL获取用户ID
84 | const parts = userUrl.split("/");
85 | const userId = parts[parts.length - 1];
86 | const regex = /^[0-9a-zA-Z]{24}$/;
87 | if(!regex.test(userId)) {
88 | throw new Error('用户主页地址错误');
89 | }
90 |
91 | // 使用用户ID和cookie获取用户详情
92 | const profileInfo = await getProfileInfo(userId, xhsCookies);
93 | res.send({code:0, data: profileInfo, msg: '成功'})
94 | } catch (error) {
95 | console.log("error==========>", error);
96 | next(error); // 转发错误到错误处理中间件
97 | }
98 | });
99 |
100 | /* 获取笔记API */
101 | router.post('/getNoteInfo', async function(req, res, next) {
102 | try {
103 | // 尝试从cookies中获取并解析dycookie
104 | let xhsCookies;
105 | try {
106 | xhsCookies = req.body.xhsCookies;
107 | } catch {
108 | xhsCookies = null;
109 | }
110 |
111 |
112 | // 如果dycookie不存在或无法解析rs
113 | const allEmpty = Object.values(xhsCookies).every(value => value === '');
114 | if (allEmpty) {
115 | res.status(400).send({code:400, data: null, msg: '提供cookies参数缺失'})
116 | return;
117 | }
118 |
119 | // 如果未提供URL
120 | let noteUrl = req.body.url;
121 | if (!noteUrl) {
122 | throw new Error('用户主页地址为空');
123 | }
124 |
125 | // 检查URL的有效性(简单的)
126 | if (!noteUrl.startsWith('https://www.xiaohongshu.com/')) {
127 | throw new Error('无效的URL地址');
128 | }
129 |
130 | // 根据提供的URL获取笔记ID
131 | const parts = noteUrl.split("/");
132 | const noteId = parts[parts.length - 1];
133 | const regex = /^[0-9a-zA-Z]{24}$/;
134 | if(!regex.test(noteId)) {
135 | throw new Error('用户主页地址错误');
136 | }
137 |
138 | // 使用笔记ID和cookie获取笔记详情
139 | const noteInfo = await getNoteInfo(noteId, xhsCookies);
140 | res.send({code:0, data: noteInfo, msg: '成功'})
141 | } catch (error) {
142 | console.log("error==========>", error);
143 | next(error); // 转发错误到错误处理中间件
144 | }
145 | });
146 |
147 | // 错误处理中间件
148 | router.use((err, req, res, next) => {
149 | console.log("use error==========>", err);
150 | console.error(err.stack)
151 | // 不要尝试嗅探并覆盖响应的MIME类型
152 | res.setHeader('X-Content-Type-Options', 'nosniff');
153 | // 根据错误类型或消息为用户提供不同的反馈
154 | if (err.message.includes('无效的URL地址')) {
155 | res.status(200).send({code:400, data: null, msg: '提供的URL无效'})
156 | } else {
157 | res.status(200).send({code:400, data: null, msg: err.message ?? '服务器内部错误'})
158 | }
159 | });
160 |
161 | module.exports = router;
--------------------------------------------------------------------------------
/backend/utils/douyinService.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 |
3 | // 常量定义
4 | const invalid = /[\\\n\r/:*?\"<>|]/g;
5 | const repWith = ``;
6 | const AWE_URL_BASE = "http://aweme.snssdk.com/aweme/v1/play/?";
7 | const DETAIL_URL_BASE = 'https://www.douyin.com/aweme/v1/web/aweme/detail/?'
8 | const USER_AGENT_MOBILE = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36";
9 | const USER_AGENT_DESKTOP = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1";
10 | const VIDEO_REGEX = /video\/(\d*)/;
11 | const NOTE_REGEX = /note\/(\d*)/;
12 |
13 | /**
14 | * 获取作品ID
15 | * @param {string} dyurl - 抖音短链接。
16 | * @returns {string} 作品ID
17 | * @throws {Error} 在请求失败或解析ID时可能会抛出错误。
18 | */
19 | async function GetID(dyurl) {
20 | const response = await axios.get(dyurl, {
21 | headers: { "user-agent": USER_AGENT_DESKTOP },
22 | });
23 |
24 | if (response.request.res.responseUrl.includes('video')) {
25 | item_ids = VIDEO_REGEX.exec(response.request.res.responseUrl)[1];
26 | } else if (response.request.res.responseUrl.includes('note')) {
27 | item_ids = NOTE_REGEX.exec(response.request.res.responseUrl)[1];
28 | } else {
29 | console.error("URL格式不匹配任何已知模式");
30 | return;
31 | }
32 | return item_ids;
33 | }
34 |
35 | /**
36 | * 根据作品ID和cookie获取作品详细信息
37 | * @param {string} item_ids - 作品ID。
38 | * @param {Object} dycookie - 抖音cookie对象。
39 | * @param {Function} getXB - 获取XB参数的函数。
40 | * @returns {Object} 作品的详细信息
41 | * @throws {Error} 在请求失败或解析数据时可能会抛出错误。
42 | */
43 | async function GetInfo(item_ids, dycookie, getXB) {
44 | // 构造请求URL
45 | const params_url = `aweme_id=${item_ids}&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333`;
46 | const xb = getXB(params_url);
47 | const url = `${DETAIL_URL_BASE}${params_url}&X-Bogus=${xb}`;
48 |
49 | const response = await axios.get(url, {
50 | headers: {
51 | 'cookie': `odin_tt=${dycookie["odin_tt"]};sessionid_ss=${dycookie["sessionid_ss"]};ttwid=${dycookie['ttwid']};passport_csrf_token=${dycookie['passport_csrf_token']};msToken=${dycookie['msToken']};`,
52 | 'referer': 'https://www.douyin.com/',
53 | 'user-agent': USER_AGENT_MOBILE
54 | }
55 | });
56 |
57 | // 如果response.data为空或未定义
58 | if (!response.data) {
59 | return { work: false };
60 | }
61 |
62 | // 校验响应状态
63 | if (response.data.status_code === 0) {
64 | // console.log(JSON.stringify(response.data))
65 | const aweme_detail = response.data.aweme_detail;
66 | // 提取需要的数据
67 | const { video, music, author, desc, aweme_id, aweme_type, statistics, create_time} = aweme_detail;
68 | const uniqueId = author.unique_id || author.short_id; // 如果unique_id为空,则使用short_id
69 | const userhome = `https://www.douyin.com/user/${author.sec_uid}`;
70 | const type = Number(aweme_type) === 0 ? '视频' : '图集';
71 | const images = Number(aweme_type) !== 0 ? aweme_detail.images.map(image => image.url_list[0]) : [];
72 | //const images = aweme_type !== 0 && response.data.aweme_detail.images ? response.data.aweme_detail.images.map(image => image.url_list[0]) : [];
73 |
74 | const url = video?.bit_rate?.[0]?.play_addr?.url_list?.[0] ?? '';
75 | const videoCover = video?.cover?.url_list?.[0] ?? '';
76 | const cleanedDesc = desc.replaceAll(invalid, repWith);
77 |
78 | const res = {
79 | url,
80 | type,
81 | title: cleanedDesc,
82 | videoUrl: url,
83 | videoCover,
84 | musicUrl: music.play_url.uri,
85 | musicTitle: music.title,
86 | nickname: author.nickname,
87 | signature: author.signature,
88 | userhome,
89 | uniqueId,
90 | videoId: aweme_id,
91 | images,
92 | statistics,
93 | releaseTime: aweme_detail.create_time * 1000,
94 | collectionCount: Number(statistics.collect_count) ,
95 | likeCount: Number(statistics.digg_count),
96 | shareCount: Number(statistics.share_count),
97 | commentCount: Number(statistics.comment_count),
98 | };
99 | // console.log(res)
100 | return res;
101 | } else {
102 | // 如果响应状态码不为0,抛出错误
103 | throw new Error(`Error with status code: ${response.data.status_code}`);
104 | }
105 | }
106 |
107 | module.exports = { GetID, GetInfo };
108 |
--------------------------------------------------------------------------------
/backend/utils/redbookService.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 | const getXs = require("./xhs-sign");
3 | const { json } = require("express");
4 |
5 |
6 | function getTypeWord(type) {
7 | if (type == 'normal') {
8 | return "图文"
9 | } else {
10 | return "视频"
11 | }
12 | }
13 |
14 | function getGenderWord(gender) {
15 | if (gender === 0) {
16 | return '男';
17 | } else if (gender === 1) {
18 | return '女';
19 | } else {
20 | return '未知';
21 | }
22 | }
23 |
24 | function extractIdFromUrl(url) {
25 | const regex = /\/(\w+)!/;
26 | const match = url.match(regex);
27 | return match ? match[1] : null;
28 | }
29 |
30 | function processUrls(urlList) {
31 | return urlList.map(url => {
32 | const id = extractIdFromUrl(url);
33 | return id ? 'https://sns-img-bd.xhscdn.com/' + id : null;
34 | }).filter(url => url !== null);
35 | }
36 |
37 | function decodedUniChars(url) {
38 | let decodedUniChars = decodeURIComponent(escape(url));
39 | return decodedUniChars;
40 | }
41 |
42 | function get_cookies() {
43 | return {
44 | "xsecappid": "",
45 | "a1": "",
46 | "webId": "",
47 | "gid": "",
48 | "webBuild": "3.3.4",
49 | "web_session": "",
50 | "websectiga": "",
51 | "sec_poison_id": ""
52 | }
53 | }
54 |
55 | function get_home_headers() {
56 | return {
57 | "authority": "www.xiaohongshu.com",
58 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
59 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
60 | "cache-control": "no-cache",
61 | "pragma": "no-cache",
62 | "sec-ch-ua": "\"Microsoft Edge\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\"",
63 | "sec-ch-ua-mobile": "?0",
64 | "sec-ch-ua-platform": "\"Windows\"",
65 | "sec-fetch-dest": "document",
66 | "sec-fetch-mode": "navigate",
67 | "sec-fetch-site": "none",
68 | "sec-fetch-user": "?1",
69 | "upgrade-insecure-requests": "1",
70 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.47"
71 | }
72 | }
73 |
74 | function get_headers() {
75 | return {
76 | "authority": "edith.xiaohongshu.com",
77 | "accept": "application/json, text/plain, */*",
78 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
79 | "content-type": "application/json;charset=UTF-8",
80 | "origin": "https://www.xiaohongshu.com",
81 | "referer": "https://www.xiaohongshu.com/",
82 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188",
83 | "x-s": "",
84 | "x-t": ""
85 | }
86 | }
87 |
88 | function get_note_data(note_id) {
89 | return {
90 | "source_note_id": note_id,
91 | "image_scenes": [
92 | "CRD_PRV_WEBP",
93 | "CRD_WM_WEBP"
94 | ]
95 | }
96 | }
97 |
98 | function get_search_data() {
99 | return {
100 | "image_scenes": "FD_PRV_WEBP,FD_WM_WEBP",
101 | "keyword": "",
102 | "note_type": "0",
103 | "page": "",
104 | "page_size": "20",
105 | "search_id": "2c7hu5b3kzoivkh848hp0",
106 | "sort": "general"
107 | }
108 | }
109 |
110 | function get_params() {
111 | return {
112 | "num": "30",
113 | "cursor": "",
114 | "user_id": "",
115 | "image_scenes": ""
116 | }
117 | }
118 |
119 | function formatNote(note) {
120 | if (!note) {
121 | return null;
122 | }
123 | return {
124 | "url" : "https://www.xiaohongshu.com/explore/" + note.note_id,
125 | "type" : getTypeWord(note.type),
126 | "title" : note.display_title,
127 | "likeCount" : note.interact_info?.liked_count,
128 | "nickname" : note.user?.nick_name,
129 | "userhome" : "https://www.xiaohongshu.com/user/profile/" + note.user?.user_id,
130 | "userAvatar" : note.user?.avatar,
131 | "noteCover" : note.cover?.url,
132 | "noteId" : note.note_id,
133 | }
134 | }
135 |
136 | function handleProfileInfo(userId, htmlText) {
137 | const userhome = "https://www.xiaohongshu.com/user/profile/" + userId
138 | const infoRegex = /
12 |