feat: 添加推送到Bing支持 (#2379)

* feat(core): 添加 Bing 搜索推送功能

- 在 errorCode.constant.ts 中添加 Bing 相关的错误代码
- 在 configs.dto.ts 中添加 BingSearchOptionDto 类用于配置 Bing 推送选项
- 在 helper.cron.service.ts 中实现 pushToBingSearch 方法进行 Bing 搜索推送

* feat(core): 添加必应搜索推送配置并优化相关功能

- 在默认配置中添加 bingSearchOptions 项
- 更新 BingSearchOptionDto 类,统一字段命名
- 在 IConfig 接口中添加 bingSearchOptions 字段
- 修改 CronService 中的 pushToBingSearch 方法,适配新配置

* build: 更新下载资源链接

- 将 CDN 下载 URL 从 `https://mirror.ghproxy.com/` 更改为 `https://ghfast.top/`

* feat(api-client): 添加 Bing 搜索配置模型

- 新增 BingSearchOptionsModel 类,用于 Bing 搜索的配置选项
- 该模型包括 enable 和 token 两个属性,与 BaiduSearchOptionsModel 类似

* refactor(configs): 统一配置类命名规则

- 将 BingSearchOptionDto 重命名为 BingSearchOptionsDto,与 BaiduSearchOptionsDto 保持一致
- 更新相关引用和配置字段类型

* feat(core): 添加每日凌晨1点执行的 Bing 搜索推送任务

- 在 CronService 类中添加了 pushToBingSearch 方法
- 使用 @CronOnce 装饰器设置任务执行时间为每天凌晨1点
- 任务名称为 'pushToBingSearch'
- 方法描述为 '推送到Bing'

* chore: 添加.eslintcache到.gitignore

- 在.gitignore文件中添加.eslintcache,避免eslint缓存文件被版本控制

* feat(docs): 更新readme

* refactor(core): 优化 Bing 站长提交日志输出

- 修改了 Bing 站长提交结果的日志输出格式
- 当提交成功时,输出简短的成功日志
- 当提交失败时,仍输出详细的错误信息

* Update apps/core/src/processors/helper/helper.cron.service.ts

Co-authored-by: Innei <tukon479@gmail.com>
Signed-off-by: Teror Fox <i@trfox.top>

* chore: 删除废弃的服务器部署脚本

* feat(core): 添加 Bing API 域名无效错误码

- 在 ErrorCodeEnum 枚举中添加 BingDomainInvalid 错误码
- 在 ErrorCode 对象中添加对应的错误信息和状态码

* refactor(core): 修复 Bing 推送异常时返回值问题

---------

Signed-off-by: Teror Fox <i@trfox.top>
Co-authored-by: Innei <tukon479@gmail.com>
This commit is contained in:
Teror Fox
2025-03-17 23:46:05 +08:00
committed by GitHub
parent 490320ee4f
commit 400d217aac
10 changed files with 83 additions and 5 deletions

2
.gitignore vendored
View File

@@ -51,3 +51,5 @@ bin/process-reporter
dist
dev/
.eslintcache

View File

@@ -166,6 +166,7 @@ ResponseInterceptor -> ResponseFilterInterceptor -> JSONTransformInterceptor ->
1. [CronService] 维护管理计划任务
- 自动备份
- 推送百度搜索
- 推送Bing搜索
- 清除缓存
- etc.
1. [EmailService] 送信服务

View File

@@ -25,6 +25,11 @@ export enum ErrorCodeEnum {
// system
MasterLost = 99998,
BanInDemo = 999999,
//Bing
BingAPIFailed = 300002,
BingKeyInvalid = 300003,
BingDomainInvalid = 300004,
}
export const ErrorCode = Object.freeze<Record<ErrorCodeEnum, [string, number]>>(
@@ -53,5 +58,9 @@ export const ErrorCode = Object.freeze<Record<ErrorCodeEnum, [string, number]>>(
[ErrorCodeEnum.AIResultParsingError]: ['AI 结果解析错误', 500],
[ErrorCodeEnum.EmailTemplateNotFound]: ['邮件模板不存在', 400],
[ErrorCodeEnum.BingAPIFailed]: ['Bing API请求失败', 503],
[ErrorCodeEnum.BingKeyInvalid]: ['Bing API密钥无效', 401],
[ErrorCodeEnum.BingDomainInvalid]: ['Bing API域名无效', 400],
},
)

View File

@@ -51,6 +51,7 @@ export const generateDefaultConfig: () => IConfig = () => ({
secretKey: null!,
},
baiduSearchOptions: { enable: false, token: null! },
bingSearchOptions: { enable: false, token: null! },
algoliaSearchOptions: {
enable: false,
apiKey: '',

View File

@@ -3,11 +3,9 @@ import {
ArrayUnique,
IsBoolean,
IsEmail,
isInt,
IsInt,
IsIP,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
@@ -210,6 +208,20 @@ export class BaiduSearchOptionsDto {
token?: string
}
@JSONSchema({ title: 'Bing推送设定' })
export class BingSearchOptionsDto {
@IsOptional()
@IsBoolean()
@JSONSchemaToggleField('开启推送')
enable?: boolean
@IsOptional()
@IsString()
@SecretField
@JSONSchemaPasswordField('Bing API密钥')
token?: string
}
@JSONSchema({ title: 'Algolia Search' })
export class AlgoliaSearchOptionsDto {
@IsBoolean()

View File

@@ -15,6 +15,7 @@ import {
BackupOptionsDto,
BaiduSearchOptionsDto,
BarkOptionsDto,
BingSearchOptionsDto,
CommentOptionsDto,
FeatureListDto,
FriendLinkOptionsDto,
@@ -67,6 +68,8 @@ export abstract class IConfig {
backupOptions: Required<BackupOptionsDto>
@ConfigField(() => BaiduSearchOptionsDto)
baiduSearchOptions: Required<BaiduSearchOptionsDto>
@ConfigField(() => BingSearchOptionsDto)
bingSearchOptions: Required<BingSearchOptionsDto>
@ConfigField(() => AlgoliaSearchOptionsDto)
algoliaSearchOptions: Required<AlgoliaSearchOptionsDto>

View File

@@ -65,7 +65,7 @@ export class UpdateService {
return
}
const cdnDownloadUrl = `https://mirror.ghproxy.com/${downloadUrl}`
const cdnDownloadUrl = `https://ghfast.top/${downloadUrl}`
// const cdnDownloadUrl = downloadUrl
subscriber.next(

View File

@@ -21,7 +21,6 @@ import { ConfigsService } from '~/modules/configs/configs.service'
import { InjectModel } from '~/transformers/model.transformer'
import { getRedisKey } from '~/utils/redis.util'
import { CacheService } from '../redis/cache.service'
import { RedisService } from '../redis/redis.service'
import { HttpService } from './helper.http.service'
import { JWTService } from './helper.jwt.service'
@@ -160,6 +159,52 @@ export class CronService {
return null
}
@CronOnce(CronExpression.EVERY_DAY_AT_1AM, { name: 'pushToBingSearch' })
@CronDescription('推送到Bing')
async pushToBingSearch() {
const {
url: { webUrl },
bingSearchOptions: configs,
} = await this.configs.waitForConfigReady()
if (!configs.enable) {
return
}
const apiKey = configs.token
if (!apiKey) {
this.logger.error('[BingSearchPushTask] API key 为空')
return
}
const pushUrls = await this.aggregateService.getSiteMapContent()
const urls = pushUrls.map((item) => item.url)
try {
const res = await this.http.axiosRef.post(
`https://ssl.bing.com/webmaster/api.svc/json/SubmitUrlbatch?apikey=${apiKey}`,
{
siteUrl: webUrl,
urlList: urls,
},
{
headers: {
'Content-Type': 'application/json',
charset: 'utf-8',
},
},
)
if (res?.data?.d === null) {
this.logger.log('Bing站长提交成功')
} else {
this.logger.log(`Bing站长提交结果${JSON.stringify(res.data)}`)
}
return res.data
} catch (error) {
this.logger.error(`Bing推送错误${error.message}`)
}
return null
}
@CronDescription('扫表:删除过期 JWT')
@CronOnce(CronExpression.EVERY_DAY_AT_1AM, {
name: 'deleteExpiredJWT',

View File

@@ -37,6 +37,11 @@ export declare class BaiduSearchOptionsModel {
enable: boolean
token?: string
}
export declare class BingSearchOptionsModel {
enable: boolean
token?: string
}
export declare class AlgoliaSearchOptionsModel {
enable: boolean
apiKey?: string

View File

@@ -16,7 +16,7 @@ const { homedir } = os
const { repository } = require('../package.json')
const argv = process.argv.slice(2)
const scpPath = av['scp_path']
const scpPath = av.scp_path
function getOsBuildAssetName() {
return `release-linux.zip`
}