feat: update ai integration (#2422)

* init

Signed-off-by: Innei <tukon479@gmail.com>

* update

Signed-off-by: Innei <tukon479@gmail.com>

* chore: update package dependencies and remove unused code

- Updated package manager version in package.json to pnpm@10.10.0.
- Removed references to `@modelcontextprotocol/sdk` from core application files.
- Deleted unused `mcp.controller.ts` and cleaned up related imports in `mcp.module.ts`.
- Adjusted optional dependencies in pnpm-lock.yaml.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: update AI summary and writer services to use new tools parser

- Replaced `JsonOutputFunctionsParser` with `JsonOutputToolsParser` in both `ai-summary.service.ts` and `ai-writer.service.ts`.
- Updated function definitions to align with the new tools structure, including changes to how functions are defined and invoked.
- Enhanced error handling to return empty objects when no results are found.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-05-05 20:50:52 +08:00
committed by GitHub
parent bb7e9a38b1
commit 46704d2498
19 changed files with 1881 additions and 1108 deletions

View File

@@ -52,121 +52,121 @@
"redis-memory-server": "^0.12.1" "redis-memory-server": "^0.12.1"
}, },
"dependencies": { "dependencies": {
"@algolia/client-search": "^4.22.1", "@algolia/client-search": "catalog:",
"@antfu/install-pkg": "1.0.0", "@antfu/install-pkg": "catalog:",
"@aws-sdk/client-s3": "3.802.0", "@aws-sdk/client-s3": "catalog:",
"@babel/core": "7.27.1", "@babel/core": "catalog:",
"@babel/plugin-transform-modules-commonjs": "7.27.1", "@babel/plugin-transform-modules-commonjs": "catalog:",
"@babel/plugin-transform-typescript": "7.27.1", "@babel/plugin-transform-typescript": "catalog:",
"@babel/types": "^7.27.1", "@babel/types": "catalog:",
"@fastify/cookie": "11.0.2", "@fastify/cookie": "catalog:",
"@fastify/multipart": "9.0.3", "@fastify/multipart": "catalog:",
"@fastify/static": "8.1.1", "@fastify/static": "catalog:",
"@innei/next-async": "0.3.0", "@innei/next-async": "catalog:",
"@innei/pretty-logger-nestjs": "0.3.3", "@innei/pretty-logger-nestjs": "catalog:",
"@keyv/redis": "4.4.0", "@keyv/redis": "catalog:",
"@langchain/openai": "0.5.10", "@langchain/openai": "catalog:",
"@mx-space/compiled": "workspace:*", "@mx-space/compiled": "workspace:*",
"@nestjs/cache-manager": "3.0.1", "@nestjs/cache-manager": "catalog:",
"@nestjs/common": "11.1.0", "@nestjs/common": "catalog:",
"@nestjs/core": "11.1.0", "@nestjs/core": "catalog:",
"@nestjs/event-emitter": "3.0.1", "@nestjs/event-emitter": "catalog:",
"@nestjs/mapped-types": "^2.1.0", "@nestjs/mapped-types": "catalog:",
"@nestjs/platform-fastify": "11.1.0", "@nestjs/platform-fastify": "catalog:",
"@nestjs/platform-socket.io": "11.1.0", "@nestjs/platform-socket.io": "catalog:",
"@nestjs/schedule": "6.0.0", "@nestjs/schedule": "catalog:",
"@nestjs/throttler": "6.4.0", "@nestjs/throttler": "catalog:",
"@nestjs/websockets": "11.1.0", "@nestjs/websockets": "catalog:",
"@simplewebauthn/server": "10.0.1", "@simplewebauthn/server": "catalog:",
"@socket.io/redis-adapter": "8.3.0", "@socket.io/redis-adapter": "catalog:",
"@socket.io/redis-emitter": "5.1.0", "@socket.io/redis-emitter": "catalog:",
"@typegoose/auto-increment": "4.13.0", "@typegoose/auto-increment": "catalog:",
"@typegoose/typegoose": "12.15.0", "@typegoose/typegoose": "catalog:",
"@types/jsonwebtoken": "9.0.9", "@types/jsonwebtoken": "catalog:",
"algoliasearch": "4.24.0", "algoliasearch": "catalog:",
"axios": "^1.9.0", "axios": "catalog:",
"axios-retry": "4.5.0", "axios-retry": "catalog:",
"bcryptjs": "^3.0.2", "bcryptjs": "catalog:",
"blurhash": "2.0.5", "blurhash": "catalog:",
"cache-manager": "6.4.2", "cache-manager": "catalog:",
"class-transformer": "0.5.1", "class-transformer": "catalog:",
"class-validator": "0.13.2", "class-validator": "catalog:",
"class-validator-jsonschema": "npm:@innei/class-validator-jsonschema@3.1.2", "class-validator-jsonschema": "catalog:",
"cls-hooked": "^4.2.2", "cls-hooked": "catalog:",
"commander": "13.1.0", "commander": "catalog:",
"dayjs": "1.11.13", "dayjs": "catalog:",
"ejs": "3.1.10", "ejs": "catalog:",
"form-data": "4.0.2", "form-data": "catalog:",
"inquirer": "^10.2.2", "inquirer": "catalog:",
"isbot": "5.1.27", "isbot": "catalog:",
"js-yaml": "^4.1.0", "js-yaml": "catalog:",
"json5": "2.2.3", "json5": "catalog:",
"jsonwebtoken": "9.0.2", "jsonwebtoken": "catalog:",
"jszip": "3.10.1", "jszip": "catalog:",
"keyv": "5.3.3", "keyv": "catalog:",
"langchain": "0.3.24", "langchain": "catalog:",
"linkedom": "0.18.10", "linkedom": "catalog:",
"lodash": "^4.17.21", "lodash": "catalog:",
"lru-cache": "11.1.0", "lru-cache": "catalog:",
"marked": "15.0.11", "marked": "catalog:",
"mime-types": "^3.0.1", "mime-types": "catalog:",
"mkdirp": "^3.0.1", "mkdirp": "catalog:",
"mongoose": "8.14.1", "mongoose": "catalog:",
"mongoose-aggregate-paginate-v2": "1.1.4", "mongoose-aggregate-paginate-v2": "catalog:",
"mongoose-autopopulate": "1.1.0", "mongoose-autopopulate": "catalog:",
"mongoose-lean-getters": "2.2.1", "mongoose-lean-getters": "catalog:",
"mongoose-lean-virtuals": "1.1.0", "mongoose-lean-virtuals": "catalog:",
"mongoose-paginate-v2": "1.9.0", "mongoose-paginate-v2": "catalog:",
"node-machine-id": "1.1.12", "node-machine-id": "catalog:",
"nodemailer": "7.0.0", "nodemailer": "catalog:",
"openai": "4.97.0", "openai": "catalog:",
"pluralize": "^8.0.0", "pluralize": "catalog:",
"qs": "6.14.0", "qs": "catalog:",
"reflect-metadata": "0.2.2", "reflect-metadata": "catalog:",
"remove-markdown": "0.6.2", "remove-markdown": "catalog:",
"remove-md-codeblock": "0.0.4", "remove-md-codeblock": "catalog:",
"rxjs": "7.8.2", "rxjs": "catalog:",
"semver": "7.7.1", "semver": "catalog:",
"slugify": "1.6.6", "slugify": "catalog:",
"snakecase-keys": "6.0.0", "snakecase-keys": "catalog:",
"source-map-support": "^0.5.21", "source-map-support": "catalog:",
"ua-parser-js": "2.0.3", "ua-parser-js": "catalog:",
"vm2": "3.9.19", "vm2": "catalog:",
"wildcard-match": "5.1.4", "wildcard-match": "catalog:",
"xss": "1.0.15", "xss": "catalog:",
"zx-cjs": "7.0.7-0" "zx-cjs": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@langchain/core": "0.3.51", "@langchain/core": "catalog:",
"@nestjs/cli": "11.0.7", "@nestjs/cli": "catalog:",
"@nestjs/schematics": "11.0.5", "@nestjs/schematics": "catalog:",
"@nestjs/testing": "11.1.0", "@nestjs/testing": "catalog:",
"@swc/core": "1.11.24", "@swc/core": "catalog:",
"@types/babel__core": "7.20.5", "@types/babel__core": "catalog:",
"@types/bcryptjs": "^3.0.0", "@types/bcryptjs": "catalog:",
"@types/cls-hooked": "^4.3.9", "@types/cls-hooked": "catalog:",
"@types/ejs": "3.1.5", "@types/ejs": "catalog:",
"@types/get-image-colors": "4.0.5", "@types/get-image-colors": "catalog:",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "catalog:",
"@types/lodash": "4.17.16", "@types/lodash": "catalog:",
"@types/mime-types": "2.1.4", "@types/mime-types": "catalog:",
"@types/mongoose-aggregate-paginate-v2": "1.0.12", "@types/mongoose-aggregate-paginate-v2": "catalog:",
"@types/node": "22.15.3", "@types/node": "catalog:",
"@types/nodemailer": "6.4.17", "@types/nodemailer": "catalog:",
"@types/qs": "6.9.18", "@types/qs": "catalog:",
"@types/remove-markdown": "0.3.4", "@types/remove-markdown": "catalog:",
"@types/semver": "7.7.0", "@types/semver": "catalog:",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "catalog:",
"@types/validator": "13.15.0", "@types/validator": "catalog:",
"@vercel/ncc": "0.38.3", "@vercel/ncc": "catalog:",
"cron": "^3.5.0", "cron": "catalog:",
"ioredis": "5.6.1", "ioredis": "catalog:",
"sharp": "0.34.1", "sharp": "catalog:",
"socket.io": "^4.8.1", "socket.io": "catalog:",
"unplugin-swc": "1.5.2", "unplugin-swc": "catalog:",
"vite": "5.4.10", "vite": "catalog:",
"vite-tsconfig-paths": "5.1.4", "vite-tsconfig-paths": "catalog:",
"vitest": "1.5.2", "vitest": "catalog:",
"zx": "7.2.3" "zx": "catalog:"
} }
} }

View File

@@ -42,6 +42,7 @@ import { HelperModule as BizHelperModule } from './modules/helper/helper.module'
import { InitModule } from './modules/init/init.module' import { InitModule } from './modules/init/init.module'
import { LinkModule } from './modules/link/link.module' import { LinkModule } from './modules/link/link.module'
import { MarkdownModule } from './modules/markdown/markdown.module' import { MarkdownModule } from './modules/markdown/markdown.module'
import { McpModule } from './modules/mcp/mcp.module'
import { NoteModule } from './modules/note/note.module' import { NoteModule } from './modules/note/note.module'
import { OptionModule } from './modules/option/option.module' import { OptionModule } from './modules/option/option.module'
import { PageModule } from './modules/page/page.module' import { PageModule } from './modules/page/page.module'
@@ -94,6 +95,7 @@ import { RedisModule } from './processors/redis/redis.module'
HealthModule, HealthModule,
LinkModule, LinkModule,
MarkdownModule, MarkdownModule,
McpModule,
NoteModule, NoteModule,
OptionModule, OptionModule,
PageModule, PageModule,

View File

@@ -0,0 +1,13 @@
import { forwardRef, Module } from '@nestjs/common'
import { McpModule } from '../../mcp/mcp.module'
import { AiModule } from '../ai.module'
import { AIAgentService } from './ai-agent.service'
import { TestController } from './test.controller'
@Module({
imports: [McpModule, forwardRef(() => AiModule)],
providers: [AIAgentService],
controllers: isDev ? [TestController] : [],
})
export class AiAgentModule {}

View File

@@ -0,0 +1,490 @@
import { AgentExecutor, createOpenAIToolsAgent } from 'langchain/agents'
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from '@langchain/core/prompts'
import { DynamicStructuredTool, ToolInterface } from '@langchain/core/tools'
import { z } from '@mx-space/compiled/zod'
import { Injectable } from '@nestjs/common'
import { McpService } from '../../mcp/mcp.service'
import { AiService } from '../ai.service'
@Injectable()
export class AIAgentService {
constructor(
private readonly aiService: AiService,
private readonly mcpService: McpService,
) {}
// 创建获取帖子的工具
private createGetPostTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_post_by_id',
description: '根据ID获取博客文章',
schema: z.object({
id: z.string().describe('帖子的ID'),
}),
func: async ({ id }) => {
try {
const post = await this.mcpService.getPostById(id)
return JSON.stringify(post)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取帖子列表的工具
private createGetPostsTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_posts',
description: '获取博客文章列表',
schema: z.object({
page: z.number().optional().describe('页码默认为1'),
size: z.number().optional().describe('每页数量默认为10'),
}),
func: async ({ page = 1, size = 10 }) => {
try {
const posts = await this.mcpService.getPosts(page, size)
return JSON.stringify(posts)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取笔记的工具
private createGetNoteTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_note_by_id',
description: '根据ID获取笔记',
schema: z.object({
id: z.string().describe('笔记的ID或nid'),
}),
func: async ({ id }) => {
try {
const note = await this.mcpService.getNoteById(id)
return JSON.stringify(note)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取笔记列表的工具
private createGetNotesTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_notes',
description: '获取笔记列表',
schema: z.object({
page: z.number().optional().describe('页码默认为1'),
size: z.number().optional().describe('每页数量默认为10'),
}),
func: async ({ page = 1, size = 10 }) => {
try {
const notes = await this.mcpService.getNotes(page, size)
return JSON.stringify(notes)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
private createGetLatestPostTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_latest_post',
description: '获取最新的一篇博客文章',
func: async () => {
const post = await this.mcpService.getLatestPost()
return JSON.stringify(post)
},
schema: z.object({}),
})
}
private createGetLatestNotesTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_latest_notes',
description: '获取最新的一篇笔记',
func: async () => {
const notes = await this.mcpService.getLatestNotes()
return JSON.stringify(notes)
},
schema: z.object({}),
})
}
// 创建获取分类的工具
private createGetCategoryTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_category_by_id',
description: '根据ID获取分类',
schema: z.object({
id: z.string().describe('分类的ID'),
}),
func: async ({ id }) => {
try {
const category = await this.mcpService.getCategoryById(id)
return JSON.stringify(category)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取所有分类的工具
private createGetAllCategoriesTools(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_all_categories',
description: '获取所有分类及其文章数量',
schema: z.object({}),
func: async () => {
try {
const categories = await this.mcpService.getAllCategories()
return JSON.stringify(categories)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取分类下文章的工具
private createGetPostsByCategoryTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_posts_by_category',
description: '获取指定分类下的所有文章',
schema: z.object({
categoryId: z.string().describe('分类的ID'),
}),
func: async ({ categoryId }) => {
try {
const posts = await this.mcpService.getPostsByCategory(categoryId)
return JSON.stringify(posts)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取标签统计的工具
private createGetTagsSummaryTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_tags_summary',
description: '获取所有标签及其文章数量统计',
schema: z.object({}),
func: async () => {
try {
const tags = await this.mcpService.getTagsSummary()
return JSON.stringify(tags)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取标签下文章的工具
private createGetPostsByTagTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_posts_by_tag',
description: '获取指定标签下的所有文章',
schema: z.object({
tag: z.string().describe('标签名称'),
}),
func: async ({ tag }) => {
try {
const posts = await this.mcpService.getPostsByTag(tag)
return JSON.stringify(posts)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取页面的工具
private createGetPageTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_page_by_id',
description: '根据ID获取页面',
schema: z.object({
id: z.string().describe('页面的ID'),
}),
func: async ({ id }) => {
try {
const page = await this.mcpService.getPageById(id)
return JSON.stringify(page)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取所有页面的工具
private createGetAllPagesTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_all_pages',
description: '获取所有页面',
schema: z.object({}),
func: async () => {
try {
const pages = await this.mcpService.getAllPages()
return JSON.stringify(pages)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取所有说说的工具
private createGetAllSaysTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_all_says',
description: '获取所有说说/状态更新',
schema: z.object({}),
func: async () => {
try {
const says = await this.mcpService.getAllSays()
return JSON.stringify(says)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取随机说说的工具
private createGetRandomSayTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_random_say',
description: '获取随机一条说说/状态更新',
schema: z.object({}),
func: async () => {
try {
const say = await this.mcpService.getRandomSay()
return JSON.stringify(say)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取所有动态的工具
private createGetAllRecentlyTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_all_recently',
description: '获取所有动态/活动',
schema: z.object({}),
func: async () => {
try {
const recently = await this.mcpService.getAllRecently()
return JSON.stringify(recently)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取特定动态的工具
private createGetRecentlyByIdTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_recently_by_id',
description: '根据ID获取特定动态/活动',
schema: z.object({
id: z.string().describe('动态的ID'),
}),
func: async ({ id }) => {
try {
const recently = await this.mcpService.getRecentlyById(id)
return JSON.stringify(recently)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取最新动态的工具
private createGetLatestRecentlyTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_latest_recently',
description: '获取最新的一条动态/活动',
schema: z.object({}),
func: async () => {
try {
const recently = await this.mcpService.getLatestRecently()
return JSON.stringify(recently)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取分页动态的工具
private createGetRecentlyOffsetTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_recently_offset',
description: '获取指定范围的动态/活动',
schema: z.object({
size: z.number().optional().describe('数量默认为10'),
before: z.string().optional().describe('获取此ID之前的动态'),
after: z.string().optional().describe('获取此ID之后的动态'),
}),
func: async ({ size = 10, before, after }) => {
try {
const recently = await this.mcpService.getRecentlyOffset({
size,
before,
after,
})
return JSON.stringify(recently)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取评论列表的工具
private createGetCommentsTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_comments',
description: '获取所有评论,可按状态筛选',
schema: z.object({
page: z.number().optional().describe('页码默认为1'),
size: z.number().optional().describe('每页数量默认为10'),
state: z.number().optional().describe('评论状态筛选0表示所有'),
}),
func: async ({ page = 1, size = 10, state = 0 }) => {
try {
const comments = await this.mcpService.getComments({
page,
size,
state,
})
return JSON.stringify(comments)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 创建获取内容评论的工具
private createGetContentCommentsTool(): ToolInterface {
return new DynamicStructuredTool({
name: 'get_content_comments',
description: '获取特定内容的评论',
schema: z.object({
id: z.string().describe('内容的ID'),
type: z.string().optional().describe('内容类型如post, note, page等'),
}),
func: async ({ id, type }) => {
try {
const comments = await this.mcpService.getContentComments(id, type)
return JSON.stringify(comments)
} catch (error) {
return `Error: ${error.message}`
}
},
})
}
// 使用LangChain的Agent模式与MCP数据交互
async runWithTools(userPrompt: string) {
// 获取OpenAI模型
const model = await this.aiService.getOpenAiChain()
// 初始化工具列表
const tools = [
this.createGetPostTool(),
this.createGetPostsTool(),
this.createGetNoteTool(),
this.createGetNotesTool(),
this.createGetLatestPostTool(),
this.createGetLatestNotesTool(),
this.createGetCategoryTool(),
this.createGetAllCategoriesTools(),
this.createGetPostsByCategoryTool(),
this.createGetTagsSummaryTool(),
this.createGetPostsByTagTool(),
this.createGetPageTool(),
this.createGetAllPagesTool(),
this.createGetAllSaysTool(),
this.createGetRandomSayTool(),
this.createGetAllRecentlyTool(),
this.createGetRecentlyByIdTool(),
this.createGetLatestRecentlyTool(),
this.createGetRecentlyOffsetTool(),
this.createGetCommentsTool(),
this.createGetContentCommentsTool(),
]
// 创建Agent提示模板
const prompt = ChatPromptTemplate.fromMessages([
[
'system',
`你是一个可以访问博客数据库的智能助手。使用提供的工具来获取和分析数据。
当你需要回答用户问题时,请遵循以下步骤:
1. 分析用户的问题,确定需要获取什么数据
2. 使用合适的工具获取数据
3. 检查和分析获取的数据
4. 如需更多信息,继续使用工具获取
5. 根据所有收集到的数据提供完整回答
你可以查询的内容包括:
- 博客文章posts
- 笔记notes
- 分类categories
- 标签tags
- 自定义页面pages
- 说说/状态更新says
- 动态/活动recently
- 评论comments
不要编造信息,只使用通过工具获得的真实数据。`,
],
new MessagesPlaceholder('chat_history'),
['human', '{input}'],
new MessagesPlaceholder('agent_scratchpad'),
])
// 创建Agent
const agent = await createOpenAIToolsAgent({
llm: model,
tools,
prompt,
})
// 创建Agent执行器
const executor = new AgentExecutor({
agent,
tools,
maxIterations: 5,
verbose: isDev,
})
// 执行Agent
const result = await executor.invoke({
input: userPrompt,
chat_history: [],
})
return result.output
}
}

View File

@@ -0,0 +1,15 @@
import { Controller, Get } from '@nestjs/common'
import { AIAgentService } from './ai-agent.service'
@Controller('ai/test')
export class TestController {
constructor(private readonly aiAgentService: AIAgentService) {}
@Get('/')
async test() {
return this.aiAgentService.runWithTools(
'Posts Id: 65e99e28eb677674816310c7 主要了写了什么内容',
)
}
}

View File

@@ -2,6 +2,7 @@ import { JsonOutputFunctionsParser } from 'langchain/output_parsers'
import removeMdCodeblock from 'remove-md-codeblock' import removeMdCodeblock from 'remove-md-codeblock'
import type { PagerDto } from '~/shared/dto/pager.dto' import type { PagerDto } from '~/shared/dto/pager.dto'
import { JsonOutputToolsParser } from '@langchain/core/output_parsers/openai_tools'
import { Injectable, Logger } from '@nestjs/common' import { Injectable, Logger } from '@nestjs/common'
import { OnEvent } from '@nestjs/event-emitter' import { OnEvent } from '@nestjs/event-emitter'
@@ -57,33 +58,42 @@ export class AiSummaryService {
throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess) throw new BizException(ErrorCodeEnum.ContentNotFoundCantProcess)
} }
const parser = new JsonOutputFunctionsParser() const parser = new JsonOutputToolsParser()
const runnable = openai const runnable = openai
.bind({ .bind({
functions: [ tools: [
{ {
name: 'extractor', type: 'function',
parameters: { function: {
type: 'object', name: 'extractor',
properties: { description: `Extract the summary of the input text in the ${LANGUAGE_CODE_TO_NAME[lang] || LANGUAGE_CODE_TO_NAME[DEFAULT_SUMMARY_LANG]}, and the length of the summary is less than 150 words.`,
summary: { parameters: {
type: 'string', type: 'object',
description: `The summary of the input text in the ${LANGUAGE_CODE_TO_NAME[lang] || LANGUAGE_CODE_TO_NAME[DEFAULT_SUMMARY_LANG]}, and the length of the summary is less than 150 words.`, properties: {
summary: {
type: 'string',
description: `The summary of the input text in the ${LANGUAGE_CODE_TO_NAME[lang] || LANGUAGE_CODE_TO_NAME[DEFAULT_SUMMARY_LANG]}, and the length of the summary is less than 150 words.`,
},
}, },
required: ['summary'],
}, },
required: ['summary'],
}, },
}, },
], ],
function_call: { name: 'extractor' },
tool_choice: { type: 'function', function: { name: 'extractor' } },
}) })
.pipe(parser) .pipe(parser)
const result = await runnable.invoke([ const result = (await runnable.invoke([
this.serializeText(article.document.text), this.serializeText(article.document.text),
]) ])) as any[]
return (result as any).summary if (result.length === 0) {
return {}
}
return result[0]?.args?.summary
} }
async generateSummaryByOpenAI(articleId: string, lang: string) { async generateSummaryByOpenAI(articleId: string, lang: string) {
const { const {

View File

@@ -1,6 +1,9 @@
import { JsonOutputFunctionsParser } from 'langchain/output_parsers' import type {
import type { FunctionDefinition } from '@langchain/core/language_models/base' FunctionDefinition,
ToolDefinition,
} from '@langchain/core/language_models/base'
import { JsonOutputToolsParser } from '@langchain/core/output_parsers/openai_tools'
import { Injectable, Logger } from '@nestjs/common' import { Injectable, Logger } from '@nestjs/common'
import { AiService } from '../ai.service' import { AiService } from '../ai.service'
@@ -16,23 +19,30 @@ export class AiWriterService {
text: string, text: string,
parameters: FunctionDefinition['parameters'], parameters: FunctionDefinition['parameters'],
) { ) {
const functionSchema: FunctionDefinition = { const toolDefinition: ToolDefinition = {
name: 'extractor', type: 'function',
description: 'Extracts fields from the input.', function: {
parameters, name: 'extractor',
description: 'Extracts fields from the input.',
parameters,
},
} }
const model = await this.aiService.getOpenAiChain() const model = await this.aiService.getOpenAiChain()
const parser = new JsonOutputFunctionsParser() const parser = new JsonOutputToolsParser()
const runnable = model const runnable = model
.bind({ .bind({
functions: [functionSchema], tools: [toolDefinition],
function_call: { name: 'extractor' }, tool_choice: { type: 'function', function: { name: 'extractor' } },
}) })
.pipe(parser) .pipe(parser)
const result = await runnable.invoke([text]) const result = (await runnable.invoke([text])) as any[]
return result if (result.length === 0) {
return {}
}
// Extract just the args object from the first tool call response
return result[0]?.args || {}
} }
async generateTitleAndSlugByOpenAI(text: string) { async generateTitleAndSlugByOpenAI(text: string) {
return this.queryByFunctionSchema(text, { return this.queryByFunctionSchema(text, {

View File

@@ -1,7 +1,4 @@
import type { ChatModel } from 'openai/resources'
export const DEFAULT_SUMMARY_LANG = 'zh' export const DEFAULT_SUMMARY_LANG = 'zh'
type _Models = ChatModel
export const LANGUAGE_CODE_TO_NAME = { export const LANGUAGE_CODE_TO_NAME = {
ar: 'Arabic', ar: 'Arabic',
bg: 'Bulgarian', bg: 'Bulgarian',
@@ -46,150 +43,3 @@ export const LANGUAGE_CODE_TO_NAME = {
vi: 'Vietnamese', vi: 'Vietnamese',
zh: 'Chinese', zh: 'Chinese',
} }
export const OpenAiSupportedModels = [
{
label: 'o3-mini',
value: 'o3-mini',
},
{
label: 'o3-mini-2025-01-31',
value: 'o3-mini-2025-01-31',
},
{
label: 'o1',
value: 'o1',
},
{
label: 'o1-2024-12-17',
value: 'o1-2024-12-17',
},
{
label: 'o1-preview',
value: 'o1-preview',
},
{
label: 'o1-preview-2024-09-12',
value: 'o1-preview-2024-09-12',
},
{
label: 'o1-mini',
value: 'o1-mini',
},
{
label: 'o1-mini-2024-09-12',
value: 'o1-mini-2024-09-12',
},
{
label: 'gpt-4o',
value: 'gpt-4o',
},
{
label: 'gpt-4o-2024-11-20',
value: 'gpt-4o-2024-11-20',
},
{
label: 'gpt-4o-2024-08-06',
value: 'gpt-4o-2024-08-06',
},
{
label: 'gpt-4o-2024-05-13',
value: 'gpt-4o-2024-05-13',
},
{
label: 'gpt-4o-audio-preview',
value: 'gpt-4o-audio-preview',
},
{
label: 'gpt-4o-audio-preview-2024-10-01',
value: 'gpt-4o-audio-preview-2024-10-01',
},
{
label: 'gpt-4o-audio-preview-2024-12-17',
value: 'gpt-4o-audio-preview-2024-12-17',
},
{
label: 'gpt-4o-mini-audio-preview',
value: 'gpt-4o-mini-audio-preview',
},
{
label: 'gpt-4o-mini-2024-07-18',
value: 'gpt-4o-mini-2024-07-18',
},
{
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,5 +1,6 @@
import { Module } from '@nestjs/common' import { forwardRef, Module } from '@nestjs/common'
import { AiAgentModule } from './ai-agent/ai-agent.module'
import { AiSummaryController } from './ai-summary/ai-summary.controller' import { AiSummaryController } from './ai-summary/ai-summary.controller'
import { AiSummaryService } from './ai-summary/ai-summary.service' import { AiSummaryService } from './ai-summary/ai-summary.service'
import { AiWriterController } from './ai-writer/ai-writer.controller' import { AiWriterController } from './ai-writer/ai-writer.controller'
@@ -7,6 +8,7 @@ import { AiWriterService } from './ai-writer/ai-writer.service'
import { AiService } from './ai.service' import { AiService } from './ai.service'
@Module({ @Module({
imports: [forwardRef(() => AiAgentModule)],
providers: [AiSummaryService, AiService, AiWriterService], providers: [AiSummaryService, AiService, AiWriterService],
controllers: [AiSummaryController, AiWriterController], controllers: [AiSummaryController, AiWriterController],
exports: [AiService], exports: [AiService],

View File

@@ -87,7 +87,7 @@ export const generateDefaultConfig: () => IConfig = () => ({
enableAutoGenerateSummary: false, enableAutoGenerateSummary: false,
enableSummary: false, enableSummary: false,
openAiEndpoint: '', openAiEndpoint: '',
openAiPreferredModel: 'gpt-3.5-turbo', openAiPreferredModel: 'gpt-4o-mini',
openAiKey: '', openAiKey: '',
aiSummaryTargetLanguage: 'auto', aiSummaryTargetLanguage: 'auto',
}, },

View File

@@ -19,7 +19,6 @@ import type { ChatModel } from 'openai/resources'
import { IsAllowedUrl } from '~/decorators/dto/isAllowedUrl' import { IsAllowedUrl } from '~/decorators/dto/isAllowedUrl'
import { OpenAiSupportedModels } from '../ai/ai.constants'
import { Encrypt } from './configs.encrypt.util' import { Encrypt } from './configs.encrypt.util'
import { import {
halfFieldOption, halfFieldOption,
@@ -431,13 +430,8 @@ export class AIDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
@JSONSchemaPlainField('OpenAI 默认模型', { @JSONSchemaPlainField('OpenAI 默认模型')
'ui:options': { openAiPreferredModel: string
type: 'select',
values: OpenAiSupportedModels,
},
})
openAiPreferredModel: ChatModel
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()

View File

@@ -0,0 +1,26 @@
import { forwardRef, Module } from '@nestjs/common'
import { CategoryModule } from '../category/category.module'
import { CommentModule } from '../comment/comment.module'
import { NoteModule } from '../note/note.module'
import { PageModule } from '../page/page.module'
import { PostModule } from '../post/post.module'
import { RecentlyModule } from '../recently/recently.module'
import { SayModule } from '../say/say.module'
import { McpService } from './mcp.service'
@Module({
imports: [
forwardRef(() => NoteModule),
forwardRef(() => PostModule),
forwardRef(() => CategoryModule),
forwardRef(() => PageModule),
forwardRef(() => SayModule),
forwardRef(() => RecentlyModule),
forwardRef(() => CommentModule),
],
providers: [McpService],
exports: [McpService],
})
export class McpModule {}

View File

@@ -0,0 +1,327 @@
import { Injectable } from '@nestjs/common'
import { CannotFindException } from '~/common/exceptions/cant-find.exception'
import { CategoryService } from '../category/category.service'
import { CommentService } from '../comment/comment.service'
import { NoteService } from '../note/note.service'
import { PageService } from '../page/page.service'
import { PostService } from '../post/post.service'
import { RecentlyService } from '../recently/recently.service'
import { SayService } from '../say/say.service'
@Injectable()
export class McpService {
constructor(
private readonly postService: PostService,
private readonly noteService: NoteService,
private readonly categoryService: CategoryService,
private readonly pageService: PageService,
private readonly sayService: SayService,
private readonly recentlyService: RecentlyService,
private readonly commentService: CommentService,
) {}
/**
* Get a post by ID
* @param id Post ID
* @returns The requested post
*/
async getPostById(id: string) {
const post = await this.postService.model
.findById(id)
.populate('category')
.populate({
path: 'related',
select: 'title slug id _id categoryId category',
})
if (!post) {
throw new CannotFindException()
}
return post
}
/**
* Get posts with pagination
* @param page Page number
* @param size Page size
* @returns Paginated posts
*/
async getPosts(page = 1, size = 10) {
const query = this.postService.model
.find()
.populate('category')
.sort({ created: -1 })
.skip((page - 1) * size)
.limit(size)
const posts = await query.exec()
const total = await this.postService.model.countDocuments()
return {
data: posts,
pagination: {
total,
size,
currentPage: page,
totalPage: Math.ceil(total / size),
},
}
}
/**
* Get a note by ID
* @param id Note ID or nid
* @returns The requested note
*/
async getNoteById(id: string | number) {
const note = await this.noteService.findOneByIdOrNid(id)
if (!note) {
throw new CannotFindException()
}
return note
}
/**
* Get notes with pagination
* @param page Page number
* @param size Page size
* @returns Paginated notes
*/
async getNotes(page = 1, size = 10) {
const query = this.noteService.model
.find({ hide: false })
.sort({ created: -1 })
.skip((page - 1) * size)
.limit(size)
const notes = await query.exec()
const total = await this.noteService.model.countDocuments({ hide: false })
return {
data: notes,
pagination: {
total,
size,
currentPage: page,
totalPage: Math.ceil(total / size),
},
}
}
async getLatestPost() {
const post = await this.postService.model.findOne().sort({ created: -1 })
if (!post) {
throw new CannotFindException()
}
return post
}
async getLatestNotes() {
const notes = await this.noteService.model.findOne().sort({ created: -1 })
if (!notes) {
throw new CannotFindException()
}
return notes
}
/**
* Get a category by ID
* @param id Category ID
* @returns The requested category with post count
*/
async getCategoryById(id: string) {
const category = await this.categoryService.findCategoryById(id)
if (!category) {
throw new CannotFindException()
}
return category
}
/**
* Get all categories
* @returns All categories with post counts
*/
async getAllCategories() {
return await this.categoryService.findAllCategory()
}
/**
* Get posts in a specific category
* @param categoryId Category ID
* @returns Posts in the specified category
*/
async getPostsByCategory(categoryId: string) {
const posts = await this.categoryService.findCategoryPost(categoryId)
if (!posts || posts.length === 0) {
throw new CannotFindException()
}
return posts
}
/**
* Get a summary of all tags and their post counts
* @returns Tags with post counts
*/
async getTagsSummary() {
return await this.categoryService.getPostTagsSum()
}
/**
* Get posts with a specific tag
* @param tag Tag name
* @returns Posts with the specified tag
*/
async getPostsByTag(tag: string) {
return await this.categoryService.findArticleWithTag(tag)
}
/**
* Get a page by ID
* @param id Page ID
* @returns The requested page
*/
async getPageById(id: string) {
const page = await this.pageService.model.findById(id)
if (!page) {
throw new CannotFindException()
}
return page
}
/**
* Get all pages
* @returns All pages
*/
async getAllPages() {
const pages = await this.pageService.model.find().sort({ order: 1 })
return pages
}
/**
* Get all says (quotes/status updates)
* @returns All says
*/
async getAllSays() {
const says = await this.sayService.model.find().sort({ created: -1 })
return says
}
/**
* Get a random say
* @returns A random say
*/
async getRandomSay() {
const res = await this.sayService.model.find({}).lean()
if (res.length === 0) {
throw new CannotFindException()
}
// Get a random item from the array
const randomIndex = Math.floor(Math.random() * res.length)
return res[randomIndex]
}
/**
* Get all recently activity
* @returns All recently activity
*/
async getAllRecently() {
return await this.recentlyService.getAll()
}
/**
* Get a specific recently activity by ID
* @param id Recently activity ID
* @returns The requested recently activity
*/
async getRecentlyById(id: string) {
const recently = await this.recentlyService.getOne(id)
if (!recently) {
throw new CannotFindException()
}
return recently
}
/**
* Get latest recently activities with pagination
* @param size Number of items to retrieve
* @param before Retrieve items before this ID
* @param after Retrieve items after this ID
* @returns Paginated recently activities
*/
async getRecentlyOffset({
size = 10,
before,
after,
}: {
size?: number
before?: string
after?: string
}) {
return await this.recentlyService.getOffset({ size, before, after })
}
/**
* Get the latest recently activity
* @returns The latest recently activity
*/
async getLatestRecently() {
const recently = await this.recentlyService.getLatestOne()
if (!recently) {
throw new CannotFindException()
}
return recently
}
/**
* Get comments with pagination
* @param page Page number
* @param size Page size
* @param state Comment state filter (0 = all)
* @returns Paginated comments
*/
async getComments({ page = 1, size = 10, state = 0 } = {}) {
return await this.commentService.getComments({ page, size, state })
}
/**
* Get comments for a specific content
* @param id Content ID
* @param type Content type (post, page, note, etc.)
* @returns Comments for the specified content
*/
async getContentComments(id: string, type?: string) {
const allowComment = await this.commentService.allowComment(id, type as any)
if (!allowComment) {
return []
}
const comments = await this.commentService.model.find({
ref: id,
})
await this.commentService.fillAndReplaceAvatarUrl(comments)
return comments
}
}

View File

@@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"packageManager": "pnpm@9.15.9", "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"license": "AGPLv3", "license": "AGPLv3",
"homepage": "https://github.com/mx-space/core#readme", "homepage": "https://github.com/mx-space/core#readme",
"repository": { "repository": {
@@ -24,23 +24,31 @@
"redis-memory-server": "0.12.1" "redis-memory-server": "0.12.1"
}, },
"dependencies": { "dependencies": {
"zx-cjs": "7.0.7-0" "zx-cjs": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@innei/prettier": "0.15.0", "@innei/prettier": "catalog:",
"@sxzz/eslint-config": "6.1.1", "@sxzz/eslint-config": "catalog:",
"@types/node": "22.14.0", "@types/node": "catalog:conflicts_@types/node_22_14_0",
"cross-env": "7.0.3", "cross-env": "catalog:",
"eslint": "^9.24.0", "eslint": "catalog:",
"lint-staged": "15.5.0", "lint-staged": "catalog:",
"prettier": "3.5.3", "prettier": "catalog:",
"rimraf": "6.0.1", "rimraf": "catalog:",
"simple-git-hooks": "2.12.1", "simple-git-hooks": "catalog:",
"ts-node": "10.9.2", "ts-node": "catalog:",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "catalog:",
"tsup": "8.4.0", "tsup": "catalog:",
"typescript": "5.8.3", "typescript": "catalog:",
"vite-tsconfig-paths": "5.1.4" "vite-tsconfig-paths": "catalog:"
},
"resolutions": {
"get-pixels@^3>request": "./external/request",
"mongodb": "6.12.0",
"pino": "./external/pino",
"semver": "7.7.1",
"typescript": "5.7.3",
"whatwg-url": "14.1.1"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"pre-commit": "pnpm lint-staged" "pre-commit": "pnpm lint-staged"
@@ -51,13 +59,5 @@
"prettier --ignore-path ./.prettierignore --write " "prettier --ignore-path ./.prettierignore --write "
] ]
}, },
"issues": "https://github.com/mx-space/core/issues", "issues": "https://github.com/mx-space/core/issues"
"resolutions": { }
"get-pixels@^3>request": "./external/request",
"mongodb": "6.12.0",
"pino": "./external/pino",
"semver": "7.7.1",
"typescript": "5.7.3",
"whatwg-url": "14.1.1"
}
}

View File

@@ -6,14 +6,16 @@
"exports": { "exports": {
".": "./dist/index.cjs", ".": "./dist/index.cjs",
"./zx-global": "./zx-global.cjs", "./zx-global": "./zx-global.cjs",
"./auth": "./dist/auth.cjs" "./auth": "./dist/auth.cjs",
"./zod": "./dist/zod.cjs"
}, },
"scripts": { "scripts": {
"build": "tsup" "build": "tsup"
}, },
"devDependencies": { "devDependencies": {
"better-auth": "1.2.5", "better-auth": "catalog:",
"nanoid": "5.1.5", "nanoid": "catalog:",
"zx": "7.2.3" "zod": "catalog:",
"zx": "catalog:"
} }
} }

View File

@@ -3,7 +3,7 @@ import { defineConfig } from 'tsup'
export default defineConfig({ export default defineConfig({
clean: true, clean: true,
target: 'es2020', target: 'es2020',
entry: ['index.ts', 'auth.ts'], entry: ['index.ts', 'auth.ts', 'zod.ts'],
dts: true, dts: true,
external: ['mongodb'], external: ['mongodb'],
format: ['cjs'], format: ['cjs'],

1
packages/compiled/zod.ts Normal file
View File

@@ -0,0 +1 @@
export * from './node_modules/zod'

1452
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,136 @@
packages: packages:
- packages/* - packages/*
- apps/* - apps/*
catalog:
'@algolia/client-search': ^4.22.1
'@antfu/install-pkg': 1.0.0
'@aws-sdk/client-s3': 3.802.0
'@babel/core': 7.27.1
'@babel/plugin-transform-modules-commonjs': 7.27.1
'@babel/plugin-transform-typescript': 7.27.1
'@babel/types': ^7.27.1
'@fastify/cookie': 11.0.2
'@fastify/multipart': 9.0.3
'@fastify/static': 8.1.1
'@innei/next-async': 0.3.0
'@innei/prettier': 0.15.0
'@innei/pretty-logger-nestjs': 0.3.3
'@keyv/redis': 4.4.0
'@langchain/core': 0.3.51
'@langchain/openai': 0.5.10
'@modelcontextprotocol/sdk': 1.11.0
'@nestjs/cache-manager': 3.0.1
'@nestjs/cli': 11.0.7
'@nestjs/common': 11.1.0
'@nestjs/core': 11.1.0
'@nestjs/event-emitter': 3.0.1
'@nestjs/mapped-types': ^2.1.0
'@nestjs/platform-fastify': 11.1.0
'@nestjs/platform-socket.io': 11.1.0
'@nestjs/schedule': 6.0.0
'@nestjs/schematics': 11.0.5
'@nestjs/testing': 11.1.0
'@nestjs/throttler': 6.4.0
'@nestjs/websockets': 11.1.0
'@simplewebauthn/server': 10.0.1
'@socket.io/redis-adapter': 8.3.0
'@socket.io/redis-emitter': 5.1.0
'@swc/core': 1.11.24
'@sxzz/eslint-config': 6.1.1
'@typegoose/auto-increment': 4.13.0
'@typegoose/typegoose': 12.15.0
'@types/babel__core': 7.20.5
'@types/bcryptjs': ^3.0.0
'@types/cls-hooked': ^4.3.9
'@types/ejs': 3.1.5
'@types/get-image-colors': 4.0.5
'@types/js-yaml': 4.0.9
'@types/jsonwebtoken': 9.0.9
'@types/lodash': 4.17.16
'@types/mime-types': 2.1.4
'@types/mongoose-aggregate-paginate-v2': 1.0.12
'@types/node': 22.15.3
'@types/nodemailer': 6.4.17
'@types/qs': 6.9.18
'@types/remove-markdown': 0.3.4
'@types/semver': 7.7.0
'@types/ua-parser-js': 0.7.39
'@types/validator': 13.15.0
'@vercel/ncc': 0.38.3
algoliasearch: 4.24.0
axios: ^1.9.0
axios-retry: 4.5.0
bcryptjs: ^3.0.2
better-auth: 1.2.5
blurhash: 2.0.5
cache-manager: 6.4.2
class-transformer: 0.5.1
class-validator: 0.13.2
class-validator-jsonschema: npm:@innei/class-validator-jsonschema@3.1.2
cls-hooked: ^4.2.2
commander: 13.1.0
cron: ^3.5.0
cross-env: 7.0.3
dayjs: 1.11.13
ejs: 3.1.10
eslint: ^9.24.0
form-data: 4.0.2
inquirer: ^10.2.2
ioredis: 5.6.1
isbot: 5.1.27
js-yaml: ^4.1.0
json5: 2.2.3
jsonwebtoken: 9.0.2
jszip: 3.10.1
keyv: 5.3.3
langchain: 0.3.24
linkedom: 0.18.10
lint-staged: 15.5.0
lodash: ^4.17.21
lru-cache: 11.1.0
marked: 15.0.11
mime-types: ^3.0.1
mkdirp: ^3.0.1
mongoose: 8.14.1
mongoose-aggregate-paginate-v2: 1.1.4
mongoose-autopopulate: 1.1.0
mongoose-lean-getters: 2.2.1
mongoose-lean-virtuals: 1.1.0
mongoose-paginate-v2: 1.9.0
nanoid: 5.1.5
node-machine-id: 1.1.12
nodemailer: 7.0.0
openai: 4.97.0
pluralize: ^8.0.0
prettier: 3.5.3
qs: 6.14.0
reflect-metadata: 0.2.2
remove-markdown: 0.6.2
remove-md-codeblock: 0.0.4
rimraf: 6.0.1
rxjs: 7.8.2
semver: 7.7.1
sharp: 0.34.1
simple-git-hooks: 2.12.1
slugify: 1.6.6
snakecase-keys: 6.0.0
socket.io: ^4.8.1
source-map-support: ^0.5.21
ts-node: 10.9.2
tsconfig-paths: 4.2.0
tsup: 8.4.0
typescript: 5.8.3
ua-parser-js: 2.0.3
unplugin-swc: 1.5.2
vite: 5.4.10
vite-tsconfig-paths: 5.1.4
vitest: 1.5.2
vm2: 3.9.19
wildcard-match: 5.1.4
xss: 1.0.15
zod: 3.24.3
zx: 7.2.3
zx-cjs: 7.0.7-0
catalogs:
conflicts_@types/node_22_14_0:
'@types/node': 22.14.0