feat: ai module (#1649)

* fix: pass `truncate`

Signed-off-by: Innei <i@innei.in>

* feat: add openai summary

Signed-off-by: Innei <i@innei.in>

* feat: ai list api

Signed-off-by: Innei <i@innei.in>

---------

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei
2024-04-26 19:52:11 +08:00
committed by GitHub
parent d78a359a7d
commit c989a2a7b0
30 changed files with 7989 additions and 5764 deletions

View File

@@ -0,0 +1,34 @@
import { mockRequestInstance } from '~/__tests__/helpers/instance'
import { mockResponse } from '~/__tests__/helpers/response'
import { AIController } from '~/controllers'
describe('test ai client', () => {
const client = mockRequestInstance(AIController)
test('POST /generate-summary', async () => {
mockResponse('/ai/generate-summary', {}, 'post', {
lang: 'zh-CN',
refId: '11',
})
await expect(
client.ai.generateSummary('11', 'zh-CN'),
).resolves.not.toThrowError()
})
test('GET /summary/:id', async () => {
mockResponse(
'/ai/summaries/article/11?articleId=11&lang=zh-CN&onlyDb=true',
{},
'get',
)
await expect(
client.ai.getSummary({
articleId: '11',
lang: 'zh-CN',
onlyDb: true,
}),
).resolves.not.toThrowError()
})
})

View File

@@ -1,6 +1,6 @@
import camelcaseKeysLib from 'camelcase-keys'
import { camelcaseKeys } from '~/utils/camelcase-keys'
import { camelcase, camelcaseKeys } from '~/utils/camelcase-keys'
describe('test camelcase keys', () => {
it('case 1 normal', () => {
@@ -80,7 +80,33 @@ describe('test camelcase keys', () => {
]
expect(camelcaseKeys(arr)).toStrictEqual(
camelcaseKeysLib(arr, { deep: true }),
camelcaseKeysLib(arr as any, { deep: true }),
)
})
it('case 6: filter out mongo id', () => {
const obj = {
_id: '123',
a_b: 1,
collections: {
posts: {
'661bb93307d35005ba96731b': {},
},
},
}
expect(camelcaseKeys(obj)).toStrictEqual({
id: '123',
aB: 1,
collections: {
posts: {
'661bb93307d35005ba96731b': {},
},
},
})
})
it('case 7: start with underscore should not camelcase', () => {
expect(camelcase('_id')).toBe('id')
})
})

View File

@@ -0,0 +1,61 @@
import type { IRequestAdapter } from '~/interfaces/adapter'
import type { IController } from '~/interfaces/controller'
import type { IRequestHandler } from '~/interfaces/request'
import type { HTTPClient } from '../core'
import type { AISummaryModel } from '../models/ai'
import { autoBind } from '~/utils/auto-bind'
declare module '../core/client' {
interface HTTPClient<
T extends IRequestAdapter = IRequestAdapter,
ResponseWrapper = unknown,
> {
ai: AIController<ResponseWrapper>
}
}
/**
* @support core >= 5.6.0
*/
export class AIController<ResponseWrapper> implements IController {
base = 'ai'
name = 'ai'
constructor(private client: HTTPClient) {
autoBind(this)
}
public get proxy(): IRequestHandler<ResponseWrapper> {
return this.client.proxy(this.base)
}
async getSummary({
articleId,
lang = 'zh-CN',
onlyDb,
}: {
articleId: string
lang?: string
onlyDb?: boolean
}) {
return this.proxy.summaries.article(articleId).get<AISummaryModel>({
params: {
lang,
onlyDb,
},
})
}
async generateSummary(articleId: string, lang = 'zh-CN', token = '') {
return this.proxy('generate-summary').post<AISummaryModel>({
params: {
token,
},
data: {
lang,
refId: articleId,
},
})
}
}

View File

@@ -1,6 +1,7 @@
import { AckController } from './ack'
import { ActivityController } from './activity'
import { AggregateController } from './aggregate'
import { AIController } from './ai'
import { CategoryController } from './category'
import { CommentController } from './comment'
import { LinkController } from './link'
@@ -22,6 +23,7 @@ import { TopicController } from './topic'
import { UserController } from './user'
export const allControllers = [
AIController,
AckController,
ActivityController,
AggregateController,
@@ -43,6 +45,7 @@ export const allControllers = [
]
export const allControllerNames = [
'ai',
'ack',
'activity',
'aggregate',
@@ -69,6 +72,7 @@ export const allControllerNames = [
] as const
export {
AIController,
AckController,
ActivityController,
AggregateController,

View File

@@ -0,0 +1,8 @@
export interface AISummaryModel {
id: string
created: string
summary: string
hash: string
refId: string
lang: string
}

View File

@@ -1,5 +1,6 @@
export * from './activity'
export * from './aggregate'
export * from './ai'
export * from './base'
export * from './category'
export * from './comment'

View File

@@ -11,7 +11,8 @@ export const camelcaseKeys = <T = any>(obj: any): T => {
if (isPlainObject(obj)) {
return Object.keys(obj).reduce((result: any, key) => {
result[camelcase(key)] = camelcaseKeys(obj[key])
const nextKey = isMongoId(key) ? key : camelcase(key)
result[nextKey] = camelcaseKeys(obj[key])
return result
}, {}) as any
}
@@ -20,7 +21,9 @@ export const camelcaseKeys = <T = any>(obj: any): T => {
}
export function camelcase(str: string) {
return str.replace(/([-_][a-z])/gi, ($1) => {
return str.replace(/^_+/, '').replace(/([-_][a-z])/gi, ($1) => {
return $1.toUpperCase().replace('-', '').replace('_', '')
})
}
const isMongoId = (id: string) =>
id.length === 24 && /^[0-9a-fA-F]{24}$/.test(id)