Merge branch 'main' into release/4.8.1

This commit is contained in:
tangly1024
2025-01-05 20:00:11 +08:00
20 changed files with 633 additions and 419 deletions

View File

@@ -37,6 +37,7 @@ const BLOG = {
...require('./conf/animation.config'), // 动效美化效果 ...require('./conf/animation.config'), // 动效美化效果
...require('./conf/widget.config'), // 悬浮在网页上的挂件,聊天客服、宠物挂件、音乐播放器等 ...require('./conf/widget.config'), // 悬浮在网页上的挂件,聊天客服、宠物挂件、音乐播放器等
...require('./conf/ad.config'), // 广告营收插件 ...require('./conf/ad.config'), // 广告营收插件
...require('./conf/plugin.config'), // 其他第三方插件 algolia全文索引
// 高级用法 // 高级用法
...require('./conf/layout-map.config'), // 路由与布局映射自定义,例如自定义特定路由的页面布局 ...require('./conf/layout-map.config'), // 路由与布局映射自定义,例如自定义特定路由的页面布局
@@ -50,13 +51,6 @@ const BLOG = {
// 自定义菜单 // 自定义菜单
CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || true, // 支持Menu类型的菜单替代了3.12版本前的Page类型 CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || true, // 支持Menu类型的菜单替代了3.12版本前的Page类型
// 网站全文搜索
ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || null, // 在这里查看 https://dashboard.algolia.com/account/api-keys/
ALGOLIA_ADMIN_APP_KEY: process.env.ALGOLIA_ADMIN_APP_KEY || null, // 管理后台的KEY不要暴露在代码中在这里查看 https://dashboard.algolia.com/account/api-keys/
ALGOLIA_SEARCH_ONLY_APP_KEY:
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY
ALGOLIA_INDEX: process.env.NEXT_PUBLIC_ALGOLIA_INDEX || null, // 在Algolia中创建一个index用作数据库
// 文章列表相关设置 // 文章列表相关设置
CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许如果设置为false、则全栈禁止复制内容。 CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许如果设置为false、则全栈禁止复制内容。
@@ -67,7 +61,10 @@ const BLOG = {
// 欢迎语打字效果,Hexo,Matery主题支持, 英文逗号隔开多个欢迎语。 // 欢迎语打字效果,Hexo,Matery主题支持, 英文逗号隔开多个欢迎语。
GREETING_WORDS: GREETING_WORDS:
process.env.NEXT_PUBLIC_GREETING_WORDS || process.env.NEXT_PUBLIC_GREETING_WORDS ||
'Hi我是一个程序员, Hi我是一个打工人,Hi我是一个干饭人,欢迎来到我的博客🎉' 'Hi我是一个程序员, Hi我是一个打工人,Hi我是一个干饭人,欢迎来到我的博客🎉',
// uuid重定向至 slug
UUID_REDIRECT: process.env.UUID_REDIRECT || false
} }
module.exports = BLOG module.exports = BLOG

View File

@@ -5,48 +5,48 @@ import { useRouter } from 'next/router'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { import {
EmailIcon, EmailIcon,
EmailShareButton, EmailShareButton,
FacebookIcon, FacebookIcon,
FacebookMessengerIcon, FacebookMessengerIcon,
FacebookMessengerShareButton, FacebookMessengerShareButton,
FacebookShareButton, FacebookShareButton,
HatenaIcon, HatenaIcon,
HatenaShareButton, HatenaShareButton,
InstapaperIcon, InstapaperIcon,
InstapaperShareButton, InstapaperShareButton,
LineIcon, LineIcon,
LineShareButton, LineShareButton,
LinkedinIcon, LinkedinIcon,
LinkedinShareButton, LinkedinShareButton,
LivejournalIcon, LivejournalIcon,
LivejournalShareButton, LivejournalShareButton,
MailruIcon, MailruIcon,
MailruShareButton, MailruShareButton,
OKIcon, OKIcon,
OKShareButton, OKShareButton,
PinterestIcon, PinterestIcon,
PinterestShareButton, PinterestShareButton,
PocketIcon, PocketIcon,
PocketShareButton, PocketShareButton,
RedditIcon, RedditIcon,
RedditShareButton, RedditShareButton,
TelegramIcon, TelegramIcon,
TelegramShareButton, TelegramShareButton,
TumblrIcon, TumblrIcon,
TumblrShareButton, TumblrShareButton,
TwitterIcon, TwitterIcon,
TwitterShareButton, TwitterShareButton,
VKIcon, ViberIcon,
VKShareButton, ViberShareButton,
ViberIcon, VKIcon,
ViberShareButton, VKShareButton,
WeiboIcon, WeiboIcon,
WeiboShareButton, WeiboShareButton,
WhatsappIcon, WhatsappIcon,
WhatsappShareButton, WhatsappShareButton,
WorkplaceIcon, WorkplaceIcon,
WorkplaceShareButton WorkplaceShareButton
} from 'react-share' } from 'react-share'
const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false }) const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false })
@@ -61,6 +61,8 @@ const ShareButtons = ({ post }) => {
const [shareUrl, setShareUrl] = useState(siteConfig('LINK') + router.asPath) const [shareUrl, setShareUrl] = useState(siteConfig('LINK') + router.asPath)
const title = post?.title || siteConfig('TITLE') const title = post?.title || siteConfig('TITLE')
const image = post?.pageCover const image = post?.pageCover
const tags = post.tags || []
const hashTags = tags.map(tag => `#${tag}`).join(',')
const body = const body =
post?.title + ' | ' + title + ' ' + shareUrl + ' ' + post?.summary post?.title + ' | ' + title + ' ' + shareUrl + ' ' + post?.summary
@@ -90,299 +92,282 @@ const ShareButtons = ({ post }) => {
return ( return (
<> <>
{services.map(singleService => { {services.map(singleService => {
if (singleService === 'facebook') { switch (singleService) {
return ( case 'facebook':
<FacebookShareButton return (
key={singleService} <FacebookShareButton
url={shareUrl} key={singleService}
className='mx-1'> url={shareUrl}
<FacebookIcon size={32} round /> hashtag={hashTags}
</FacebookShareButton> className='mx-1'>
) <FacebookIcon size={32} round />
} </FacebookShareButton>
if (singleService === 'messenger') { )
return ( case 'messenger':
<FacebookMessengerShareButton return (
key={singleService} <FacebookMessengerShareButton
url={shareUrl} key={singleService}
appId={siteConfig('FACEBOOK_APP_ID')} url={shareUrl}
className='mx-1'> appId={siteConfig('FACEBOOK_APP_ID')}
<FacebookMessengerIcon size={32} round /> className='mx-1'>
</FacebookMessengerShareButton> <FacebookMessengerIcon size={32} round />
) </FacebookMessengerShareButton>
} )
if (singleService === 'line') { case 'line':
return ( return (
<LineShareButton <LineShareButton
key={singleService} key={singleService}
url={shareUrl} url={shareUrl}
className='mx-1'> className='mx-1'>
<LineIcon size={32} round /> <LineIcon size={32} round />
</LineShareButton> </LineShareButton>
) )
} case 'reddit':
if (singleService === 'reddit') { return (
return ( <RedditShareButton
<RedditShareButton key={singleService}
key={singleService} url={shareUrl}
url={shareUrl} title={titleWithSiteInfo}
title={titleWithSiteInfo} windowWidth={660}
windowWidth={660} windowHeight={460}
windowHeight={460} className='mx-1'>
className='mx-1'> <RedditIcon size={32} round />
<RedditIcon size={32} round /> </RedditShareButton>
</RedditShareButton> )
) case 'email':
} return (
if (singleService === 'email') { <EmailShareButton
return ( key={singleService}
<EmailShareButton url={shareUrl}
key={singleService} subject={titleWithSiteInfo}
url={shareUrl} body={body}
subject={titleWithSiteInfo} className='mx-1'>
body={body} <EmailIcon size={32} round />
className='mx-1'> </EmailShareButton>
<EmailIcon size={32} round /> )
</EmailShareButton> case 'twitter':
) return (
} <TwitterShareButton
if (singleService === 'twitter') { key={singleService}
return ( url={shareUrl}
<TwitterShareButton title={titleWithSiteInfo}
key={singleService} hashtags={tags}
url={shareUrl} className='mx-1'>
title={titleWithSiteInfo} <TwitterIcon size={32} round />
className='mx-1'> </TwitterShareButton>
<TwitterIcon size={32} round /> )
</TwitterShareButton> case 'telegram':
) return (
} <TelegramShareButton
if (singleService === 'telegram') { key={singleService}
return ( url={shareUrl}
<TelegramShareButton title={titleWithSiteInfo}
key={singleService} className='mx-1'>
url={shareUrl} <TelegramIcon size={32} round />
title={titleWithSiteInfo} </TelegramShareButton>
className='mx-1'> )
<TelegramIcon size={32} round /> case 'whatsapp':
</TelegramShareButton> return (
) <WhatsappShareButton
} key={singleService}
if (singleService === 'whatsapp') { url={shareUrl}
return ( title={titleWithSiteInfo}
<WhatsappShareButton separator=':: '
key={singleService} className='mx-1'>
url={shareUrl} <WhatsappIcon size={32} round />
title={titleWithSiteInfo} </WhatsappShareButton>
separator=':: ' )
className='mx-1'> case 'linkedin':
<WhatsappIcon size={32} round /> return (
</WhatsappShareButton> <LinkedinShareButton
) key={singleService}
} url={shareUrl}
if (singleService === 'linkedin') { className='mx-1'>
return ( <LinkedinIcon size={32} round />
<LinkedinShareButton </LinkedinShareButton>
key={singleService} )
url={shareUrl} case 'pinterest':
className='mx-1'> return (
<LinkedinIcon size={32} round /> <PinterestShareButton
</LinkedinShareButton> key={singleService}
) url={shareUrl}
} media={image}
if (singleService === 'pinterest') { className='mx-1'>
return ( <PinterestIcon size={32} round />
<PinterestShareButton </PinterestShareButton>
key={singleService} )
url={shareUrl} case 'vkshare':
media={image} return (
className='mx-1'> <VKShareButton
<PinterestIcon size={32} round /> key={singleService}
</PinterestShareButton> url={shareUrl}
) image={image}
} className='mx-1'>
if (singleService === 'vkshare') { <VKIcon size={32} round />
return ( </VKShareButton>
<VKShareButton )
key={singleService} case 'okshare':
url={shareUrl} return (
image={image} <OKShareButton
className='mx-1'> key={singleService}
<VKIcon size={32} round /> url={shareUrl}
</VKShareButton> image={image}
) className='mx-1'>
} <OKIcon size={32} round />
if (singleService === 'okshare') { </OKShareButton>
return ( )
<OKShareButton case 'tumblr':
key={singleService} return (
url={shareUrl} <TumblrShareButton
image={image} key={singleService}
className='mx-1'> url={shareUrl}
<OKIcon size={32} round /> title={titleWithSiteInfo}
</OKShareButton> tags={tags}
) className='mx-1'>
} <TumblrIcon size={32} round />
if (singleService === 'tumblr') { </TumblrShareButton>
return ( )
<TumblrShareButton case 'livejournal':
key={singleService} return (
url={shareUrl} <LivejournalShareButton
title={titleWithSiteInfo} key={singleService}
className='mx-1'> url={shareUrl}
<TumblrIcon size={32} round /> title={titleWithSiteInfo}
</TumblrShareButton> description={shareUrl}
) className='mx-1'>
} <LivejournalIcon size={32} round />
if (singleService === 'livejournal') { </LivejournalShareButton>
return ( )
<LivejournalShareButton case 'mailru':
key={singleService} return (
url={shareUrl} <MailruShareButton
title={titleWithSiteInfo} key={singleService}
description={shareUrl} url={shareUrl}
className='mx-1'> title={titleWithSiteInfo}
<LivejournalIcon size={32} round /> className='mx-1'>
</LivejournalShareButton> <MailruIcon size={32} round />
) </MailruShareButton>
} )
if (singleService === 'mailru') { case 'viber':
return ( return (
<MailruShareButton <ViberShareButton
key={singleService} key={singleService}
url={shareUrl} url={shareUrl}
title={titleWithSiteInfo} title={titleWithSiteInfo}
className='mx-1'> className='mx-1'>
<MailruIcon size={32} round /> <ViberIcon size={32} round />
</MailruShareButton> </ViberShareButton>
) )
} case 'workplace':
if (singleService === 'viber') { return (
return ( <WorkplaceShareButton
<ViberShareButton key={singleService}
key={singleService} url={shareUrl}
url={shareUrl} quote={titleWithSiteInfo}
title={titleWithSiteInfo} hashtag={hashTags}
className='mx-1'> className='mx-1'>
<ViberIcon size={32} round /> <WorkplaceIcon size={32} round />
</ViberShareButton> </WorkplaceShareButton>
) )
} case 'weibo':
if (singleService === 'workplace') { return (
return ( <WeiboShareButton
<WorkplaceShareButton key={singleService}
key={singleService} url={shareUrl}
url={shareUrl} title={titleWithSiteInfo}
quote={titleWithSiteInfo} image={image}
className='mx-1'> className='mx-1'>
<WorkplaceIcon size={32} round /> <WeiboIcon size={32} round />
</WorkplaceShareButton> </WeiboShareButton>
) )
} case 'pocket':
if (singleService === 'weibo') { return (
return ( <PocketShareButton
<WeiboShareButton key={singleService}
key={singleService} url={shareUrl}
url={shareUrl} title={titleWithSiteInfo}
title={titleWithSiteInfo} className='mx-1'>
image={image} <PocketIcon size={32} round />
className='mx-1'> </PocketShareButton>
<WeiboIcon size={32} round /> )
</WeiboShareButton> case 'instapaper':
) return (
} <InstapaperShareButton
if (singleService === 'pocket') { key={singleService}
return ( url={shareUrl}
<PocketShareButton title={titleWithSiteInfo}
key={singleService} className='mx-1'>
url={shareUrl} <InstapaperIcon size={32} round />
title={titleWithSiteInfo} </InstapaperShareButton>
className='mx-1'> )
<PocketIcon size={32} round /> case 'hatena':
</PocketShareButton> return (
) <HatenaShareButton
} key={singleService}
if (singleService === 'instapaper') { url={shareUrl}
return ( title={titleWithSiteInfo}
<InstapaperShareButton windowWidth={660}
key={singleService} windowHeight={460}
url={shareUrl} className='mx-1'>
title={titleWithSiteInfo} <HatenaIcon size={32} round />
className='mx-1'> </HatenaShareButton>
<InstapaperIcon size={32} round /> )
</InstapaperShareButton> case 'qq':
) return (
} <button
if (singleService === 'hatena') { key={singleService}
return ( className='cursor-pointer bg-blue-600 text-white rounded-full mx-1'>
<HatenaShareButton <a
key={singleService} target='_blank'
url={shareUrl} rel='noreferrer'
title={titleWithSiteInfo} aria-label='Share by QQ'
windowWidth={660} href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${title}&desc=${body}`}>
windowHeight={460} <i className='fab fa-qq w-8' />
className='mx-1'> </a>
<HatenaIcon size={32} round /> </button>
</HatenaShareButton> )
) case 'wechat':
} return (
if (singleService === 'qq') { <button
return ( onMouseEnter={openPopover}
<button onMouseLeave={closePopover}
key={singleService} aria-label={singleService}
className='cursor-pointer bg-blue-600 text-white rounded-full mx-1'> key={singleService}
<a className='cursor-pointer bg-green-600 text-white rounded-full mx-1'>
target='_blank' <div id='wechat-button'>
rel='noreferrer' <i className='fab fa-weixin w-8' />
aria-label='Share by QQ'
href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${title}&desc=${body}`}>
<i className='fab fa-qq w-8' />
</a>
</button>
)
}
if (singleService === 'wechat') {
return (
<button
onMouseEnter={openPopover}
onMouseLeave={closePopover}
aria-label={singleService}
key={singleService}
className='cursor-pointer bg-green-600 text-white rounded-full mx-1'>
<div id='wechat-button'>
<i className='fab fa-weixin w-8' />
</div>
<div className='absolute'>
<div
id='pop'
className={
(qrCodeShow ? 'opacity-100 ' : ' invisible opacity-0') +
' z-40 absolute bottom-10 -left-10 bg-white shadow-xl transition-all duration-200 text-center'
}>
<div className='p-2 mt-1 w-28 h-28'>
{qrCodeShow && <QrCode value={shareUrl} />}
</div>
<span className='text-black font-semibold p-1 rounded-t-lg text-sm mx-auto mb-1'>
{locale.COMMON.SCAN_QR_CODE}
</span>
</div> </div>
</div> <div className='absolute'>
</button> <div
) id='pop'
className={
(qrCodeShow ? 'opacity-100 ' : ' invisible opacity-0') +
' z-40 absolute bottom-10 -left-10 bg-white shadow-xl transition-all duration-200 text-center'
}>
<div className='p-2 mt-1 w-28 h-28'>
{qrCodeShow && <QrCode value={shareUrl} />}
</div>
<span className='text-black font-semibold p-1 rounded-t-lg text-sm mx-auto mb-1'>
{locale.COMMON.SCAN_QR_CODE}
</span>
</div>
</div>
</button>
)
case 'link':
return (
<button
aria-label={singleService}
key={singleService}
className='cursor-pointer bg-yellow-500 text-white rounded-full mx-1'>
<div alt={locale.COMMON.URL_COPIED} onClick={copyUrl}>
<i className='fas fa-link w-8' />
</div>
</button>
)
default:
return <></>
} }
if (singleService === 'link') {
return (
<button
aria-label={singleService}
key={singleService}
className='cursor-pointer bg-yellow-500 text-white rounded-full mx-1'>
<div alt={locale.COMMON.URL_COPIED} onClick={copyUrl}>
<i className='fas fa-link w-8' />
</div>
</button>
)
}
return <></>
})} })}
</> </>
) )

View File

@@ -7,6 +7,10 @@ module.exports = {
// TAILWINDCSS 配置的自定义颜色,作废 // TAILWINDCSS 配置的自定义颜色,作废
BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
BACKGROUND_DARK: '#000000', // use hex value, don't forget '#' BACKGROUND_DARK: '#000000', // use hex value, don't forget '#'
// Redis 缓存数据库地址
REDIS_URL: process.env.REDIS_URL || '',
ENABLE_CACHE: ENABLE_CACHE:
process.env.ENABLE_CACHE || process.env.ENABLE_CACHE ||
process.env.npm_lifecycle_event === 'build' || process.env.npm_lifecycle_event === 'build' ||

View File

@@ -2,6 +2,13 @@
* 一些插件 * 一些插件
*/ */
module.exports = { module.exports = {
// 网站全文搜索
ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || null, // 在这里查看 https://dashboard.algolia.com/account/api-keys/
ALGOLIA_ADMIN_APP_KEY: process.env.ALGOLIA_ADMIN_APP_KEY || null, // 管理后台的KEY不要暴露在代码中在这里查看 https://dashboard.algolia.com/account/api-keys/
ALGOLIA_SEARCH_ONLY_APP_KEY:
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY
ALGOLIA_INDEX: process.env.NEXT_PUBLIC_ALGOLIA_INDEX || null, // 在Algolia中创建一个index用作数据库
// AI 文章摘要生成 // AI 文章摘要生成
AI_SUMMARY_API: process.env.AI_SUMMARY_API || '', AI_SUMMARY_API: process.env.AI_SUMMARY_API || '',

View File

@@ -1,6 +1,55 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import FileCache from './local_file_cache' import FileCache from './local_file_cache'
import MemoryCache from './memory_cache' import MemoryCache from './memory_cache'
import RedisCache from './redis_cache'
// 配置是否开启Vercel环境中的缓存因为Vercel中现有两种缓存方式在无服务环境下基本都是无意义的纯粹的浪费资源
const enableCacheInVercel =
process.env.npm_lifecycle_event === 'build' ||
process.env.npm_lifecycle_event === 'export' ||
!BLOG['isProd']
/**
* 尝试从缓存中获取数据,如果没有则尝试获取数据并写入缓存,最终返回所需数据
* @param key
* @param getDataFunction
* @param getDataArgs
* @returns {Promise<*|null>}
*/
export async function getOrSetDataWithCache(
key,
getDataFunction,
...getDataArgs
) {
return getOrSetDataWithCustomCache(key, null, getDataFunction, ...getDataArgs)
}
/**
* 尝试从缓存中获取数据,如果没有则尝试获取数据并自定义写入缓存,最终返回所需数据
* @param key
* @param customCacheTime
* @param getDataFunction
* @param getDataArgs
* @returns {Promise<*|null>}
*/
export async function getOrSetDataWithCustomCache(
key,
customCacheTime,
getDataFunction,
...getDataArgs
) {
const dataFromCache = await getDataFromCache(key)
if (dataFromCache) {
console.log('[缓存-->>API]:', key)
return dataFromCache
}
const data = await getDataFunction(...getDataArgs)
if (data) {
console.log('[API-->>缓存]:', key)
await setDataToCache(key, data, customCacheTime)
}
return data || null
}
/** /**
* 为减少频繁接口请求notion数据将被缓存 * 为减少频繁接口请求notion数据将被缓存
@@ -20,8 +69,15 @@ export async function getDataFromCache(key, force) {
} }
} }
/**
* 写入缓存
* @param {*} key
* @param {*} data
* @param {*} customCacheTime
* @returns
*/
export async function setDataToCache(key, data, customCacheTime) { export async function setDataToCache(key, data, customCacheTime) {
if (!data) { if (!enableCacheInVercel || !data) {
return return
} }
// console.trace('[API-->>缓存写入]:', key) // console.trace('[API-->>缓存写入]:', key)
@@ -39,8 +95,10 @@ export async function delCacheData(key) {
* 缓存实现类 * 缓存实现类
* @returns * @returns
*/ */
function getApi() { export function getApi() {
if (process.env.ENABLE_FILE_CACHE) { if (BLOG.REDIS_URL) {
return RedisCache
} else if (process.env.ENABLE_FILE_CACHE) {
return FileCache return FileCache
} else { } else {
return MemoryCache return MemoryCache

41
lib/cache/redis_cache.js vendored Normal file
View File

@@ -0,0 +1,41 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import Redis from 'ioredis'
export const redisClient = BLOG.REDIS_URL ? new Redis(BLOG.REDIS_URL) : {}
const cacheTime = Math.trunc(
siteConfig('NEXT_REVALIDATE_SECOND', BLOG.NEXT_REVALIDATE_SECOND) * 1.5
)
export async function getCache(key) {
try {
const data = await redisClient.get(key)
return data ? JSON.parse(data) : null
} catch (e) {
console.error('redisClient读取失败 ' + e)
}
}
export async function setCache(key, data, customCacheTime) {
try {
await redisClient.set(
key,
JSON.stringify(data),
'EX',
customCacheTime || cacheTime
)
} catch (e) {
console.error('redisClient写入失败 ' + e)
}
}
export async function delCache(key) {
try {
await redisClient.del(key)
} catch (e) {
console.error('redisClient删除失败 ' + e)
}
}
export default { getCache, setCache, delCache }

View File

@@ -43,6 +43,7 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
case 'AI_SUMMARY_KEY': case 'AI_SUMMARY_KEY':
case 'AI_SUMMARY_CACHE_TIME': case 'AI_SUMMARY_CACHE_TIME':
case 'AI_SUMMARY_WORD_LIMIT': case 'AI_SUMMARY_WORD_LIMIT':
case 'UUID_REDIRECT':
// LINK比较特殊 // LINK比较特殊
if (key === 'LINK') { if (key === 'LINK') {
if (!extendConfig || Object.keys(extendConfig).length === 0) { if (!extendConfig || Object.keys(extendConfig).length === 0) {

View File

@@ -12,6 +12,7 @@ import { deepClone } from '@/lib/utils'
import { idToUuid } from 'notion-utils' import { idToUuid } from 'notion-utils'
import { siteConfig } from '../config' import { siteConfig } from '../config'
import { extractLangId, extractLangPrefix, getShortId } from '../utils/pageId' import { extractLangId, extractLangPrefix, getShortId } from '../utils/pageId'
import { getOrSetDataWithCache } from '@/lib/cache/cache_manager'
export { getAllTags } from '../notion/getAllTags' export { getAllTags } from '../notion/getAllTags'
export { getPost } from '../notion/getNotionPost' export { getPost } from '../notion/getNotionPost'
@@ -65,14 +66,15 @@ export async function getGlobalData({
*/ */
export async function getSiteDataByPageId({ pageId, from }) { export async function getSiteDataByPageId({ pageId, from }) {
// 获取NOTION原始数据此接支持mem缓存。 // 获取NOTION原始数据此接支持mem缓存。
const pageRecordMap = await getPage(pageId, from) return await getOrSetDataWithCache(
// 将Notion数据按规则转成站点数据 `site_data_${pageId}`,
const data = await converNotionToSiteDate( async (pageId, from) => {
const pageRecordMap = await getPage(pageId, from)
return convertNotionToSiteDate(pageId, from, deepClone(pageRecordMap))
},
pageId, pageId,
from, from
deepClone(pageRecordMap)
) )
return data
} }
/** /**
@@ -139,7 +141,7 @@ const EmptyData = pageId => {
* 这里统一对数据格式化 * 这里统一对数据格式化
* @returns {Promise<JSX.Element|null|*>} * @returns {Promise<JSX.Element|null|*>}
*/ */
async function converNotionToSiteDate(pageId, from, pageRecordMap) { async function convertNotionToSiteDate(pageId, from, pageRecordMap) {
if (!pageRecordMap) { if (!pageRecordMap) {
console.error('can`t get Notion Data ; Which id is: ', pageId) console.error('can`t get Notion Data ; Which id is: ', pageId)
return {} return {}
@@ -273,11 +275,12 @@ async function converNotionToSiteDate(pageId, from, pageRecordMap) {
categoryOptions: getCategoryOptions(schema) categoryOptions: getCategoryOptions(schema)
}) })
// 所有标签 // 所有标签
const tagOptions = getAllTags({ const tagOptions =
allPages, getAllTags({
tagOptions: getTagOptions(schema), allPages,
NOTION_CONFIG tagOptions: getTagOptions(schema),
}) NOTION_CONFIG
}) || null
// 旧的菜单 // 旧的菜单
const customNav = getCustomNav({ const customNav = getCustomNav({
allPages: collectionData.filter( allPages: collectionData.filter(

View File

@@ -1,10 +1,14 @@
import { NotionAPI } from 'notion-client' import { NotionAPI as NotionLibrary } from 'notion-client'
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
export default function getNotionAPI() { const notionAPI = getNotionAPI()
return new NotionAPI({
function getNotionAPI() {
return new NotionLibrary({
activeUser: BLOG.NOTION_ACTIVE_USER || null, activeUser: BLOG.NOTION_ACTIVE_USER || null,
authToken: BLOG.NOTION_TOKEN_V2 || null, authToken: BLOG.NOTION_TOKEN_V2 || null,
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
}) })
} }
export default notionAPI

View File

@@ -1,5 +1,4 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { NotionAPI } from 'notion-client'
import { getDateValue, getTextContent } from 'notion-utils' import { getDateValue, getTextContent } from 'notion-utils'
import formatDate from '../utils/formatDate' import formatDate from '../utils/formatDate'
// import { createHash } from 'crypto' // import { createHash } from 'crypto'
@@ -12,7 +11,7 @@ import {
} from '../utils' } from '../utils'
import { extractLangPrefix } from '../utils/pageId' import { extractLangPrefix } from '../utils/pageId'
import { mapImgUrl } from './mapImage' import { mapImgUrl } from './mapImage'
import getNotionAPI from '@/lib/notion/getNotionAPI' import notionAPI from '@/lib/notion/getNotionAPI'
/** /**
* 获取页面元素成员属性 * 获取页面元素成员属性
@@ -57,12 +56,11 @@ export default async function getPageProperties(
case 'person': { case 'person': {
const rawUsers = val.flat() const rawUsers = val.flat()
const users = [] const users = []
const api = getNotionAPI()
for (let i = 0; i < rawUsers.length; i++) { for (let i = 0; i < rawUsers.length; i++) {
if (rawUsers[i][0][1]) { if (rawUsers[i][0][1]) {
const userId = rawUsers[i][0] const userId = rawUsers[i][0]
const res = await api.getUsers(userId) const res = await notionAPI.getUsers(userId)
const resValue = const resValue =
res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value
const user = { const user = {

View File

@@ -1,8 +1,7 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager' import { getDataFromCache, getOrSetDataWithCache, setDataToCache } from '@/lib/cache/cache_manager'
import { NotionAPI } from 'notion-client'
import { deepClone, delay } from '../utils' import { deepClone, delay } from '../utils'
import getNotionAPI from '@/lib/notion/getNotionAPI' import notionAPI from '@/lib/notion/getNotionAPI'
/** /**
* 获取文章内容 * 获取文章内容
@@ -12,21 +11,28 @@ import getNotionAPI from '@/lib/notion/getNotionAPI'
* @returns * @returns
*/ */
export async function getPage(id, from = null, slice) { export async function getPage(id, from = null, slice) {
const cacheKey = `page_block_${id}` return await getOrSetDataWithCache(
let pageBlock = await getDataFromCache(cacheKey) `page_content_${id}_${slice}`,
if (pageBlock) { async (id, slice) => {
// console.debug('[API<<--缓存]', `from:${from}`, cacheKey) const cacheKey = `page_block_${id}`
return convertNotionBlocksToPost(id, pageBlock, slice) let pageBlock = await getDataFromCache(cacheKey)
} if (pageBlock) {
// console.debug('[API<<--缓存]', `from:${from}`, cacheKey)
return convertNotionBlocksToPost(id, pageBlock, slice)
}
// 抓取最新数据 // 抓取最新数据
pageBlock = await getPageWithRetry(id, from) pageBlock = await getPageWithRetry(id, from)
if (pageBlock) { if (pageBlock) {
await setDataToCache(cacheKey, pageBlock) await setDataToCache(cacheKey, pageBlock)
return convertNotionBlocksToPost(id, pageBlock, slice) return convertNotionBlocksToPost(id, pageBlock, slice)
} }
return pageBlock return pageBlock
},
id,
slice
)
} }
/** /**
@@ -43,9 +49,8 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
retryAttempts < 3 ? `剩余重试次数:${retryAttempts}` : '' retryAttempts < 3 ? `剩余重试次数:${retryAttempts}` : ''
) )
try { try {
const api = getNotionAPI()
const start = new Date().getTime() const start = new Date().getTime()
const pageData = await api.getPage(id) const pageData = await notionAPI.getPage(id)
const end = new Date().getTime() const end = new Date().getTime()
console.log('[API<<--响应]', `耗时:${end - start}ms - from:${from}`) console.log('[API<<--响应]', `耗时:${end - start}ms - from:${from}`)
return pageData return pageData
@@ -163,14 +168,12 @@ export const fetchInBatches = async (ids, batchSize = 100) => {
ids = [ids] ids = [ids]
} }
const api = getNotionAPI()
let fetchedBlocks = {} let fetchedBlocks = {}
for (let i = 0; i < ids.length; i += batchSize) { for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize) const batch = ids.slice(i, i + batchSize)
console.log('[API-->>请求] Fetching missing blocks', batch, ids.length) console.log('[API-->>请求] Fetching missing blocks', batch, ids.length)
const start = new Date().getTime() const start = new Date().getTime()
const pageChunk = await api.getBlocks(batch) const pageChunk = await notionAPI.getBlocks(batch)
const end = new Date().getTime() const end = new Date().getTime()
console.log( console.log(
`[API<<--响应] 耗时:${end - start}ms Fetching missing blocks count:${ids.length} ` `[API<<--响应] 耗时:${end - start}ms Fetching missing blocks count:${ids.length} `

15
lib/redirect.js Normal file
View File

@@ -0,0 +1,15 @@
import fs from 'fs'
export function generateRedirectJson({ allPages }) {
let uuidSlugMap = {}
allPages.forEach(page => {
if (page.type === 'Post' && page.status === 'Published') {
uuidSlugMap[page.id] = page.slug
}
})
try {
fs.writeFileSync('./public/redirect.json', JSON.stringify(uuidSlugMap))
} catch (error) {
console.warn('无法写入文件', error)
}
}

View File

@@ -1,5 +1,8 @@
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { checkStrIsNotionId, getLastPartOfUrl } from '@/lib/utils'
import { idToUuid } from 'notion-utils'
import BLOG from './blog.config'
/** /**
* Clerk 身份验证中间件 * Clerk 身份验证中间件
@@ -30,8 +33,31 @@ const isTenantAdminRoute = createRouteMatcher([
* @returns * @returns
*/ */
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
const noAuthMiddleware = async (req: any, ev: any) => { const noAuthMiddleware = async (req: NextRequest, ev: any) => {
// 如果没有配置 Clerk 相关环境变量,返回一个默认响应或者继续处理请求 // 如果没有配置 Clerk 相关环境变量,返回一个默认响应或者继续处理请求
if (BLOG['UUID_REDIRECT']) {
let redirectJson: Record<string, string> = {}
try {
const response = await fetch(`${req.nextUrl.origin}/redirect.json`)
if (response.ok) {
redirectJson = (await response.json()) as Record<string, string>
}
} catch (err) {
console.error('Error fetching static file:', err)
}
let lastPart = getLastPartOfUrl(req.nextUrl.pathname) as string
if (checkStrIsNotionId(lastPart)) {
lastPart = idToUuid(lastPart)
}
if (lastPart && redirectJson[lastPart]) {
const redirectToUrl = req.nextUrl.clone()
redirectToUrl.pathname = '/' + redirectJson[lastPart]
console.log(
`redirect from ${req.nextUrl.pathname} to ${redirectToUrl.pathname}`
)
return NextResponse.redirect(redirectToUrl, 308)
}
}
return NextResponse.next() return NextResponse.next()
} }
/** /**

View File

@@ -1,6 +1,6 @@
{ {
"name": "notion-next", "name": "notion-next",
"version": "4.8.0", "version": "4.8.1",
"homepage": "https://github.com/tangly1024/NotionNext.git", "homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
@@ -31,6 +31,7 @@
"algoliasearch": "^4.18.0", "algoliasearch": "^4.18.0",
"axios": "^1.7.2", "axios": "^1.7.2",
"feed": "^4.2.2", "feed": "^4.2.2",
"ioredis": "^5.4.2",
"js-md5": "^0.7.3", "js-md5": "^0.7.3",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",

View File

@@ -5,6 +5,7 @@ import { generateRobotsTxt } from '@/lib/robots.txt'
import { generateRss } from '@/lib/rss' import { generateRss } from '@/lib/rss'
import { generateSitemapXml } from '@/lib/sitemap.xml' import { generateSitemapXml } from '@/lib/sitemap.xml'
import { DynamicLayout } from '@/themes/theme' import { DynamicLayout } from '@/themes/theme'
import { generateRedirectJson } from '@/lib/redirect'
/** /**
* 首页布局 * 首页布局
@@ -60,6 +61,10 @@ export async function getStaticProps(req) {
generateRss(props) generateRss(props)
// 生成 // 生成
generateSitemapXml(props) generateSitemapXml(props)
if (siteConfig('UUID_REDIRECT', false, props?.NOTION_CONFIG)) {
// 生成重定向 JSON
generateRedirectJson(props)
}
// 生成全文索引 - 仅在 yarn build 时执行 && process.env.npm_lifecycle_event === 'build' // 生成全文索引 - 仅在 yarn build 时执行 && process.env.npm_lifecycle_event === 'build'

View File

@@ -1,15 +1,19 @@
/* Spoiler text styles */ /* Spoiler text styles with sharp edges */
.spoiler-text { .spoiler-text {
color: transparent; /* 文字透明 */ color: transparent; /* 文字透明 */
background-color: #808080; /* 背景为黑色 */ background-color: #000000; /* 背景为黑色 */
border-color: #808080; border-color: #000000; /* 边框颜色 */
text-decoration-color: #808080; text-decoration-color: #000000; /* 删除线颜色 */
text-emphasis-color: #808080; text-emphasis-color: #000000; /* 强调文字颜色 */
border-radius: 8px; filter: none; /* 移除模糊效果 */
filter: blur(1px); /* 初始模糊 */
--hide-transition: 0.3s ease-out; --hide-transition: 0.3s ease-out;
transition: opacity var(--hide-transition), transition:
filter var(--hide-transition); color var(--hide-transition),
background-color var(--hide-transition),
border-color var(--hide-transition),
text-decoration-color var(--hide-transition),
text-emphasis-color var(--hide-transition),
opacity 0.35s cubic-bezier(.25,.46,.45,.94); /* 平滑过渡 */
} }
.spoiler-text:hover { .spoiler-text:hover {
@@ -19,5 +23,26 @@
text-decoration-color: inherit; text-decoration-color: inherit;
text-emphasis-color: inherit; text-emphasis-color: inherit;
opacity: 1; /* 鼠标悬停时恢复不透明度 */ opacity: 1; /* 鼠标悬停时恢复不透明度 */
filter: blur(0); /* 鼠标悬停时解除模糊 */ }
}
/* Spoiler child elements with transition */
.spoiler-text * {
transition: opacity 0.35s cubic-bezier(.25,.46,.45,.94); /* 子元素透明度平滑过渡 */
}
/* Spoiler when not hovered */
.spoiler-text:not(:hover) {
color: transparent!important; /* 非悬停时文字透明 */
background-color: #000000!important; /* 非悬停时背景为黑色 */
border-color: #000000!important; /* 非悬停时边框为黑色 */
}
.spoiler-text:not(:hover) * {
opacity: 0!important; /* 非悬停时子元素透明 */
}
/* Remove border in non-hover states */
.spoiler-text:not(:hover),
.spoiler-text:not(:hover) * {
border: none!important;
}

View File

@@ -26,7 +26,7 @@ export const CTA = () => {
</span> </span>
</h2> </h2>
<p className='mx-auto mb-6 max-w-[515px] text-base leading-[1.5] text-white'> <p className='mx-auto mb-6 max-w-[515px] text-base leading-[1.5] text-white'>
{siteConfig('STARTER_CTA_DESCRIOTN')} {siteConfig('STARTER_CTA_DESCRIPTION')}
</p> </p>
{siteConfig('STARTER_CTA_BUTTON') && ( {siteConfig('STARTER_CTA_BUTTON') && (
<> <>

View File

@@ -364,7 +364,7 @@ const CONFIG = {
STARTER_CTA_ENABLE: true, STARTER_CTA_ENABLE: true,
STARTER_CTA_TITLE: '你还在等待什么呢?', STARTER_CTA_TITLE: '你还在等待什么呢?',
STARTER_CTA_TITLE_2: '现在开始吧', STARTER_CTA_TITLE_2: '现在开始吧',
STARTER_CTA_DESCRIOTN: STARTER_CTA_DESCRIPTION:
'访问NotionNext的操作文档我们提供了详细的教程帮助你即刻搭建站点', '访问NotionNext的操作文档我们提供了详细的教程帮助你即刻搭建站点',
STARTER_CTA_BUTTON: true, // 是否显示按钮 STARTER_CTA_BUTTON: true, // 是否显示按钮
STARTER_CTA_BUTTON_URL: STARTER_CTA_BUTTON_URL:

View File

@@ -6,7 +6,7 @@ import { useRouter } from 'next/router'
import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils' import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils'
// 在next.config.js中扫描所有主题 // 在next.config.js中扫描所有主题
export const { THEMES = [] } = getConfig().publicRuntimeConfig export const { THEMES = [] } = getConfig()?.publicRuntimeConfig || {}
/** /**
* 获取主题配置 * 获取主题配置

View File

@@ -299,6 +299,11 @@
resolved "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" resolved "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@ioredis/commands@^1.1.1":
version "1.2.0"
resolved "https://mirrors.cloud.tencent.com/npm/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
"@isaacs/cliui@^8.0.2": "@isaacs/cliui@^8.0.2":
version "8.0.2" version "8.0.2"
resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -1238,6 +1243,11 @@ clone-response@^1.0.2:
dependencies: dependencies:
mimic-response "^1.0.0" mimic-response "^1.0.0"
cluster-key-slot@^1.1.0:
version "1.1.2"
resolved "https://mirrors.cloud.tencent.com/npm/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
color-convert@^2.0.1: color-convert@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@@ -1443,6 +1453,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
denque@^2.1.0:
version "2.1.0"
resolved "https://mirrors.cloud.tencent.com/npm/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
didyoumean@^1.2.2: didyoumean@^1.2.2:
version "1.2.2" version "1.2.2"
resolved "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" resolved "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@@ -2404,6 +2419,21 @@ invariant@^2.2.4:
dependencies: dependencies:
loose-envify "^1.0.0" loose-envify "^1.0.0"
ioredis@^5.4.2:
version "5.4.2"
resolved "https://mirrors.cloud.tencent.com/npm/ioredis/-/ioredis-5.4.2.tgz#ebb6f1a10b825b2c0fb114763d7e82114a0bee6c"
integrity sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==
dependencies:
"@ioredis/commands" "^1.1.1"
cluster-key-slot "^1.1.0"
debug "^4.3.4"
denque "^2.1.0"
lodash.defaults "^4.2.0"
lodash.isarguments "^3.1.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
standard-as-callback "^2.1.0"
is-array-buffer@^3.0.4: is-array-buffer@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" resolved "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98"
@@ -2770,6 +2800,16 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" p-locate "^5.0.0"
lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://mirrors.cloud.tencent.com/npm/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==
lodash.isarguments@^3.1.0:
version "3.1.0"
resolved "https://mirrors.cloud.tencent.com/npm/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==
lodash.merge@^4.6.2: lodash.merge@^4.6.2:
version "4.6.2" version "4.6.2"
resolved "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" resolved "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -3604,6 +3644,18 @@ readdirp@~3.6.0:
dependencies: dependencies:
picomatch "^2.2.1" picomatch "^2.2.1"
redis-errors@^1.0.0, redis-errors@^1.2.0:
version "1.2.0"
resolved "https://mirrors.cloud.tencent.com/npm/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
redis-parser@^3.0.0:
version "3.0.0"
resolved "https://mirrors.cloud.tencent.com/npm/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
dependencies:
redis-errors "^1.0.0"
reflect.getprototypeof@^1.0.4, reflect.getprototypeof@^1.0.6: reflect.getprototypeof@^1.0.4, reflect.getprototypeof@^1.0.6:
version "1.0.8" version "1.0.8"
resolved "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.8.tgz#c58afb17a4007b4d1118c07b92c23fca422c5d82" resolved "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.8.tgz#c58afb17a4007b4d1118c07b92c23fca422c5d82"
@@ -3917,6 +3969,11 @@ stacktrace-js@^2.0.2:
stack-generator "^2.0.5" stack-generator "^2.0.5"
stacktrace-gps "^3.0.4" stacktrace-gps "^3.0.4"
standard-as-callback@^2.1.0:
version "2.1.0"
resolved "https://mirrors.cloud.tencent.com/npm/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
std-env@^3.7.0: std-env@^3.7.0:
version "3.8.0" version "3.8.0"
resolved "https://registry.npmmirror.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" resolved "https://registry.npmmirror.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5"
@@ -3927,16 +3984,7 @@ streamsearch@^1.1.0:
resolved "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" resolved "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
version "4.2.3" version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -4017,14 +4065,7 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1" define-properties "^1.2.1"
es-object-atoms "^1.0.0" es-object-atoms "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==