Merge branch 'main' into original-fix

This commit is contained in:
tangly1024
2025-01-01 15:49:15 +08:00
committed by GitHub
355 changed files with 16915 additions and 6775 deletions

View File

@@ -0,0 +1,131 @@
const axios = require('axios')
// 发送 Notion API 请求
async function postNotion(
properties: any,
databaseId: string,
listContentMain: any[],
token: string
) {
const url = 'https://api.notion.com/v1/pages'
const children = listContentMain
.map(contentMain => {
if (contentMain.type === 'paragraph') {
return {
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{ type: 'text', text: { content: contentMain.content } }
]
}
}
} else if (['file', 'image'].includes(contentMain.type)) {
return {
object: 'block',
type: contentMain.type,
[contentMain.type]: {
type: 'external',
external: { url: contentMain.content }
}
}
}
return null
})
.filter(Boolean)
const payload = {
parent: { database_id: databaseId },
properties,
children
}
const headers = {
accept: 'application/json',
'Notion-Version': '2022-06-28',
'content-type': 'application/json',
Authorization: `Bearer ${token}`
}
try {
const response = await axios.post(url, payload, { headers })
return response
} catch (error: any) {
console.error('写入Notion异常', error)
throw new Error(`Error posting to Notion: ${error.message}`)
}
}
// 处理响应结果
function responseResult(response: { status: number; data: any }) {
if (response.status === 200) {
console.log('成功...')
console.log(response.data)
} else {
console.log('失败...')
console.log(response.data)
}
}
// 准备属性字段
function notionProperty(
id: any,
avatar: any,
name: any,
mail: any,
lastLoginTime: any,
token: any
) {
return {
id: {
rich_text: [
{
type: 'text',
text: {
content: id,
link: null
}
}
]
},
avatar: {
files: [
{
name: 'Project Alpha blueprint',
external: {
url: avatar
}
}
]
},
name: {
title: [
{
text: {
content: name
}
}
]
},
mail: {
email: mail
},
last_login_time: {
date: {
start: lastLoginTime
}
},
token: {
rich_text: [
{
type: 'text',
text: {
content: token,
link: null
}
}
]
}
}
}

View File

@@ -1,5 +1,6 @@
import { idToUuid } from 'notion-utils'
import { checkStrIsNotionId, getLastPartOfUrl, isBrowser } from '../utils'
import { loadLangFromLocalStorage } from '@/lib/lang'
/**
* 处理页面内连接跳转:
@@ -7,45 +8,46 @@ import { checkStrIsNotionId, getLastPartOfUrl, isBrowser } from '../utils'
* 2.url是notion-id转成站内文章链接
*/
export const convertInnerUrl = allPages => {
if (isBrowser) {
const allAnchorTags = document
?.getElementById('notion-article')
?.getElementsByTagName('a')
if (!isBrowser) {
return
}
const allAnchorTags = document
?.getElementById('notion-article')
?.querySelectorAll('a.notion-link')
if (!allAnchorTags) {
return
}
const currentURL = window.location.origin + window.location.pathname
if (!allAnchorTags) {
return
}
const { origin, pathname } = window.location;
const currentURL = origin + pathname
const currentPathLang = pathname.split('/').filter(Boolean)[0]
const lang = loadLangFromLocalStorage().split(/[-_]/)[0]
const langPrefix = lang === currentPathLang ? '/' + lang : ''
for (const anchorTag of allAnchorTags) {
// url替换成slug
for (const anchorTag of allAnchorTags) {
// 检查url
if (anchorTag?.href) {
// 如果url是一个Notion_id尝试匹配成博客的文章内链
const slug = getLastPartOfUrl(anchorTag.href)
if (checkStrIsNotionId(slug)) {
const slugPage = allPages?.find(page => {
return idToUuid(slug).indexOf(page.short_id) === 0
})
if (slugPage) {
anchorTag.href = slugPage?.href
}
if (anchorTag?.href) {
// 如果url是一个Notion_id尝试匹配成博客的文章内链
const slug = getLastPartOfUrl(anchorTag.href)
if (checkStrIsNotionId(slug)) {
const slugPage = allPages?.find(page => {
return idToUuid(slug).indexOf(page.short_id) === 14
})
if (slugPage) {
anchorTag.href = langPrefix + slugPage?.href
}
}
}
// 链接在当前页面打开
for (const anchorTag of allAnchorTags) {
if (anchorTag?.target === '_blank') {
const hrefWithoutQueryHash = anchorTag.href.split('?')[0].split('#')[0]
const hrefWithRelativeHash =
currentURL.split('#')[0] || '' + anchorTag.href.split('#')[1] || ''
if (
currentURL === hrefWithoutQueryHash ||
currentURL === hrefWithRelativeHash
) {
anchorTag.target = '_self'
}
if (anchorTag?.target === '_blank') {
const hrefWithoutQueryHash = anchorTag.href.split('?')[0].split('#')[0]
const hrefWithRelativeHash =
currentURL.split('#')[0] || '' + anchorTag.href.split('#')[1] || ''
if (
currentURL === hrefWithoutQueryHash ||
currentURL === hrefWithRelativeHash
) {
anchorTag.target = '_self'
}
}
}
}
}

View File

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

View File

@@ -36,7 +36,7 @@ export async function getConfigMapFromConfigPage(allPages) {
})
if (!configPage) {
console.warn('[Notion配置] 未找到配置页面')
// console.warn('[Notion配置] 未找到配置页面')
return null
}
const configPageId = configPage.id
@@ -51,11 +51,11 @@ export async function getConfigMapFromConfigPage(allPages) {
}
if (!content) {
console.warn(
'[Notion配置] 未找到配置表格',
pageRecordMap.block[configPageId],
pageRecordMap.block[configPageId].value
)
// console.warn(
// '[Notion配置] 未找到配置表格',
// pageRecordMap.block[configPageId],
// pageRecordMap.block[configPageId].value
// )
return null
}
@@ -66,11 +66,11 @@ export async function getConfigMapFromConfigPage(allPages) {
// eslint-disable-next-line no-constant-condition, no-self-compare
if (!configTableId) {
console.warn(
'[Notion配置]未找到配置表格数据',
pageRecordMap.block[configPageId],
pageRecordMap.block[configPageId].value
)
// console.warn(
// '[Notion配置]未找到配置表格数据',
// pageRecordMap.block[configPageId],
// pageRecordMap.block[configPageId].value
// )
return null
}
@@ -157,17 +157,24 @@ export async function getConfigMapFromConfigPage(allPages) {
// 只导入生效的配置
if (config.enable) {
// console.log('[Notion配置]', config.key, config.value)
notionConfig[config.key] = config.value
notionConfig[config.key] =
parseTextToJson(config.value) || config.value || null
// 配置不能是undefined至少是null
}
}
}
// 最后检查Notion_Config页面的INLINE_CONFIG是否是一个js对象
const combine = Object.assign(
{},
deepClone(notionConfig),
parseConfig(notionConfig?.INLINE_CONFIG)
)
let combine = notionConfig
try {
// 将INLINE_CONFIG合并@see https://docs.tangly1024.com/article/notion-next-inline-config
combine = Object.assign(
{},
deepClone(notionConfig),
notionConfig?.INLINE_CONFIG
)
} catch (err) {
console.warn('解析 INLINE_CONFIG 配置时出错,请检查JSON格式', err)
}
return combine
}
@@ -186,7 +193,23 @@ export function parseConfig(configString) {
const config = eval('(' + configString + ')')
return config
} catch (evalError) {
console.error('解析 eval(INLINE_CONFIG) 配置时出错:', evalError)
console.warn(
'解析 eval(INLINE_CONFIG) 配置时出错,请检查JSON格式',
evalError
)
return {}
}
}
/**
* 解析文本为JSON
* @param text
* @returns {any|null}
*/
export function parseTextToJson(text) {
try {
return JSON.parse(text)
} catch (error) {
return null
}
}

View File

@@ -6,12 +6,13 @@ import formatDate from '../utils/formatDate'
import md5 from 'js-md5'
import { siteConfig } from '../config'
import {
checkStartWithHttp,
convertUrlStartWithOneSlash,
getLastSegmentFromUrl
checkStartWithHttp,
convertUrlStartWithOneSlash,
getLastSegmentFromUrl
} from '../utils'
import { extractLangPrefix } from '../utils/pageId'
import { mapImgUrl } from './mapImage'
import getNotionAPI from '@/lib/notion/getNotionAPI'
/**
* 获取页面元素成员属性
@@ -56,7 +57,7 @@ export default async function getPageProperties(
case 'person': {
const rawUsers = val.flat()
const users = []
const api = new NotionAPI({ authToken })
const api = getNotionAPI()
for (let i = 0; i < rawUsers.length; i++) {
if (rawUsers[i][0][1]) {
@@ -149,20 +150,25 @@ function convertToJSON(str) {
* 映射用户自定义表头
*/
function mapProperties(properties) {
if (properties?.type === BLOG.NOTION_PROPERTY_NAME.type_post) {
properties.type = 'Post'
const typeMap = {
[BLOG.NOTION_PROPERTY_NAME.type_post]: 'Post',
[BLOG.NOTION_PROPERTY_NAME.type_page]: 'Page',
[BLOG.NOTION_PROPERTY_NAME.type_notice]: 'Notice',
[BLOG.NOTION_PROPERTY_NAME.type_menu]: 'Menu',
[BLOG.NOTION_PROPERTY_NAME.type_sub_menu]: 'SubMenu'
}
if (properties?.type === BLOG.NOTION_PROPERTY_NAME.type_page) {
properties.type = 'Page'
const statusMap = {
[BLOG.NOTION_PROPERTY_NAME.status_publish]: 'Published',
[BLOG.NOTION_PROPERTY_NAME.status_invisible]: 'Invisible'
}
if (properties?.type === BLOG.NOTION_PROPERTY_NAME.type_notice) {
properties.type = 'Notice'
if (properties?.type && typeMap[properties.type]) {
properties.type = typeMap[properties.type]
}
if (properties?.status === BLOG.NOTION_PROPERTY_NAME.status_publish) {
properties.status = 'Published'
}
if (properties?.status === BLOG.NOTION_PROPERTY_NAME.status_invisible) {
properties.status = 'Invisible'
if (properties?.status && statusMap[properties.status]) {
properties.status = statusMap[properties.status]
}
}
@@ -175,9 +181,7 @@ export function adjustPageProperties(properties, NOTION_CONFIG) {
// 1.按照用户配置的URL_PREFIX 转换一下slug
// 2.为文章添加一个href字段存储最终调整的路径
if (properties.type === 'Post') {
if (siteConfig('POST_URL_PREFIX', '', NOTION_CONFIG)) {
properties.slug = generateCustomizeSlug(properties, NOTION_CONFIG)
}
properties.slug = generateCustomizeSlug(properties, NOTION_CONFIG)
properties.href = properties.slug ?? properties.id
} else if (properties.type === 'Page') {
properties.href = properties.slug ?? properties.id
@@ -240,11 +244,16 @@ function generateCustomizeSlug(postProperties, NOTION_CONFIG) {
return postProperties.slug
}
let fullPrefix = ''
const allSlugPatterns = siteConfig(
'POST_URL_PREFIX',
'',
NOTION_CONFIG
).split('/')
let allSlugPatterns = NOTION_CONFIG?.POST_URL_PREFIX
if (allSlugPatterns === undefined || allSlugPatterns === null) {
allSlugPatterns = siteConfig(
'POST_URL_PREFIX',
BLOG.POST_URL_PREFIX,
NOTION_CONFIG
).split('/')
} else {
allSlugPatterns = allSlugPatterns.split('/')
}
const POST_URL_PREFIX_MAPPING_CATEGORY = siteConfig(
'POST_URL_PREFIX_MAPPING_CATEGORY',
@@ -291,9 +300,9 @@ function generateCustomizeSlug(postProperties, NOTION_CONFIG) {
fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1) // 去掉尾部部的"/"
}
if(fullPrefix){
if (fullPrefix) {
return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`
}else{
} else {
return `${postProperties.slug ?? postProperties.id}`
}
}

View File

@@ -12,7 +12,7 @@ const indentLevels = {
* H1, H2, and H3 elements.
*/
export const getPageTableOfContents = (page, recordMap) => {
const contents = (page.content ?? [])
const contents = page.content ?? []
const toc = getBlockHeader(contents, recordMap)
const indentLevelStack = [
{
@@ -69,20 +69,28 @@ function getBlockHeader(contents, recordMap, toc) {
continue
}
const { type } = block
if (type.indexOf('header') >= 0) {
const existed = toc.find(e => e.id === blockId)
if (!existed) {
toc.push({
id: blockId,
type,
text: getTextContent(block.properties?.title),
indentLevel: indentLevels[type]
})
}
}
if (block.content?.length > 0) {
getBlockHeader(block.content, recordMap, toc)
} else {
if (type.indexOf('header') >= 0) {
const existed = toc.find(e => e.id === blockId)
if (!existed) {
toc.push({
id: blockId,
type,
text: getTextContent(block.properties?.title),
indentLevel: indentLevels[type]
})
}
} else if (type === 'transclusion_reference') {
getBlockHeader(
[block.format.transclusion_reference_pointer.id],
recordMap,
toc
)
} else if (type === 'transclusion_container') {
getBlockHeader(block.content, recordMap, toc)
}
}
}

View File

@@ -2,6 +2,7 @@ import BLOG from '@/blog.config'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
import { NotionAPI } from 'notion-client'
import { deepClone, delay } from '../utils'
import getNotionAPI from '@/lib/notion/getNotionAPI'
/**
* 获取文章内容
@@ -10,19 +11,20 @@ import { deepClone, delay } from '../utils'
* @param {*} slice
* @returns
*/
export async function getPage(id, from, slice) {
const cacheKey = 'page_block_' + id
export async function getPage(id, from = null, slice) {
const cacheKey = `page_block_${id}`
let pageBlock = await getDataFromCache(cacheKey)
if (pageBlock) {
// console.log('[API<<--缓存]', `from:${from}`, cacheKey)
return filterPostBlocks(id, pageBlock, slice)
// console.debug('[API<<--缓存]', `from:${from}`, cacheKey)
return convertNotionBlocksToPost(id, pageBlock, slice)
}
// 抓取最新数据
pageBlock = await getPageWithRetry(id, from)
if (pageBlock) {
await setDataToCache(cacheKey, pageBlock)
return filterPostBlocks(id, pageBlock, slice)
return convertNotionBlocksToPost(id, pageBlock, slice)
}
return pageBlock
}
@@ -41,11 +43,7 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
retryAttempts < 3 ? `剩余重试次数:${retryAttempts}` : ''
)
try {
const authToken = BLOG.NOTION_ACCESS_TOKEN || null
const api = new NotionAPI({
authToken,
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
})
const api = getNotionAPI()
const start = new Date().getTime()
const pageData = await api.getPage(id)
const end = new Date().getTime()
@@ -69,7 +67,7 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
}
/**
* 获取到的页面BLOCK特殊处理
* Notion页面BLOCK格式化处理
* 1.删除冗余字段
* 2.比如文件、视频、音频、url格式化
* 3.代码块等元素兼容
@@ -78,72 +76,72 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
* @param {*} slice 截取数量
* @returns
*/
function filterPostBlocks(id, blockMap, slice) {
function convertNotionBlocksToPost(id, blockMap, slice) {
const clonePageBlock = deepClone(blockMap)
let count = 0
const blocksToProcess = Object.keys(clonePageBlock?.block || {})
// 循环遍历文档的每个block
for (let i = 0; i < blocksToProcess.length; i++) {
const blockId = blocksToProcess[i]
const b = clonePageBlock?.block[blockId]
if (slice && slice > 0 && count > slice) {
delete clonePageBlock?.block[blockId]
continue
}
// 当BlockId等于PageId时移除
if (b?.value?.id === id) {
// 此block含有敏感信息
delete b?.value?.properties
continue
}
count++
if (b?.value?.type === 'sync_block' && b?.value?.children) {
const childBlocks = b.value.children
// 移除同步块
delete clonePageBlock.block[blockId]
// 用子块替代同步块
childBlocks.forEach((childBlock, index) => {
const newBlockId = `${blockId}_child_${index}`
clonePageBlock.block[newBlockId] = childBlock
blocksToProcess.splice(i + index + 1, 0, newBlockId)
})
// 重新处理新加入的子块
i--
continue
}
// 处理 c++、c#、汇编等语言名字映射
if (b?.value?.type === 'code') {
if (b?.value?.properties?.language?.[0][0] === 'C++') {
b.value.properties.language[0][0] = 'cpp'
const blockId = blocksToProcess[i]
const b = clonePageBlock?.block[blockId]
if (slice && slice > 0 && count > slice) {
delete clonePageBlock?.block[blockId]
continue
}
if (b?.value?.properties?.language?.[0][0] === 'C#') {
b.value.properties.language[0][0] = 'csharp'
// 当BlockId等于PageId时移除
if (b?.value?.id === id) {
// 此block含有敏感信息
delete b?.value?.properties
continue
}
if (b?.value?.properties?.language?.[0][0] === 'Assembly') {
b.value.properties.language[0][0] = 'asm6502'
count++
if (b?.value?.type === 'sync_block' && b?.value?.children) {
const childBlocks = b.value.children
// 移除同步块
delete clonePageBlock.block[blockId]
// 用子块替代同步块
childBlocks.forEach((childBlock, index) => {
const newBlockId = `${blockId}_child_${index}`
clonePageBlock.block[newBlockId] = childBlock
blocksToProcess.splice(i + index + 1, 0, newBlockId)
})
// 重新处理新加入的子块
i--
continue
}
// 处理 c++、c#、汇编等语言名字映射
if (b?.value?.type === 'code') {
if (b?.value?.properties?.language?.[0][0] === 'C++') {
b.value.properties.language[0][0] = 'cpp'
}
if (b?.value?.properties?.language?.[0][0] === 'C#') {
b.value.properties.language[0][0] = 'csharp'
}
if (b?.value?.properties?.language?.[0][0] === 'Assembly') {
b.value.properties.language[0][0] = 'asm6502'
}
}
// 如果是文件或嵌入式PDF需要重新加密签名
if (
(b?.value?.type === 'file' ||
b?.value?.type === 'pdf' ||
b?.value?.type === 'video' ||
b?.value?.type === 'audio') &&
b?.value?.properties?.source?.[0][0] &&
b?.value?.properties?.source?.[0][0].indexOf('amazonaws.com') > 0
) {
const oldUrl = b?.value?.properties?.source?.[0][0]
const newUrl = `https://notion.so/signed/${encodeURIComponent(oldUrl)}?table=block&id=${b?.value?.id}`
b.value.properties.source[0][0] = newUrl
}
}
// 如果是文件或嵌入式PDF需要重新加密签名
if (
(b?.value?.type === 'file' ||
b?.value?.type === 'pdf' ||
b?.value?.type === 'video' ||
b?.value?.type === 'audio') &&
b?.value?.properties?.source?.[0][0] &&
b?.value?.properties?.source?.[0][0].indexOf('amazonaws.com') > 0
) {
const oldUrl = b?.value?.properties?.source?.[0][0]
const newUrl = `https://notion.so/signed/${encodeURIComponent(oldUrl)}?table=block&id=${b?.value?.id}`
b.value.properties.source[0][0] = newUrl
}
}
// 去掉不用的字段
if (id === BLOG.NOTION_PAGE_ID) {
@@ -165,11 +163,7 @@ export const fetchInBatches = async (ids, batchSize = 100) => {
ids = [ids]
}
const authToken = BLOG.NOTION_ACCESS_TOKEN || null
const api = new NotionAPI({
authToken,
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
})
const api = getNotionAPI()
let fetchedBlocks = {}
for (let i = 0; i < ids.length; i += batchSize) {