mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-06-27 21:25:08 +00:00
Compare commits
1 Commits
b8ac9b60fb
...
b0dbeae99e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0dbeae99e |
13
app.js
13
app.js
@ -8,11 +8,16 @@ async function start() {
|
|||||||
if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) {
|
if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) {
|
||||||
fs.writeFileSync(path.resolve(tmpPath, 'anonymous_token'), '', 'utf-8')
|
fs.writeFileSync(path.resolve(tmpPath, 'anonymous_token'), '', 'utf-8')
|
||||||
}
|
}
|
||||||
// 启动时更新anonymous_token
|
// 启动时更新anonymous_token(Vercel 构建环境下跳过网络请求喵~)
|
||||||
const generateConfig = require('./generateConfig')
|
if (!process.env.VERCEL_ENV) {
|
||||||
await generateConfig()
|
const generateConfig = require('./generateConfig')
|
||||||
|
await generateConfig()
|
||||||
|
}
|
||||||
require('./server').serveNcmApi({
|
require('./server').serveNcmApi({
|
||||||
checkVersion: true,
|
checkVersion: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
start()
|
start().catch((err) => {
|
||||||
|
console.error('[FATAL] 启动失败:', err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|||||||
@ -4,38 +4,159 @@ const { register_anonimous } = require('./main')
|
|||||||
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
||||||
const { getXeapiPublicKey } = require('./util/xeapiKey')
|
const { getXeapiPublicKey } = require('./util/xeapiKey')
|
||||||
const tmpPath = require('os').tmpdir()
|
const tmpPath = require('os').tmpdir()
|
||||||
|
const logger = require('./util/logger')
|
||||||
|
|
||||||
async function generateConfig() {
|
const MAX_RETRIES = 10
|
||||||
global.cnIp = generateRandomChineseIP()
|
const RETRY_DELAY_MS = 1000
|
||||||
try {
|
|
||||||
const res = await register_anonimous()
|
function sleep(ms) {
|
||||||
const cookie = res.body.cookie
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
if (cookie) {
|
}
|
||||||
const cookieObj = cookieToJson(cookie)
|
|
||||||
|
function isRetryableError(err) {
|
||||||
|
const status =
|
||||||
|
(err && err.status) || (err && err.response && err.response.status)
|
||||||
|
if (status && status >= 500) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {{ success: boolean, error?: Error }} */
|
||||||
|
async function fetchAnonymousToken() {
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
const res = await register_anonimous()
|
||||||
|
const body = res.body
|
||||||
|
if (body.code && body.code !== 200) {
|
||||||
|
const err = new Error(`匿名注册返回错误码 ${body.code}`)
|
||||||
|
err.status = 502
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
const cookie = body.cookie
|
||||||
|
if (cookie) {
|
||||||
|
const cookieObj = cookieToJson(cookie)
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(tmpPath, 'anonymous_token'),
|
||||||
|
cookieObj.MUSIC_A,
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
logger.success('[generateConfig] 匿名 token 注册成功')
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`[generateConfig] 匿名注册返回了空 cookie (attempt ${attempt})`,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: new Error('empty cookie from anonymous register'),
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (isRetryableError(err) && attempt < MAX_RETRIES) {
|
||||||
|
const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1)
|
||||||
|
logger.warn(
|
||||||
|
`[generateConfig] 获取匿名 token 失败 (attempt ${attempt}/${MAX_RETRIES}), ${delay}ms 后重试...`,
|
||||||
|
)
|
||||||
|
await sleep(delay)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (attempt >= MAX_RETRIES) {
|
||||||
|
logger.error(
|
||||||
|
`[generateConfig] 获取匿名 token 已达最大重试次数 (${MAX_RETRIES}):`,
|
||||||
|
err.message,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`[generateConfig] 获取匿名 token 失败 (不可重试):`,
|
||||||
|
err.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return { success: false, error: err }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: false, error: new Error('unreachable') }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 xeapi public key,带重试
|
||||||
|
* @returns {{ success: boolean, error?: Error }}
|
||||||
|
*/
|
||||||
|
async function fetchXeapiPublicKey() {
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
let currentPublicKey = {}
|
||||||
|
try {
|
||||||
|
currentPublicKey = JSON.parse(
|
||||||
|
fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'),
|
||||||
|
)
|
||||||
|
} catch (_) {
|
||||||
|
// 本地无缓存文件,用空对象正常请求
|
||||||
|
}
|
||||||
|
const publicKey = await getXeapiPublicKey(
|
||||||
|
currentPublicKey,
|
||||||
|
global.deviceId,
|
||||||
|
)
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.resolve(tmpPath, 'anonymous_token'),
|
path.resolve(tmpPath, 'xeapi_public_key'),
|
||||||
cookieObj.MUSIC_A,
|
JSON.stringify(publicKey),
|
||||||
'utf-8',
|
'utf-8',
|
||||||
)
|
)
|
||||||
|
logger.success('[generateConfig] xeapi public key 获取成功')
|
||||||
|
return { success: true }
|
||||||
|
} catch (err) {
|
||||||
|
if (isRetryableError(err) && attempt < MAX_RETRIES) {
|
||||||
|
const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1)
|
||||||
|
logger.warn(
|
||||||
|
`[generateConfig] 获取 xeapi public key 失败 (attempt ${attempt}/${MAX_RETRIES}), ${delay}ms 后重试...`,
|
||||||
|
)
|
||||||
|
await sleep(delay)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (attempt >= MAX_RETRIES) {
|
||||||
|
logger.error(
|
||||||
|
`[generateConfig] 获取 xeapi public key 已达最大重试次数 (${MAX_RETRIES}):`,
|
||||||
|
err.message,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`[generateConfig] 获取 xeapi public key 失败 (不可重试):`,
|
||||||
|
err.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return { success: false, error: err }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
}
|
||||||
try {
|
return { success: false, error: new Error('unreachable') }
|
||||||
let currentPublicKey = {}
|
}
|
||||||
try {
|
|
||||||
currentPublicKey = JSON.parse(
|
/**
|
||||||
fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'),
|
* 生成配置(匿名 token + xeapi public key),带容错重试
|
||||||
)
|
* @returns {{ tokenOk: boolean, keyOk: boolean }}
|
||||||
} catch (_) {}
|
*/
|
||||||
const publicKey = await getXeapiPublicKey(currentPublicKey, global.deviceId)
|
async function generateConfig() {
|
||||||
fs.writeFileSync(
|
global.cnIp = generateRandomChineseIP()
|
||||||
path.resolve(tmpPath, 'xeapi_public_key'),
|
|
||||||
JSON.stringify(publicKey),
|
// 两个任务并行执行,互不影响喵~
|
||||||
'utf-8',
|
const [tokenResult, keyResult] = await Promise.all([
|
||||||
)
|
fetchAnonymousToken(),
|
||||||
} catch (error) {
|
fetchXeapiPublicKey(),
|
||||||
console.log(error)
|
])
|
||||||
|
|
||||||
|
if (!tokenResult.success) {
|
||||||
|
logger.warn('[generateConfig] 匿名 token 获取失败')
|
||||||
|
}
|
||||||
|
if (!keyResult.success) {
|
||||||
|
logger.warn('[generateConfig] xeapi public key 获取失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenResult.success && keyResult.success) {
|
||||||
|
logger.success('[generateConfig] 配置初始化完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokenOk: tokenResult.success,
|
||||||
|
keyOk: keyResult.success,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = generateConfig
|
module.exports = generateConfig
|
||||||
|
|||||||
@ -26,7 +26,6 @@ function cloudmusic_dll_encode_id(some_id) {
|
|||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
const deviceId = generateDeviceId()
|
const deviceId = generateDeviceId()
|
||||||
logger.info(`Successfully registered anonimous token, deviceId: ${deviceId}`)
|
|
||||||
global.deviceId = deviceId
|
global.deviceId = deviceId
|
||||||
const encodedId = CryptoJS.enc.Base64.stringify(
|
const encodedId = CryptoJS.enc.Base64.stringify(
|
||||||
CryptoJS.enc.Utf8.parse(
|
CryptoJS.enc.Utf8.parse(
|
||||||
|
|||||||
@ -9,6 +9,10 @@
|
|||||||
"lint": "eslint \"**/*.{js,ts}\"",
|
"lint": "eslint \"**/*.{js,ts}\"",
|
||||||
"lint-fix": "eslint --fix \"**/*.{js,ts}\"",
|
"lint-fix": "eslint --fix \"**/*.{js,ts}\"",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
|
"docs:format": "node scripts/format-docs.js",
|
||||||
|
"docs:check": "node scripts/format-docs.js --check",
|
||||||
|
"docs:toc": "node scripts/format-docs.js --toc",
|
||||||
|
"docs:all": "node scripts/format-docs.js --dir public/docs --toc --verbose",
|
||||||
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app",
|
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app",
|
||||||
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app",
|
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app",
|
||||||
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app"
|
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app"
|
||||||
@ -50,7 +54,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
|
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.js": [
|
"*.js": [
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
## 目录
|
||||||
|
|
||||||
|
- [NeteaseCloudMusicAPI Enhanced](#neteasecloudmusicapi-enhanced)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# NeteaseCloudMusicAPI Enhanced
|
# NeteaseCloudMusicAPI Enhanced
|
||||||
|
|
||||||
> 🎉 全网收集最全的网易云音乐api接口 基于[NeteaseCloudMusicAPI](https://github.com/binaryify/NeteaseCloudMusicApi)的复刻版本
|
> 🎉 全网收集最全的网易云音乐api接口 基于[NeteaseCloudMusicAPI](https://github.com/binaryify/NeteaseCloudMusicApi)的复刻版本
|
||||||
|
|||||||
@ -88,6 +88,7 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr
|
|||||||
5. 直接点`Continue`
|
5. 直接点`Continue`
|
||||||
6. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可
|
6. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可
|
||||||
|
|
||||||
|
|
||||||
## 腾讯云 serverless 部署
|
## 腾讯云 serverless 部署
|
||||||
|
|
||||||
因 `Vercel` 在国内访问太慢(不绑定自己的域名的情况下),在此提供腾讯云 serverless 部署方法
|
因 `Vercel` 在国内访问太慢(不绑定自己的域名的情况下),在此提供腾讯云 serverless 部署方法
|
||||||
@ -101,6 +102,7 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr
|
|||||||
5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目
|
5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目
|
||||||
6. 启动文件填入:
|
6. 启动文件填入:
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
export PORT=9000
|
export PORT=9000
|
||||||
@ -113,6 +115,7 @@ export PORT=9000
|
|||||||
- 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费
|
- 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费
|
||||||
- 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的
|
- 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的
|
||||||
|
|
||||||
|
|
||||||
## 可以使用代理
|
## 可以使用代理
|
||||||
|
|
||||||
在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy
|
在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy
|
||||||
@ -180,6 +183,7 @@ request 相关的环境变量
|
|||||||
5. no_proxy
|
5. no_proxy
|
||||||
6. NO_PROXY
|
6. NO_PROXY
|
||||||
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker pull moefurina/ncm-api
|
docker pull moefurina/ncm-api
|
||||||
|
|
||||||
@ -214,6 +218,7 @@ $ sudo docker run -d -p 3000:3000 netease-music-api
|
|||||||
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
|
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
|
||||||
- 目前支持算法 有 `weapi`, `eapi`, `linuxapi` 和 `xeapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数)
|
- 目前支持算法 有 `weapi`, `eapi`, `linuxapi` 和 `xeapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数)
|
||||||
|
|
||||||
|
|
||||||
## 接口文档
|
## 接口文档
|
||||||
|
|
||||||
### 调用前须知
|
### 调用前须知
|
||||||
@ -1210,6 +1215,7 @@ tags: 歌单标签
|
|||||||
|
|
||||||
> 如果你设置 limit=50&offset=100,你就会得到第 101-150 首歌曲
|
> 如果你设置 limit=50&offset=100,你就会得到第 101-150 首歌曲
|
||||||
|
|
||||||
|
|
||||||
### 歌单详情动态
|
### 歌单详情动态
|
||||||
|
|
||||||
说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数
|
说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数
|
||||||
@ -1496,6 +1502,7 @@ tags: 歌单标签
|
|||||||
|
|
||||||
- (可能存在)JSON 歌曲元数据
|
- (可能存在)JSON 歌曲元数据
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
{"t":0,"c":[{"tx":"作曲: "},{"tx":"柳重言","li":"http://p1.music.126.net/Icj0IcaOjH2ZZpyAM-QGoQ==/6665239487822533.jpg","or":"orpheus://nm/artist/home?id=228547&type=artist"}]}
|
{"t":0,"c":[{"tx":"作曲: "},{"tx":"柳重言","li":"http://p1.music.126.net/Icj0IcaOjH2ZZpyAM-QGoQ==/6665239487822533.jpg","or":"orpheus://nm/artist/home?id=228547&type=artist"}]}
|
||||||
{"t":5403,"c":[{"tx":"编曲: "},{"tx":"Alex San","li":"http://p1.music.126.net/pSbvYkrzZ1RFKqoh-fA9AQ==/109951166352922615.jpg","or":"orpheus://nm/artist/home?id=28984845&type=artist"}]}
|
{"t":5403,"c":[{"tx":"编曲: "},{"tx":"Alex San","li":"http://p1.music.126.net/pSbvYkrzZ1RFKqoh-fA9AQ==/109951166352922615.jpg","or":"orpheus://nm/artist/home?id=28984845&type=artist"}]}
|
||||||
@ -1512,6 +1519,7 @@ tags: 歌单标签
|
|||||||
|
|
||||||
* 逐字歌词
|
* 逐字歌词
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
[16210,3460](16210,670,0)还(16880,410,0)没...
|
[16210,3460](16210,670,0)还(16880,410,0)没...
|
||||||
~~~~1 ~~~2 ~~~~3 ~~4 5 ~6 (...)
|
~~~~1 ~~~2 ~~~~3 ~~4 5 ~6 (...)
|
||||||
@ -2885,6 +2893,7 @@ type : 地区
|
|||||||
- 适合 Vercel、Netlify 等有请求体限制的平台
|
- 适合 Vercel、Netlify 等有请求体限制的平台
|
||||||
- 需要前端配合实现
|
- 需要前端配合实现
|
||||||
|
|
||||||
|
|
||||||
#### 客户端直传相关接口
|
#### 客户端直传相关接口
|
||||||
|
|
||||||
**获取上传凭证**
|
**获取上传凭证**
|
||||||
@ -2931,6 +2940,7 @@ type : 地区
|
|||||||
- `artist`: 艺术家
|
- `artist`: 艺术家
|
||||||
- `album`: 专辑名
|
- `album`: 专辑名
|
||||||
|
|
||||||
|
|
||||||
#### 客户端直传流程
|
#### 客户端直传流程
|
||||||
|
|
||||||
1. 客户端计算文件 MD5
|
1. 客户端计算文件 MD5
|
||||||
@ -2938,6 +2948,7 @@ type : 地区
|
|||||||
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
|
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
|
||||||
4. 调用 `/cloud/upload/complete` 完成导入
|
4. 调用 `/cloud/upload/complete` 完成导入
|
||||||
|
|
||||||
|
|
||||||
### 云盘歌曲信息匹配纠正
|
### 云盘歌曲信息匹配纠正
|
||||||
|
|
||||||
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
|
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
|
||||||
@ -4210,6 +4221,7 @@ ONLINE 已发布
|
|||||||
- `voiceFeeType: 0`:返回免费的声音
|
- `voiceFeeType: 0`:返回免费的声音
|
||||||
- `voiceFeeType: 1`:返回收费的声音
|
- `voiceFeeType: 1`:返回收费的声音
|
||||||
|
|
||||||
|
|
||||||
### 播客声音详情
|
### 播客声音详情
|
||||||
|
|
||||||
说明: 获取播客里的声音详情
|
说明: 获取播客里的声音详情
|
||||||
|
|||||||
@ -148,12 +148,12 @@
|
|||||||
updateStatus('二维码已过期,请刷新页面', 'error')
|
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
} else if (statusRes.code === 801) {
|
} else if (statusRes.code === 801) {
|
||||||
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
updateStatus('等待手机扫码...', 'waiting')
|
||||||
} else if (statusRes.code === 802) {
|
} else if (statusRes.code === 802) {
|
||||||
updateStatus('登录成功,正在保存信息...', 'waiting')
|
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||||
} else if (statusRes.code === 803) {
|
} else if (statusRes.code === 803) {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
updateStatus('授权登录成功!', 'success')
|
updateStatus('授权登录成功,正在保存信息...', 'success')
|
||||||
await getLoginStatus(statusRes.cookie)
|
await getLoginStatus(statusRes.cookie)
|
||||||
localStorage.setItem('cookie', statusRes.cookie)
|
localStorage.setItem('cookie', statusRes.cookie)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -148,12 +148,12 @@
|
|||||||
updateStatus('二维码已过期,请刷新页面', 'error')
|
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
} else if (statusRes.code === 801) {
|
} else if (statusRes.code === 801) {
|
||||||
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
updateStatus('等待手机扫码...', 'waiting')
|
||||||
} else if (statusRes.code === 802) {
|
} else if (statusRes.code === 802) {
|
||||||
updateStatus('登录成功,正在保存信息...', 'waiting')
|
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||||
} else if (statusRes.code === 803) {
|
} else if (statusRes.code === 803) {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
updateStatus('授权登录成功!', 'success')
|
updateStatus('授权登录成功,正在保存信息...', 'success')
|
||||||
await getLoginStatus(statusRes.cookie)
|
await getLoginStatus(statusRes.cookie)
|
||||||
localStorage.setItem('cookie', statusRes.cookie)
|
localStorage.setItem('cookie', statusRes.cookie)
|
||||||
}
|
}
|
||||||
|
|||||||
575
scripts/format-docs.js
Normal file
575
scripts/format-docs.js
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* 📝 Markdown 文档格式化工具喵~
|
||||||
|
* 支持格式化标题、代码块、空行、缩进,还能生成目录!
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* node scripts/format-docs.js # 格式化默认文档 (public/docs/home.md)
|
||||||
|
* node scripts/format-docs.js <文件路径> # 格式化指定文件
|
||||||
|
* node scripts/format-docs.js --dir <目录> # 格式化整个目录的 .md 文件
|
||||||
|
* node scripts/format-docs.js --check # 只检查不写入 (dry-run)
|
||||||
|
* node scripts/format-docs.js --toc # 同时生成目录
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// ======================== 配置 ========================
|
||||||
|
const CONFIG = {
|
||||||
|
maxConsecutiveBlankLines: 2, // 最大连续空行数
|
||||||
|
codeBlockLang: true, // 代码块是否保留语言标记
|
||||||
|
headingSpaceBefore: true, // 标题前是否确保空行
|
||||||
|
headingSpaceAfter: true, // 标题后是否确保空行
|
||||||
|
listIndent: 2, // 列表缩进空格数
|
||||||
|
encodeSpecialChars: false, // 是否编码特殊字符
|
||||||
|
removeTrailingSpaces: true, // 是否删除行尾空格
|
||||||
|
tocMaxLevel: 3, // 目录最大标题层级
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_FILE = path.resolve(__dirname, '..', 'public', 'docs', 'home.md')
|
||||||
|
|
||||||
|
// ======================== 颜色工具 ========================
|
||||||
|
const color = (code) => (s) => `\x1b[${code}m${s}\x1b[0m`
|
||||||
|
const green = color('32')
|
||||||
|
const cyan = color('36')
|
||||||
|
const yellow = color('33')
|
||||||
|
const red = color('31')
|
||||||
|
const bold = color('1')
|
||||||
|
const dim = color('2')
|
||||||
|
|
||||||
|
// ======================== 核心格式化函数 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Markdown 的块结构,返回块数组
|
||||||
|
* 块类型: 'heading', 'code', 'list', 'paragraph', 'empty', 'hr', 'blockquote', 'table', 'html'
|
||||||
|
*/
|
||||||
|
function parseBlocks(lines) {
|
||||||
|
const blocks = []
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i]
|
||||||
|
const trimmed = line.trim()
|
||||||
|
|
||||||
|
// 空行
|
||||||
|
if (trimmed === '') {
|
||||||
|
blocks.push({ type: 'empty', lines: [line], raw: line })
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 代码块 (``` 或 ~~~)
|
||||||
|
if (/^```/.test(trimmed) || /^~~~/.test(trimmed)) {
|
||||||
|
const marker = trimmed.match(/^(```|~~~)/)[1]
|
||||||
|
const lang = trimmed.slice(marker.length).trim()
|
||||||
|
const start = i
|
||||||
|
i++
|
||||||
|
while (i < lines.length && !lines[i].trim().startsWith(marker)) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if (i < lines.length) i++ // 跳过结束标记
|
||||||
|
const codeLines = lines.slice(start, i)
|
||||||
|
blocks.push({
|
||||||
|
type: 'code',
|
||||||
|
lines: codeLines,
|
||||||
|
lang,
|
||||||
|
marker,
|
||||||
|
raw: codeLines.join('\n'),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/)
|
||||||
|
if (headingMatch) {
|
||||||
|
const level = headingMatch[1].length
|
||||||
|
blocks.push({
|
||||||
|
type: 'heading',
|
||||||
|
lines: [line],
|
||||||
|
level,
|
||||||
|
text: headingMatch[2],
|
||||||
|
raw: line,
|
||||||
|
})
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分割线
|
||||||
|
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(trimmed)) {
|
||||||
|
blocks.push({ type: 'hr', lines: [line], raw: line })
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML 注释或标签
|
||||||
|
if (/^<!--/.test(trimmed) || /^<\w+/.test(trimmed)) {
|
||||||
|
const start = i
|
||||||
|
i++
|
||||||
|
while (i < lines.length) {
|
||||||
|
const t = lines[i].trim()
|
||||||
|
if (/-->/.test(t) || /<\/\w+>/.test(t)) { i++; break }
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
const htmlLines = lines.slice(start, i)
|
||||||
|
blocks.push({ type: 'html', lines: htmlLines, raw: htmlLines.join('\n') })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 引用块 (注: 空行不吞噬,留给后续解析,避免干扰上下文的空行判断)
|
||||||
|
if (/^>/.test(trimmed)) {
|
||||||
|
const start = i
|
||||||
|
i++
|
||||||
|
while (i < lines.length && lines[i].trimStart().startsWith('>')) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
const quoteLines = lines.slice(start, i)
|
||||||
|
blocks.push({ type: 'blockquote', lines: quoteLines, raw: quoteLines.join('\n') })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格
|
||||||
|
if (/\|/.test(trimmed) && lines[i + 1] && /^\|[\s\-:]+\|/.test(lines[i + 1].trim())) {
|
||||||
|
const start = i
|
||||||
|
i += 2
|
||||||
|
while (i < lines.length && /\|/.test(lines[i].trim())) { i++ }
|
||||||
|
const tableLines = lines.slice(start, i)
|
||||||
|
blocks.push({ type: 'table', lines: tableLines, raw: tableLines.join('\n') })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表项(有序或无序)
|
||||||
|
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line)) {
|
||||||
|
const start = i
|
||||||
|
i++
|
||||||
|
while (i < lines.length) {
|
||||||
|
const t = lines[i].trim()
|
||||||
|
if (t === '') { i++; break }
|
||||||
|
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])) { i++; continue }
|
||||||
|
// 缩进 continuation
|
||||||
|
if (/^\s{2,}/.test(lines[i])) { i++; continue }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const listLines = lines.slice(start, i)
|
||||||
|
blocks.push({ type: 'list', lines: listLines, raw: listLines.join('\n') })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通段落
|
||||||
|
const start = i
|
||||||
|
i++
|
||||||
|
while (i < lines.length && lines[i].trim() !== '') {
|
||||||
|
// 如果遇到新的块元素则停止
|
||||||
|
const t = lines[i].trim()
|
||||||
|
if (/^#{1,6}\s/.test(t) || /^```/.test(t) || /^~~~/.test(t) || /^(\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])) break
|
||||||
|
// 分割线
|
||||||
|
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(t)) break
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
const paraLines = lines.slice(start, i)
|
||||||
|
blocks.push({ type: 'paragraph', lines: paraLines, raw: paraLines.join('\n') })
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文档
|
||||||
|
*/
|
||||||
|
function formatMarkdown(input, options = {}) {
|
||||||
|
const cfg = { ...CONFIG, ...options }
|
||||||
|
const lines = input.split('\n')
|
||||||
|
let blocks = parseBlocks(lines)
|
||||||
|
|
||||||
|
// ---------- 格式化步骤 ----------
|
||||||
|
|
||||||
|
// 1. 确保标题前后有空行
|
||||||
|
if (cfg.headingSpaceBefore || cfg.headingSpaceAfter) {
|
||||||
|
blocks = blocks.map((block, idx) => {
|
||||||
|
if (block.type !== 'heading') return block
|
||||||
|
|
||||||
|
const newLines = [...block.lines]
|
||||||
|
|
||||||
|
// 标题前加空行(如果不是第一个块且前一个不是空行)
|
||||||
|
if (cfg.headingSpaceBefore && idx > 0 && blocks[idx - 1].type !== 'empty') {
|
||||||
|
newLines.unshift('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题后加空行(如果不是最后一个块且后一个不是空行)
|
||||||
|
if (cfg.headingSpaceAfter && idx < blocks.length - 1 && blocks[idx + 1].type !== 'empty') {
|
||||||
|
newLines.push('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...block, lines: newLines }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 格式化代码块
|
||||||
|
blocks = blocks.map((block, idx) => {
|
||||||
|
if (block.type !== 'code') return block
|
||||||
|
|
||||||
|
const newLines = [...block.lines]
|
||||||
|
|
||||||
|
// 先提取代码块标记信息 (第一行是 ``` 或 ~~~)
|
||||||
|
const markerMatch = newLines[0].trim().match(/^(```|~~~)/)
|
||||||
|
if (!markerMatch) return block // 安全兜底
|
||||||
|
const marker = markerMatch[1]
|
||||||
|
const lang = newLines[0].trim().slice(marker.length).trim().toLowerCase()
|
||||||
|
|
||||||
|
// 标准化语言标记
|
||||||
|
const indent = newLines[0].match(/^\s*/)[0]
|
||||||
|
newLines[0] = lang ? `${indent}${marker}${lang}` : `${indent}${marker}`
|
||||||
|
|
||||||
|
// 确保代码块前后有空行(插在标准化之后,因为只动第一行)
|
||||||
|
if (idx > 0 && blocks[idx - 1].type !== 'empty' && blocks[idx - 1].type !== 'code') {
|
||||||
|
newLines.unshift('')
|
||||||
|
}
|
||||||
|
if (idx < blocks.length - 1 && blocks[idx + 1].type !== 'empty' && blocks[idx + 1].type !== 'code') {
|
||||||
|
newLines.push('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...block, lines: newLines }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. 压缩多余空行
|
||||||
|
blocks = compressEmptyLines(blocks, cfg.maxConsecutiveBlankLines)
|
||||||
|
|
||||||
|
// 4. 删除行尾空格
|
||||||
|
if (cfg.removeTrailingSpaces) {
|
||||||
|
blocks = blocks.map((block) => ({
|
||||||
|
...block,
|
||||||
|
lines: block.lines.map((l) => l.replace(/[ \t]+$/, '')),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 修复列表缩进
|
||||||
|
blocks = blocks.map((block) => {
|
||||||
|
if (block.type !== 'list') return block
|
||||||
|
return {
|
||||||
|
...block,
|
||||||
|
lines: block.lines.map((line) => {
|
||||||
|
const trimmed = line.trimStart()
|
||||||
|
// 检测列表标记
|
||||||
|
if (/^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) {
|
||||||
|
const indent = line.length - line.trimStart().length
|
||||||
|
// 如果是顶层列表项,确保缩进为0
|
||||||
|
if (indent % cfg.listIndent !== 0) {
|
||||||
|
const normalizedIndent = Math.round(indent / cfg.listIndent) * cfg.listIndent
|
||||||
|
return ' '.repeat(normalizedIndent) + trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重新拼装
|
||||||
|
const resultLines = blocks.flatMap((b) => b.lines)
|
||||||
|
|
||||||
|
// 处理文件开头和结尾的空行
|
||||||
|
while (resultLines.length > 0 && resultLines[0] === '') resultLines.shift()
|
||||||
|
while (resultLines.length > 0 && resultLines[resultLines.length - 1] === '') resultLines.pop()
|
||||||
|
resultLines.push('') // 文件结尾留一个空行
|
||||||
|
|
||||||
|
return resultLines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩连续空行块
|
||||||
|
*/
|
||||||
|
function compressEmptyLines(blocks, maxBlank) {
|
||||||
|
const result = []
|
||||||
|
let blankCount = 0
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (block.type === 'empty') {
|
||||||
|
blankCount++
|
||||||
|
if (blankCount <= maxBlank) {
|
||||||
|
result.push(block)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blankCount = 0
|
||||||
|
result.push(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 目录生成 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文档中提取标题生成目录
|
||||||
|
*/
|
||||||
|
function generateTOC(input, maxLevel = 3) {
|
||||||
|
const lines = input.split('\n')
|
||||||
|
const headings = []
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const match = line.match(/^(#{1,6})\s+(.+)$/)
|
||||||
|
if (match) {
|
||||||
|
const level = match[1].length
|
||||||
|
if (level <= maxLevel) {
|
||||||
|
headings.push({ level, text: match[2].trim() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headings.length === 0) return ''
|
||||||
|
|
||||||
|
let toc = '\n## 目录\n\n'
|
||||||
|
for (const h of headings) {
|
||||||
|
const indent = ' '.repeat(h.level - 1)
|
||||||
|
const anchor = h.text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\w\u4e00-\u9fa5]+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '')
|
||||||
|
toc += `${indent}- [${h.text}](#${anchor})\n`
|
||||||
|
}
|
||||||
|
toc += '\n---\n'
|
||||||
|
|
||||||
|
return toc
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 统计信息 ========================
|
||||||
|
|
||||||
|
function getStats(input) {
|
||||||
|
const lines = input.split('\n')
|
||||||
|
const blocks = parseBlocks(lines)
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalLines: lines.length,
|
||||||
|
nonEmptyLines: lines.filter((l) => l.trim() !== '').length,
|
||||||
|
headings: blocks.filter((b) => b.type === 'heading').length,
|
||||||
|
codeBlocks: blocks.filter((b) => b.type === 'code').length,
|
||||||
|
lists: blocks.filter((b) => b.type === 'list').length,
|
||||||
|
tables: blocks.filter((b) => b.type === 'table').length,
|
||||||
|
blockquotes: blocks.filter((b) => b.type === 'blockquote').length,
|
||||||
|
hr: blocks.filter((b) => b.type === 'hr').length,
|
||||||
|
characters: input.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printStats(stats) {
|
||||||
|
console.log(bold('\n📊 文档统计信息:'))
|
||||||
|
console.log(` 总行数: ${cyan(String(stats.totalLines))}`)
|
||||||
|
console.log(` 非空行数: ${cyan(String(stats.nonEmptyLines))}`)
|
||||||
|
console.log(` 字符数: ${cyan(String(stats.characters))}`)
|
||||||
|
console.log(` 标题数: ${cyan(String(stats.headings))}`)
|
||||||
|
console.log(` 代码块数: ${cyan(String(stats.codeBlocks))}`)
|
||||||
|
console.log(` 列表数: ${cyan(String(stats.lists))}`)
|
||||||
|
console.log(` 表格数: ${cyan(String(stats.tables))}`)
|
||||||
|
console.log(` 引用块数: ${cyan(String(stats.blockquotes))}`)
|
||||||
|
console.log(` 分割线数: ${cyan(String(stats.hr))}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 文件处理 ========================
|
||||||
|
|
||||||
|
function readFile(filePath) {
|
||||||
|
try {
|
||||||
|
let content = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
// 统一换行符为 \n,避免 Windows 的 \r\n 导致 diff 不稳定
|
||||||
|
content = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||||
|
return content
|
||||||
|
} catch (err) {
|
||||||
|
console.error(red(`❌ 无法读取文件: ${filePath}`))
|
||||||
|
console.error(dim(err.message))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(filePath, content) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(filePath, content, 'utf-8')
|
||||||
|
console.log(green(`✅ 已写入: ${filePath}`))
|
||||||
|
} catch (err) {
|
||||||
|
console.error(red(`❌ 写入失败: ${filePath}`))
|
||||||
|
console.error(dim(err.message))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processFile(filePath, options) {
|
||||||
|
const relativePath = path.relative(process.cwd(), filePath)
|
||||||
|
console.log(bold(`\n📄 处理文件: ${cyan(relativePath)}`))
|
||||||
|
|
||||||
|
const input = readFile(filePath)
|
||||||
|
const statsBefore = getStats(input)
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log(dim(' 格式化前:'))
|
||||||
|
printStats(statsBefore)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = formatMarkdown(input, options)
|
||||||
|
const statsAfter = getStats(formatted)
|
||||||
|
|
||||||
|
const hasChanges = input !== formatted
|
||||||
|
|
||||||
|
if (options.check) {
|
||||||
|
if (hasChanges) {
|
||||||
|
console.log(yellow(' ⚠️ 文件需要格式化 (dry-run, 未写入)'))
|
||||||
|
const added = statsAfter.totalLines - statsBefore.totalLines
|
||||||
|
console.log(dim(` 行数变化: ${added > 0 ? '+' : ''}${added}`))
|
||||||
|
} else {
|
||||||
|
console.log(green(' ✅ 文件已格式良好'))
|
||||||
|
}
|
||||||
|
return hasChanges ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
// 如果指定了 --toc,在文档开头插入目录
|
||||||
|
let output = formatted
|
||||||
|
if (options.toc) {
|
||||||
|
const tocPlaceholder = '<!-- TOC -->'
|
||||||
|
if (output.includes(tocPlaceholder)) {
|
||||||
|
output = output.replace(
|
||||||
|
new RegExp(`${tocPlaceholder}[\\s\\S]*?${tocPlaceholder}`),
|
||||||
|
`${tocPlaceholder}\n${generateTOC(output, options.tocMaxLevel || CONFIG.tocMaxLevel)}${tocPlaceholder}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
output = generateTOC(output, options.tocMaxLevel || CONFIG.tocMaxLevel) + '\n' + output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeFile(filePath, output)
|
||||||
|
console.log(green(' ✨ 格式化完成!'))
|
||||||
|
if (options.verbose) {
|
||||||
|
printStats(statsAfter)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(green(' ✅ 文档已经格式良好,无需修改'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function processDirectory(dirPath, options) {
|
||||||
|
let exitCode = 0
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(dirPath)
|
||||||
|
.filter((f) => f.endsWith('.md'))
|
||||||
|
.map((f) => path.join(dirPath, f))
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log(yellow(`⚠️ 在 ${dirPath} 中未找到 .md 文件`))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(bold(`\n📁 扫描目录: ${cyan(dirPath)} (${files.length} 个文件)\n`))
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const ret = processFile(file, options)
|
||||||
|
if (ret !== 0) exitCode = ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== CLI ========================
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(bold(`
|
||||||
|
📝 Markdown 文档格式化工具 v1.0.0
|
||||||
|
|
||||||
|
${cyan('用法:')}
|
||||||
|
node scripts/format-docs.js [文件路径] [选项]
|
||||||
|
|
||||||
|
${cyan('参数:')}
|
||||||
|
文件路径 要格式化的 .md 文件 (默认: public/docs/home.md)
|
||||||
|
|
||||||
|
${cyan('选项:')}
|
||||||
|
--dir, -d <目录> 格式化整个目录下的所有 .md 文件
|
||||||
|
--check, -c dry-run 模式,只检查不写入
|
||||||
|
--toc, -t 在文档中生成目录
|
||||||
|
--verbose, -v 显示详细统计信息
|
||||||
|
--help, -h 显示帮助信息
|
||||||
|
|
||||||
|
${cyan('示例:')}
|
||||||
|
node scripts/format-docs.js
|
||||||
|
node scripts/format-docs.js README.md
|
||||||
|
node scripts/format-docs.js --dir docs/
|
||||||
|
node scripts/format-docs.js --check
|
||||||
|
node scripts/format-docs.js --toc --verbose
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs() {
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
const options = {
|
||||||
|
file: null,
|
||||||
|
dir: null,
|
||||||
|
check: false,
|
||||||
|
toc: false,
|
||||||
|
verbose: false,
|
||||||
|
help: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
switch (args[i]) {
|
||||||
|
case '--dir':
|
||||||
|
case '-d':
|
||||||
|
options.dir = args[++i]
|
||||||
|
break
|
||||||
|
case '--check':
|
||||||
|
case '-c':
|
||||||
|
options.check = true
|
||||||
|
break
|
||||||
|
case '--toc':
|
||||||
|
case '-t':
|
||||||
|
options.toc = true
|
||||||
|
break
|
||||||
|
case '--verbose':
|
||||||
|
case '-v':
|
||||||
|
options.verbose = true
|
||||||
|
break
|
||||||
|
case '--help':
|
||||||
|
case '-h':
|
||||||
|
options.help = true
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if (!args[i].startsWith('--') && !args[i].startsWith('-')) {
|
||||||
|
options.file = path.resolve(process.cwd(), args[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 主函数 ========================
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const options = parseArgs()
|
||||||
|
|
||||||
|
if (options.help) {
|
||||||
|
printHelp()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(bold(`\n${cyan('🐱 文档格式化工具')} ${dim('(用❤️制作)')}\n`))
|
||||||
|
|
||||||
|
if (options.dir) {
|
||||||
|
const dirPath = path.resolve(process.cwd(), options.dir)
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
console.error(red(`❌ 目录不存在: ${dirPath}`))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return processDirectory(dirPath, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = options.file || DEFAULT_FILE
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.error(red(`❌ 文件不存在: ${filePath}`))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return processFile(filePath, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 启动 ========================
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
const exitCode = main()
|
||||||
|
process.exit(exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { formatMarkdown, generateTOC, getStats, parseBlocks }
|
||||||
16
server.js
16
server.js
@ -10,6 +10,7 @@ const { cookieToJson } = require('./util/index')
|
|||||||
const fileUpload = require('express-fileupload')
|
const fileUpload = require('express-fileupload')
|
||||||
const decode = require('safe-decode-uri-component')
|
const decode = require('safe-decode-uri-component')
|
||||||
const logger = require('./util/logger.js')
|
const logger = require('./util/logger.js')
|
||||||
|
const { APP_CONF } = require('./util/config.json')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version check result.
|
* The version check result.
|
||||||
@ -299,15 +300,15 @@ async function constructServer(moduleDefs) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let usedCrypto = ''
|
||||||
const moduleResponse = await moduleDef.module(query, (...params) => {
|
const moduleResponse = await moduleDef.module(query, (...params) => {
|
||||||
// 参数注入客户端IP
|
|
||||||
const obj = [...params]
|
const obj = [...params]
|
||||||
const options = obj[2] || {}
|
const options = obj[2] || {}
|
||||||
|
usedCrypto = options.crypto || ''
|
||||||
let ip = ''
|
let ip = ''
|
||||||
|
|
||||||
if (options.randomCNIP) {
|
if (options.randomCNIP) {
|
||||||
ip = global.cnIp
|
ip = global.cnIp
|
||||||
// logger.info('Using random Chinese IP for request:', ip)
|
|
||||||
} else {
|
} else {
|
||||||
ip = req.ip
|
ip = req.ip
|
||||||
|
|
||||||
@ -317,7 +318,6 @@ async function constructServer(moduleDefs) {
|
|||||||
if (ip == '::1') {
|
if (ip == '::1') {
|
||||||
ip = global.cnIp
|
ip = global.cnIp
|
||||||
}
|
}
|
||||||
// logger.info('Requested from ip:', ip)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
obj[2] = {
|
obj[2] = {
|
||||||
@ -327,7 +327,10 @@ async function constructServer(moduleDefs) {
|
|||||||
|
|
||||||
return request(...obj)
|
return request(...obj)
|
||||||
})
|
})
|
||||||
logger.info(`Request Success: ${decode(req.originalUrl)}`)
|
const displayCrypto = usedCrypto || (APP_CONF.encrypt ? 'eapi' : 'api')
|
||||||
|
logger.info(
|
||||||
|
`Request Success: [${displayCrypto}] ${decode(req.originalUrl)}`,
|
||||||
|
)
|
||||||
|
|
||||||
// 夹带私货部分:如果开启了通用解锁,并且是获取歌曲URL的接口,则尝试解锁(如果需要的话)ヾ(≧▽≦*)o
|
// 夹带私货部分:如果开启了通用解锁,并且是获取歌曲URL的接口,则尝试解锁(如果需要的话)ヾ(≧▽≦*)o
|
||||||
if (
|
if (
|
||||||
@ -445,10 +448,7 @@ async function serveNcmApi(options) {
|
|||||||
╩ ╩╩ ╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝
|
╩ ╩╩ ╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝
|
||||||
`)
|
`)
|
||||||
logger.info(`
|
logger.info(`
|
||||||
- Server started successfully @ http://${host ? host : 'localhost'}:${port}
|
- Server started successfully @ http://${host ? host : 'localhost'}:${port}`)
|
||||||
- Environment: ${process.env.NODE_ENV || 'development'}
|
|
||||||
- Node Version: ${process.version}
|
|
||||||
- Process ID: ${process.pid}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return appExt
|
return appExt
|
||||||
|
|||||||
@ -16,9 +16,5 @@
|
|||||||
"Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
|
"Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "production",
|
|
||||||
"ENABLE_FLAC": "true"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user