feat: ai writer helper module

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei
2024-05-04 22:04:36 +08:00
parent d0225fe70a
commit f8909bd8c9
18 changed files with 436 additions and 288 deletions

View File

@@ -20,6 +20,7 @@ export enum ErrorCodeEnum {
AIKeyExpired = 200001,
AIException = 200002,
AIProcessing = 200003,
AIResultParsingError = 200004,
// system
MasterLost = 99998,
@@ -44,10 +45,12 @@ export const ErrorCode = Object.freeze<Record<ErrorCodeEnum, [string, number]>>(
],
[ErrorCodeEnum.MineZip]: ['文件格式必须是 zip 类型', 422],
[ErrorCodeEnum.AINotEnabled]: ['AI 功能未开启', 400],
[ErrorCodeEnum.AIKeyExpired]: ['AI Key 已过期,请联系管理员', 400],
[ErrorCodeEnum.AIException]: ['AI 服务异常', 500],
[ErrorCodeEnum.AIProcessing]: ['AI 正在处理此请求,请稍后再试', 400],
[ErrorCodeEnum.AIResultParsingError]: ['AI 结果解析错误', 500],
[ErrorCodeEnum.EmailTemplateNotFound]: ['邮件模板不存在', 400],
},

View File

@@ -10,48 +10,48 @@ import {
} from '@nestjs/common'
import { ApiController } from '~/common/decorators/api-controller.decorator'
import { Auth, AuthButProd } from '~/common/decorators/auth.decorator'
import { Auth } from '~/common/decorators/auth.decorator'
import { BizException } from '~/common/exceptions/biz.exception'
import { ErrorCodeEnum } from '~/constants/error-code.constant'
import { MongoIdDto } from '~/shared/dto/id.dto'
import { PagerDto } from '~/shared/dto/pager.dto'
import { FastifyBizRequest } from '~/transformers/get-req.transformer'
import { ConfigsService } from '../configs/configs.service'
import { ConfigsService } from '../../configs/configs.service'
import { DEFAULT_SUMMARY_LANG } from '../ai.constants'
import {
GenerateAiSummaryDto,
GetSummaryQueryDto,
UpdateSummaryDto,
} from './ai.dto'
import { AiService } from './ai.service'
import { DEFAULT_SUMMARY_LANG } from './ai.constants'
} from './ai-summary.dto'
import { AiSummaryService } from './ai-summary.service'
@ApiController('ai')
export class AiController {
@ApiController('ai/summaries')
export class AiSummaryController {
constructor(
private readonly service: AiService,
private readonly service: AiSummaryService,
private readonly configService: ConfigsService,
) {}
@Post('/generate-summary')
@Post('/generate')
@Auth()
generateSummary(@Body() body: GenerateAiSummaryDto) {
return this.service.generateSummaryByOpenAI(body.refId, body.lang)
}
@Get('/summaries/ref/:id')
@Get('/ref/:id')
@Auth()
async getSummaryByRefId(@Param() params: MongoIdDto) {
return this.service.getSummariesByRefId(params.id)
}
@Get('/summaries')
@Get('/')
@Auth()
async getSummaries(@Query() query: PagerDto) {
return this.service.getAllSummaries(query)
}
@Patch('/summaries/:id')
@Patch('/:id')
@Auth()
async updateSummary(
@Param() params: MongoIdDto,
@@ -60,13 +60,13 @@ export class AiController {
return this.service.updateSummaryInDb(params.id, body.summary)
}
@Delete('/summaries/:id')
@Delete('/:id')
@Auth()
async deleteSummary(@Param() params: MongoIdDto) {
return this.service.deleteSummaryInDb(params.id)
}
@Get('/summaries/article/:id')
@Get('/article/:id')
async getArticleSummary(
@Param() params: MongoIdDto,
@Query() query: GetSummaryQueryDto,

View File

@@ -0,0 +1,262 @@
import removeMdCodeblock from 'remove-md-codeblock'
import { Injectable, Logger } from '@nestjs/common'
import { OnEvent } from '@nestjs/event-emitter'
import { BizException } from '~/common/exceptions/biz.exception'
import { BusinessEvents } from '~/constants/business-event.constant'
import { CollectionRefTypes } from '~/constants/db.constant'
import { ErrorCodeEnum } from '~/constants/error-code.constant'
import { DatabaseService } from '~/processors/database/database.service'
import { CacheService } from '~/processors/redis/cache.service'
import { InjectModel } from '~/transformers/model.transformer'
import { transformDataToPaginate } from '~/transformers/paginate.transformer'
import { md5 } from '~/utils'
import { ConfigsService } from '../../configs/configs.service'
import { DEFAULT_SUMMARY_LANG, LANGUAGE_CODE_TO_NAME } from '../ai.constants'
import { AiService } from '../ai.service'
import { AISummaryModel } from './ai-summary.model'
import type { PagerDto } from '~/shared/dto/pager.dto'
@Injectable()
export class AiSummaryService {
private readonly logger: Logger
constructor(
@InjectModel(AISummaryModel)
private readonly aiSummaryModel: MongooseModel<AISummaryModel>,
private readonly databaseService: DatabaseService,
private readonly configService: ConfigsService,
private readonly cacheService: CacheService,
private readonly aiService: AiService,
) {
this.logger = new Logger(AiSummaryService.name)
}
private cachedTaskId2AiPromise = new Map<string, Promise<any>>()
private serializeText(text: string) {
return removeMdCodeblock(text)
}
async generateSummaryByOpenAI(
articleId: string,
lang = DEFAULT_SUMMARY_LANG,
) {
const {
ai: { enableSummary, openAiPreferredModel },
} = await this.configService.waitForConfigReady()
if (!enableSummary) {
throw new BizException(ErrorCodeEnum.AINotEnabled)
}
const openai = await this.aiService.getOpenAiClient()
const article = await this.databaseService.findGlobalById(articleId)
if (!article) {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
}
if (article.type === CollectionRefTypes.Recently) {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
}
const taskId = `ai:summary:${articleId}:${lang}`
try {
if (this.cachedTaskId2AiPromise.has(taskId)) {
return this.cachedTaskId2AiPromise.get(taskId)
}
const redis = this.cacheService.getClient()
const isProcessing = await redis.get(taskId)
if (isProcessing === 'processing') {
throw new BizException(ErrorCodeEnum.AIProcessing)
}
const taskPromise = handle.bind(this)(
articleId,
this.serializeText(article.document.text),
article.document.title,
) as Promise<any>
this.cachedTaskId2AiPromise.set(taskId, taskPromise)
return await taskPromise
async function handle(
this: AiSummaryService,
id: string,
text: string,
title: string,
) {
// 等待 30s
await redis.set(taskId, 'processing', 'EX', 30)
const completion = await openai.chat.completions.create({
messages: [
{
role: 'user',
content: `Summarize this article in ${LANGUAGE_CODE_TO_NAME[lang] || 'Chinese'} to 150 words:
"${text}"
CONCISE SUMMARY:`,
},
],
model: openAiPreferredModel,
})
await redis.del(taskId)
const summary = completion.choices[0].message.content
this.logger.log(
`OpenAI 生成文章 ${id}${title}」的摘要花费了 ${completion.usage?.total_tokens}token`,
)
const contentMd5 = md5(text)
const doc = await this.aiSummaryModel.create({
hash: contentMd5,
lang,
refId: id,
summary,
})
return doc
}
} catch (error) {
this.logger.error(
`OpenAI 在处理文章 ${articleId} 时出错:${error.message}`,
)
throw new BizException(ErrorCodeEnum.AIException, error.message)
} finally {
this.cachedTaskId2AiPromise.delete(taskId)
}
}
async getSummariesByRefId(refId: string) {
const article = await this.databaseService.findGlobalById(refId)
if (!article) {
throw new BizException(ErrorCodeEnum.ContentNotFound)
}
const summaries = await this.aiSummaryModel.find({
refId,
})
return {
summaries,
article,
}
}
async getAllSummaries(pager: PagerDto) {
const { page, size } = pager
const summaries = await this.aiSummaryModel.paginate(
{},
{
page,
limit: size,
sort: {
created: -1,
},
lean: true,
leanWithId: true,
},
)
const data = transformDataToPaginate(summaries)
return {
...data,
articles: await this.getRefArticles(summaries.docs),
}
}
private getRefArticles(docs: AISummaryModel[]) {
return this.databaseService
.findGlobalByIds(docs.map((d) => d.refId))
.then((articles) => {
const articleMap = {} as Record<
string,
{ title: string; id: string; type: CollectionRefTypes }
>
for (const a of articles.notes) {
articleMap[a.id] = {
title: a.title,
id: a.id,
type: CollectionRefTypes.Note,
}
}
for (const a of articles.posts) {
articleMap[a.id] = {
title: a.title,
id: a.id,
type: CollectionRefTypes.Post,
}
}
return articleMap
})
}
async updateSummaryInDb(id: string, summary: string) {
const doc = await this.aiSummaryModel.findById(id)
if (!doc) {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
}
doc.summary = summary
await doc.save()
return doc
}
async getSummaryByArticleId(articleId: string, lang = DEFAULT_SUMMARY_LANG) {
const article = await this.databaseService.findGlobalById(articleId)
if (!article) {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
}
if (article.type === CollectionRefTypes.Recently) {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
}
const contentMd5 = md5(this.serializeText(article.document.text))
const doc = await this.aiSummaryModel.findOne({
hash: contentMd5,
lang,
})
return doc
}
async deleteSummaryByArticleId(articleId: string) {
await this.aiSummaryModel.deleteMany({
refId: articleId,
})
}
async deleteSummaryInDb(id: string) {
await this.aiSummaryModel.deleteOne({
_id: id,
})
}
@OnEvent(BusinessEvents.POST_DELETE)
@OnEvent(BusinessEvents.NOTE_DELETE)
async handleDeleteArticle(event: { id: string }) {
await this.deleteSummaryByArticleId(event.id)
}
@OnEvent(BusinessEvents.POST_CREATE)
@OnEvent(BusinessEvents.NOTE_CREATE)
async handleCreateArticle(event: { id: string }) {
const enableAutoGenerate = await this.configService
.get('ai')
.then((c) => c.enableAutoGenerateSummary && c.enableSummary)
if (!enableAutoGenerate) {
return
}
await this.generateSummaryByOpenAI(event.id)
}
}

View File

@@ -0,0 +1,19 @@
import { Body, Post } from '@nestjs/common'
import { ApiController } from '~/common/decorators/api-controller.decorator'
import { AiQueryType, GenerateAiDto } from './ai-writer.dto'
import { AiWriterService } from './ai-writer.service'
@ApiController('ai/writer')
export class AiWriterController {
constructor(private readonly aiWriterService: AiWriterService) {}
@Post('generate')
async generate(@Body() body: GenerateAiDto) {
switch (body.type) {
case AiQueryType.TitleSlug:
return this.aiWriterService.generateTitleAndSlugByOpenAI(body.text)
case AiQueryType.Title:
return this.aiWriterService.generateSlugByTitleViaOpenAI(body.title)
}
}
}

View File

@@ -0,0 +1,18 @@
import { IsEnum, IsString, ValidateIf } from 'class-validator'
export enum AiQueryType {
TitleSlug = 'title-slug',
Title = 'title',
}
export class GenerateAiDto {
@IsEnum(AiQueryType)
type: AiQueryType
@ValidateIf((o: GenerateAiDto) => o.type === AiQueryType.TitleSlug)
@IsString()
text: string
@ValidateIf((o: GenerateAiDto) => o.type === AiQueryType.Title)
@IsString()
title: string
}

View File

@@ -0,0 +1,55 @@
import { Injectable, Logger } from '@nestjs/common'
import { ConfigsService } from '~/modules/configs/configs.service'
import { safeJSONParse } from '~/utils'
import { BizException } from '~/common/exceptions/biz.exception'
import { ErrorCodeEnum } from '~/constants/error-code.constant'
import { AiService } from '../ai.service'
@Injectable()
export class AiWriterService {
private readonly logger: Logger
constructor(
private readonly configService: ConfigsService,
private readonly aiService: AiService,
) {
this.logger = new Logger(AiWriterService.name)
}
async queryOpenAI(prompt: string): Promise<any> {
const openai = await this.aiService.getOpenAiClient()
const { openAiPreferredModel } = await this.configService.get('ai')
const result = await openai.chat.completions.create({
model: openAiPreferredModel,
messages: [{ role: 'user', content: prompt }],
})
const content = result.choices[0].message.content || ''
this.logger.log(
`查询 OpenAI 返回结果:${content} 花费了 ${result.usage?.total_tokens} 个 token`,
)
const json = safeJSONParse(content)
if (!json) {
throw new BizException(ErrorCodeEnum.AIResultParsingError)
}
return json
}
async generateTitleAndSlugByOpenAI(text: string) {
return this
.queryOpenAI(`Please give the following text a title in the same language as the text and a slug in English, the output format is JSON. {title:string, slug:string}:
"${text}"
RESULT:`)
}
async generateSlugByTitleViaOpenAI(title: string) {
return this
.queryOpenAI(`Please give the following title a slug in English, the output format is JSON. {slug:string}:
"${title}"
RESULT:`)
}
}

View File

@@ -44,3 +44,25 @@ export const LANGUAGE_CODE_TO_NAME = {
vi: 'Vietnamese',
zh: 'Chinese',
}
export const OpenAiSupportedModels = [
{ label: 'gpt-4-turbo', value: 'gpt-4-turbo' },
{ label: 'gpt-4-turbo-2024-04-09', value: 'gpt-4-turbo-2024-04-09' },
{ label: 'gpt-4-0125-preview', value: 'gpt-4-0125-preview' },
{ label: 'gpt-4-turbo-preview', value: 'gpt-4-turbo-preview' },
{ label: 'gpt-4-1106-preview', value: 'gpt-4-1106-preview' },
{ label: 'gpt-4-vision-preview', value: 'gpt-4-vision-preview' },
{ label: 'gpt-4', value: 'gpt-4' },
{ label: 'gpt-4-0314', value: 'gpt-4-0314' },
{ label: 'gpt-4-0613', value: 'gpt-4-0613' },
{ label: 'gpt-4-32k', value: 'gpt-4-32k' },
{ label: 'gpt-4-32k-0314', value: 'gpt-4-32k-0314' },
{ label: 'gpt-4-32k-0613', value: 'gpt-4-32k-0613' },
{ label: 'gpt-3.5-turbo', value: 'gpt-3.5-turbo' },
{ label: 'gpt-3.5-turbo-16k', value: 'gpt-3.5-turbo-16k' },
{ label: 'gpt-3.5-turbo-0301', value: 'gpt-3.5-turbo-0301' },
{ label: 'gpt-3.5-turbo-0613', value: 'gpt-3.5-turbo-0613' },
{ label: 'gpt-3.5-turbo-1106', value: 'gpt-3.5-turbo-1106' },
{ label: 'gpt-3.5-turbo-0125', value: 'gpt-3.5-turbo-0125' },
{ label: 'gpt-3.5-turbo-16k-0613', value: 'gpt-3.5-turbo-16k-0613' },
]

View File

@@ -1,10 +1,14 @@
import { Module } from '@nestjs/common'
import { AiController } from './ai.controller'
import { AiSummaryController } from './ai-summary/ai-summary.controller'
import { AiSummaryService } from './ai-summary/ai-summary.service'
import { AiService } from './ai.service'
import { AiWriterService } from './ai-writer/ai-writer.service'
import { AiWriterController } from './ai-writer/ai-writer.controller'
@Module({
providers: [AiService],
controllers: [AiController],
providers: [AiSummaryService, AiService, AiWriterService],
controllers: [AiSummaryController, AiWriterController],
exports: [AiService],
})
export class AiModule {}

View File

@@ -1,267 +1,23 @@
import { Injectable } from '@nestjs/common'
import OpenAI from 'openai'
import removeMdCodeblock from 'remove-md-codeblock'
import { Injectable, Logger } from '@nestjs/common'
import { OnEvent } from '@nestjs/event-emitter'
import { BizException } from '~/common/exceptions/biz.exception'
import { BusinessEvents } from '~/constants/business-event.constant'
import { CollectionRefTypes } from '~/constants/db.constant'
import { ErrorCodeEnum } from '~/constants/error-code.constant'
import { DatabaseService } from '~/processors/database/database.service'
import { CacheService } from '~/processors/redis/cache.service'
import { InjectModel } from '~/transformers/model.transformer'
import { transformDataToPaginate } from '~/transformers/paginate.transformer'
import { md5 } from '~/utils'
import { ConfigsService } from '../configs/configs.service'
import { AISummaryModel } from './ai-summary.model'
import { DEFAULT_SUMMARY_LANG, LANGUAGE_CODE_TO_NAME } from './ai.constants'
import type { PagerDto } from '~/shared/dto/pager.dto'
@Injectable()
export class AiService {
private readonly logger: Logger
constructor(
@InjectModel(AISummaryModel)
private readonly aiSummaryModel: MongooseModel<AISummaryModel>,
private readonly databaseService: DatabaseService,
private readonly configService: ConfigsService,
private readonly cacheService: CacheService,
) {
this.logger = new Logger(AiService.name)
}
private cachedTaskId2AiPromise = new Map<string, Promise<any>>()
private serializeText(text: string) {
return removeMdCodeblock(text)
}
async generateSummaryByOpenAI(
articleId: string,
lang = DEFAULT_SUMMARY_LANG,
) {
constructor(private readonly configService: ConfigsService) {}
public async getOpenAiClient() {
const {
ai: { enableSummary, openAiEndpoint, openAiKey },
ai: { openAiEndpoint, openAiKey },
} = await this.configService.waitForConfigReady()
if (!enableSummary) {
throw new BizException(ErrorCodeEnum.AINotEnabled)
}
if (!openAiKey) {
throw new BizException(ErrorCodeEnum.AIKeyExpired)
throw new BizException(ErrorCodeEnum.AINotEnabled, 'Key not found')
}
const openai = new OpenAI({
return new OpenAI({
apiKey: openAiKey,
baseURL: openAiEndpoint || void 0,
fetch: isDev ? fetch : void 0,
})
const article = await this.databaseService.findGlobalById(articleId)
if (!article) {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
}
if (article.type === CollectionRefTypes.Recently) {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
}
const taskId = `ai:summary:${articleId}:${lang}`
try {
if (this.cachedTaskId2AiPromise.has(taskId)) {
return this.cachedTaskId2AiPromise.get(taskId)
}
const redis = this.cacheService.getClient()
const isProcessing = await redis.get(taskId)
if (isProcessing === 'processing') {
throw new BizException(ErrorCodeEnum.AIProcessing)
}
const taskPromise = handle.bind(this)(
articleId,
this.serializeText(article.document.text),
article.document.title,
) as Promise<any>
this.cachedTaskId2AiPromise.set(taskId, taskPromise)
return await taskPromise
async function handle(
this: AiService,
id: string,
text: string,
title: string,
) {
// 等待 30s
await redis.set(taskId, 'processing', 'EX', 30)
const completion = await openai.chat.completions.create({
messages: [
{
role: 'user',
content: `Summarize this article in ${LANGUAGE_CODE_TO_NAME[lang] || 'Chinese'} to 150 words:
"${text}"
CONCISE SUMMARY:`,
},
],
model: 'gpt-3.5-turbo',
})
await redis.del(taskId)
const summary = completion.choices[0].message.content
this.logger.log(
`OpenAI 生成文章 ${id}${title}」的摘要花费了 ${completion.usage?.total_tokens}token`,
)
const contentMd5 = md5(text)
const doc = await this.aiSummaryModel.create({
hash: contentMd5,
lang,
refId: id,
summary,
})
return doc
}
} catch (error) {
this.logger.error(
`OpenAI 在处理文章 ${articleId} 时出错:${error.message}`,
)
throw new BizException(ErrorCodeEnum.AIException, error.message)
} finally {
this.cachedTaskId2AiPromise.delete(taskId)
}
}
async getSummariesByRefId(refId: string) {
const article = await this.databaseService.findGlobalById(refId)
if (!article) {
throw new BizException(ErrorCodeEnum.ContentNotFound)
}
const summaries = await this.aiSummaryModel.find({
refId,
})
return {
summaries,
article,
}
}
async getAllSummaries(pager: PagerDto) {
const { page, size } = pager
const summaries = await this.aiSummaryModel.paginate(
{},
{
page,
limit: size,
sort: {
created: -1,
},
lean: true,
leanWithId: true,
},
)
const data = transformDataToPaginate(summaries)
return {
...data,
articles: await this.getRefArticles(summaries.docs),
}
}
private getRefArticles(docs: AISummaryModel[]) {
return this.databaseService
.findGlobalByIds(docs.map((d) => d.refId))
.then((articles) => {
const articleMap = {} as Record<
string,
{ title: string; id: string; type: CollectionRefTypes }
>
for (const a of articles.notes) {
articleMap[a.id] = {
title: a.title,
id: a.id,
type: CollectionRefTypes.Note,
}
}
for (const a of articles.posts) {
articleMap[a.id] = {
title: a.title,
id: a.id,
type: CollectionRefTypes.Post,
}
}
return articleMap
})
}
async updateSummaryInDb(id: string, summary: string) {
const doc = await this.aiSummaryModel.findById(id)
if (!doc) {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
}
doc.summary = summary
await doc.save()
return doc
}
async getSummaryByArticleId(articleId: string, lang = DEFAULT_SUMMARY_LANG) {
const article = await this.databaseService.findGlobalById(articleId)
if (!article) {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
}
if (article.type === CollectionRefTypes.Recently) {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
}
const contentMd5 = md5(this.serializeText(article.document.text))
const doc = await this.aiSummaryModel.findOne({
hash: contentMd5,
lang,
})
return doc
}
async deleteSummaryByArticleId(articleId: string) {
await this.aiSummaryModel.deleteMany({
refId: articleId,
})
}
async deleteSummaryInDb(id: string) {
await this.aiSummaryModel.deleteOne({
_id: id,
})
}
@OnEvent(BusinessEvents.POST_DELETE)
@OnEvent(BusinessEvents.NOTE_DELETE)
async handleDeleteArticle(event: { id: string }) {
await this.deleteSummaryByArticleId(event.id)
}
@OnEvent(BusinessEvents.POST_CREATE)
@OnEvent(BusinessEvents.NOTE_CREATE)
async handleCreateArticle(event: { id: string }) {
const enableAutoGenerate = await this.configService
.get('ai')
.then((c) => c.enableAutoGenerateSummary && c.enableSummary)
if (!enableAutoGenerate) {
return
}
await this.generateSummaryByOpenAI(event.id)
}
}

View File

@@ -52,7 +52,7 @@ export const generateDefaultConfig: () => IConfig = () => ({
algoliaSearchOptions: { enable: false, apiKey: '', appId: '', indexName: '' },
adminExtra: {
enableAdminProxy: true,
title: 'おかえり~',
background: '',
gaodemapKey: null!,
},
@@ -78,6 +78,7 @@ export const generateDefaultConfig: () => IConfig = () => ({
enableAutoGenerateSummary: false,
enableSummary: false,
openAiEndpoint: '',
openAiPreferredModel: 'gpt-3.5-turbo',
openAiKey: '',
},
})

View File

@@ -13,8 +13,10 @@ import {
} from 'class-validator'
import { JSONSchema } from 'class-validator-jsonschema'
import { ChatModel } from 'openai/resources'
import { IsAllowedUrl } from '~/decorators/dto/isAllowedUrl'
import { OpenAiSupportedModels } from '../ai/ai.constants'
import { Encrypt } from './configs.encrypt.util'
import {
JSONSchemaArrayField,
@@ -246,11 +248,6 @@ export class AdminExtraDto {
@JSONSchemaPlainField('登录页面背景')
background?: string
@IsString()
@IsOptional()
@JSONSchemaPlainField('中后台标题')
title?: string
@IsString()
@IsOptional()
@SecretField
@@ -368,6 +365,27 @@ export class AuthSecurityDto {
}
@JSONSchema({ title: 'AI 设定' })
export class AIDto {
@IsOptional()
@JSONSchemaPasswordField('OpenAI Key')
@IsString()
@SecretField
openAiKey: string
@IsOptional()
@IsString()
@JSONSchemaPlainField('OpenAI Endpoint')
openAiEndpoint: string
@IsOptional()
@IsString()
@JSONSchemaPlainField('OpenAI 默认模型', {
'ui:options': {
type: 'select',
values: OpenAiSupportedModels,
},
})
openAiPreferredModel: ChatModel
@IsBoolean()
@IsOptional()
@JSONSchemaToggleField('可调用 AI 摘要', {
@@ -382,15 +400,4 @@ export class AIDto {
'此选项开启后,将会在文章发布后自动生成摘要,需要开启上面的选项,否则无效',
})
enableAutoGenerateSummary: boolean
@IsOptional()
@JSONSchemaPasswordField('OpenAI Key')
@IsString()
@SecretField
openAiKey: string
@IsOptional()
@IsString()
@JSONSchemaPlainField('OpenAI Endpoint')
openAiEndpoint: string
}

View File

@@ -56,7 +56,6 @@ export class PageProxyService {
// Define the base injectData object
const injectData: any = {
LOGIN_BG: adminExtra.background,
TITLE: adminExtra.title,
WEB_URL: webUrl,
}

View File

@@ -1,5 +1,5 @@
import { ActivityModel } from '~/modules/activity/activity.model'
import { AISummaryModel } from '~/modules/ai/ai-summary.model'
import { AISummaryModel } from '~/modules/ai/ai-summary/ai-summary.model'
import { AnalyzeModel } from '~/modules/analyze/analyze.model'
import { AuthnModel } from '~/modules/authn/authn.model'
import { CategoryModel } from '~/modules/category/category.model'

View File

@@ -37,6 +37,8 @@ export default sxzz(
'unicorn/filename-case': 0,
'unicorn/prefer-math-trunc': 0,
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'error',
{

View File

@@ -6,7 +6,7 @@ describe('test ai client', () => {
const client = mockRequestInstance(AIController)
test('POST /generate-summary', async () => {
mockResponse('/ai/generate-summary', {}, 'post', {
mockResponse('/ai/summaries/generate', {}, 'post', {
lang: 'zh-CN',
refId: '11',
})

View File

@@ -47,7 +47,7 @@ export class AIController<ResponseWrapper> implements IController {
}
async generateSummary(articleId: string, lang = 'zh-CN', token = '') {
return this.proxy('generate-summary').post<AISummaryModel>({
return this.proxy.summaries.generate.post<AISummaryModel>({
params: {
token,
},