337 lines
8.5 KiB
TypeScript
337 lines
8.5 KiB
TypeScript
import { Injectable, Inject } from '@nestjs/common';
|
|
import { EmbeddingService } from '../infrastructure/embedding/embedding.service';
|
|
import { ChunkingService } from '../application/services/chunking.service';
|
|
import {
|
|
IKnowledgeRepository,
|
|
KNOWLEDGE_REPOSITORY,
|
|
} from '../domain/repositories/knowledge.repository.interface';
|
|
import {
|
|
KnowledgeArticleEntity,
|
|
KnowledgeSource,
|
|
} from '../domain/entities/knowledge-article.entity';
|
|
|
|
/**
|
|
* 知识管理服务
|
|
* 提供知识库的CRUD操作和处理
|
|
*/
|
|
@Injectable()
|
|
export class KnowledgeService {
|
|
constructor(
|
|
private embeddingService: EmbeddingService,
|
|
private chunkingService: ChunkingService,
|
|
@Inject(KNOWLEDGE_REPOSITORY)
|
|
private knowledgeRepo: IKnowledgeRepository,
|
|
) {}
|
|
|
|
/**
|
|
* 创建知识文章
|
|
*/
|
|
async createArticle(params: {
|
|
title: string;
|
|
content: string;
|
|
category: string;
|
|
tags?: string[];
|
|
source?: KnowledgeSource;
|
|
sourceUrl?: string;
|
|
createdBy?: string;
|
|
autoPublish?: boolean;
|
|
}): Promise<KnowledgeArticleEntity> {
|
|
// 1. 创建文章实体
|
|
const article = KnowledgeArticleEntity.create({
|
|
title: params.title,
|
|
content: params.content,
|
|
category: params.category,
|
|
tags: params.tags,
|
|
source: params.source || KnowledgeSource.MANUAL,
|
|
sourceUrl: params.sourceUrl,
|
|
createdBy: params.createdBy,
|
|
});
|
|
|
|
// 2. 生成文章向量
|
|
const embedding = await this.embeddingService.getEmbedding(
|
|
`${article.title}\n${article.summary}`,
|
|
);
|
|
article.setEmbedding(embedding);
|
|
|
|
// 3. 自动发布(可选)
|
|
if (params.autoPublish) {
|
|
article.publish();
|
|
}
|
|
|
|
// 4. 保存文章
|
|
await this.knowledgeRepo.saveArticle(article);
|
|
|
|
// 5. 分块并保存
|
|
await this.processArticleChunks(article);
|
|
|
|
console.log(`[KnowledgeService] Created article: ${article.id} - ${article.title}`);
|
|
|
|
return article;
|
|
}
|
|
|
|
/**
|
|
* 处理文章分块
|
|
*/
|
|
private async processArticleChunks(article: KnowledgeArticleEntity): Promise<void> {
|
|
// 1. 删除旧的块
|
|
await this.knowledgeRepo.deleteChunksByArticleId(article.id);
|
|
|
|
// 2. 分块
|
|
const chunks = this.chunkingService.chunkArticle(article);
|
|
|
|
// 3. 批量生成向量
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
chunks.map(c => c.content),
|
|
);
|
|
|
|
// 4. 设置向量
|
|
chunks.forEach((chunk, index) => {
|
|
chunk.setEmbedding(embeddings[index]);
|
|
});
|
|
|
|
// 5. 保存块
|
|
await this.knowledgeRepo.saveChunks(chunks);
|
|
|
|
console.log(`[KnowledgeService] Processed ${chunks.length} chunks for article ${article.id}`);
|
|
}
|
|
|
|
/**
|
|
* 更新文章
|
|
*/
|
|
async updateArticle(
|
|
articleId: string,
|
|
params: {
|
|
title?: string;
|
|
content?: string;
|
|
category?: string;
|
|
tags?: string[];
|
|
updatedBy?: string;
|
|
},
|
|
): Promise<KnowledgeArticleEntity> {
|
|
const article = await this.knowledgeRepo.findArticleById(articleId);
|
|
if (!article) {
|
|
throw new Error(`Article not found: ${articleId}`);
|
|
}
|
|
|
|
// 更新字段
|
|
if (params.title || params.content) {
|
|
article.updateContent(
|
|
params.title || article.title,
|
|
params.content || article.content,
|
|
params.updatedBy,
|
|
);
|
|
|
|
// 重新生成向量
|
|
const embedding = await this.embeddingService.getEmbedding(
|
|
`${article.title}\n${article.summary}`,
|
|
);
|
|
article.setEmbedding(embedding);
|
|
|
|
// 重新分块
|
|
await this.processArticleChunks(article);
|
|
}
|
|
|
|
if (params.category) {
|
|
article.category = params.category;
|
|
}
|
|
|
|
if (params.tags) {
|
|
article.tags = params.tags;
|
|
}
|
|
|
|
await this.knowledgeRepo.updateArticle(article);
|
|
|
|
return article;
|
|
}
|
|
|
|
/**
|
|
* 获取文章详情
|
|
*/
|
|
async getArticle(articleId: string): Promise<KnowledgeArticleEntity | null> {
|
|
return this.knowledgeRepo.findArticleById(articleId);
|
|
}
|
|
|
|
/**
|
|
* 获取文章列表
|
|
*/
|
|
async listArticles(params: {
|
|
category?: string;
|
|
publishedOnly?: boolean;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}): Promise<{
|
|
items: KnowledgeArticleEntity[];
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
}> {
|
|
const page = params.page || 1;
|
|
const pageSize = params.pageSize || 20;
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
const [items, total] = await Promise.all([
|
|
this.knowledgeRepo.findArticlesByCategory(params.category || '', {
|
|
publishedOnly: params.publishedOnly,
|
|
limit: pageSize,
|
|
offset,
|
|
}),
|
|
this.knowledgeRepo.countArticles({
|
|
category: params.category,
|
|
publishedOnly: params.publishedOnly,
|
|
}),
|
|
]);
|
|
|
|
return { items, total, page, pageSize };
|
|
}
|
|
|
|
/**
|
|
* 搜索文章
|
|
*/
|
|
async searchArticles(params: {
|
|
query: string;
|
|
category?: string;
|
|
useVector?: boolean;
|
|
}): Promise<Array<{
|
|
article: KnowledgeArticleEntity;
|
|
similarity?: number;
|
|
}>> {
|
|
if (params.useVector) {
|
|
// 向量搜索
|
|
const embedding = await this.embeddingService.getEmbedding(params.query);
|
|
const results = await this.knowledgeRepo.searchArticlesByVector(embedding, {
|
|
category: params.category,
|
|
publishedOnly: true,
|
|
limit: 10,
|
|
});
|
|
return results;
|
|
}
|
|
|
|
// 关键词搜索
|
|
const articles = await this.knowledgeRepo.searchArticles(params.query, {
|
|
category: params.category,
|
|
publishedOnly: true,
|
|
limit: 10,
|
|
});
|
|
return articles.map(article => ({ article }));
|
|
}
|
|
|
|
/**
|
|
* 发布文章
|
|
*/
|
|
async publishArticle(articleId: string): Promise<void> {
|
|
const article = await this.knowledgeRepo.findArticleById(articleId);
|
|
if (!article) {
|
|
throw new Error(`Article not found: ${articleId}`);
|
|
}
|
|
|
|
article.publish();
|
|
await this.knowledgeRepo.updateArticle(article);
|
|
}
|
|
|
|
/**
|
|
* 取消发布
|
|
*/
|
|
async unpublishArticle(articleId: string): Promise<void> {
|
|
const article = await this.knowledgeRepo.findArticleById(articleId);
|
|
if (!article) {
|
|
throw new Error(`Article not found: ${articleId}`);
|
|
}
|
|
|
|
article.unpublish();
|
|
await this.knowledgeRepo.updateArticle(article);
|
|
}
|
|
|
|
/**
|
|
* 删除文章
|
|
*/
|
|
async deleteArticle(articleId: string): Promise<void> {
|
|
// 先删除分块
|
|
await this.knowledgeRepo.deleteChunksByArticleId(articleId);
|
|
// 再删除文章
|
|
await this.knowledgeRepo.deleteArticle(articleId);
|
|
}
|
|
|
|
/**
|
|
* 记录用户反馈
|
|
*/
|
|
async recordFeedback(articleId: string, helpful: boolean): Promise<void> {
|
|
const article = await this.knowledgeRepo.findArticleById(articleId);
|
|
if (!article) {
|
|
throw new Error(`Article not found: ${articleId}`);
|
|
}
|
|
|
|
article.recordFeedback(helpful);
|
|
await this.knowledgeRepo.updateArticle(article);
|
|
}
|
|
|
|
/**
|
|
* 批量导入文章
|
|
*/
|
|
async importArticles(
|
|
articles: Array<{
|
|
title: string;
|
|
content: string;
|
|
category: string;
|
|
tags?: string[];
|
|
}>,
|
|
createdBy?: string,
|
|
): Promise<{ success: number; failed: number; errors: string[] }> {
|
|
let success = 0;
|
|
let failed = 0;
|
|
const errors: string[] = [];
|
|
|
|
for (const articleData of articles) {
|
|
try {
|
|
await this.createArticle({
|
|
...articleData,
|
|
source: KnowledgeSource.IMPORT,
|
|
createdBy,
|
|
autoPublish: false, // 导入后需要审核
|
|
});
|
|
success++;
|
|
} catch (error) {
|
|
failed++;
|
|
errors.push(`Failed to import "${articleData.title}": ${(error as Error).message}`);
|
|
}
|
|
}
|
|
|
|
return { success, failed, errors };
|
|
}
|
|
|
|
/**
|
|
* 获取知识库统计
|
|
*/
|
|
async getStatistics(): Promise<{
|
|
totalArticles: number;
|
|
publishedArticles: number;
|
|
byCategory: Record<string, number>;
|
|
recentArticles: KnowledgeArticleEntity[];
|
|
}> {
|
|
const categories = ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TechTAS', 'GENERAL'];
|
|
|
|
const [total, published, byCategoryResults, recent] = await Promise.all([
|
|
this.knowledgeRepo.countArticles(),
|
|
this.knowledgeRepo.countArticles({ publishedOnly: true }),
|
|
Promise.all(
|
|
categories.map(async cat => ({
|
|
category: cat,
|
|
count: await this.knowledgeRepo.countArticles({ category: cat }),
|
|
})),
|
|
),
|
|
this.knowledgeRepo.findArticlesByCategory('', { limit: 5 }),
|
|
]);
|
|
|
|
const byCategory: Record<string, number> = {};
|
|
byCategoryResults.forEach(r => {
|
|
byCategory[r.category] = r.count;
|
|
});
|
|
|
|
return {
|
|
totalArticles: total,
|
|
publishedArticles: published,
|
|
byCategory,
|
|
recentArticles: recent,
|
|
};
|
|
}
|
|
}
|