feat(backend): implement knowledge base management backend API
This commit is contained in:
46
backend/package-lock.json
generated
46
backend/package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^11.1.0",
|
||||
"@fastify/jwt": "^10.0.0",
|
||||
"@fastify/multipart": "^9.2.1",
|
||||
"@prisma/client": "^6.17.0",
|
||||
"@types/form-data": "^2.2.1",
|
||||
"axios": "^1.12.2",
|
||||
@@ -507,6 +508,12 @@
|
||||
"fast-uri": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/busboy": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@fastify/busboy/-/busboy-3.2.0.tgz",
|
||||
"integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fastify/cors": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@fastify/cors/-/cors-11.1.0.tgz",
|
||||
@@ -527,6 +534,22 @@
|
||||
"toad-cache": "^3.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/deepmerge": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@fastify/deepmerge/-/deepmerge-3.1.0.tgz",
|
||||
"integrity": "sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fastify/error": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@fastify/error/-/error-4.2.0.tgz",
|
||||
@@ -620,6 +643,29 @@
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/multipart": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@fastify/multipart/-/multipart-9.2.1.tgz",
|
||||
"integrity": "sha512-U4221XDMfzCUtfzsyV1/PkR4MNgKI0158vUUyn/oF2Tl6RxMc+N7XYLr5fZXQiEC+Fmw5zFaTjxsTGTgtDtK+g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^3.0.0",
|
||||
"@fastify/deepmerge": "^3.0.0",
|
||||
"@fastify/error": "^4.0.0",
|
||||
"fastify-plugin": "^5.0.0",
|
||||
"secure-json-parse": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/proxy-addr": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^11.1.0",
|
||||
"@fastify/jwt": "^10.0.0",
|
||||
"@fastify/multipart": "^9.2.1",
|
||||
"@prisma/client": "^6.17.0",
|
||||
"@types/form-data": "^2.2.1",
|
||||
"axios": "^1.12.2",
|
||||
|
||||
246
backend/src/controllers/documentController.ts
Normal file
246
backend/src/controllers/documentController.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import * as documentService from '../services/documentService.js';
|
||||
|
||||
// Mock用户ID(实际应从JWT token中获取)
|
||||
const MOCK_USER_ID = 'user-mock-001';
|
||||
|
||||
/**
|
||||
* 上传文档
|
||||
*/
|
||||
export async function uploadDocument(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
kbId: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { kbId } = request.params;
|
||||
|
||||
// 获取上传的文件
|
||||
const data = await request.file();
|
||||
|
||||
if (!data) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: 'No file uploaded',
|
||||
});
|
||||
}
|
||||
|
||||
const file = await data.toBuffer();
|
||||
const filename = data.filename;
|
||||
const fileType = data.mimetype;
|
||||
const fileSizeBytes = file.length;
|
||||
|
||||
// 文件大小限制(10MB)
|
||||
if (fileSizeBytes > 10 * 1024 * 1024) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: 'File size exceeds 10MB limit',
|
||||
});
|
||||
}
|
||||
|
||||
// 文件类型限制
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain',
|
||||
'text/markdown',
|
||||
];
|
||||
|
||||
if (!allowedTypes.includes(fileType)) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: 'File type not supported. Allowed: PDF, DOC, DOCX, TXT, MD',
|
||||
});
|
||||
}
|
||||
|
||||
// 上传文档(这里fileUrl暂时为空,实际应该上传到对象存储)
|
||||
const document = await documentService.uploadDocument(
|
||||
MOCK_USER_ID,
|
||||
kbId,
|
||||
file,
|
||||
filename,
|
||||
fileType,
|
||||
fileSizeBytes,
|
||||
'' // fileUrl - 可以上传到OSS后填入
|
||||
);
|
||||
|
||||
return reply.status(201).send({
|
||||
success: true,
|
||||
data: document,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to upload document:', error);
|
||||
|
||||
if (error.message.includes('not found') || error.message.includes('access denied')) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('limit exceeded')) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to upload document',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档列表
|
||||
*/
|
||||
export async function getDocuments(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
kbId: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { kbId } = request.params;
|
||||
|
||||
const documents = await documentService.getDocuments(MOCK_USER_ID, kbId);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: documents,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get documents:', error);
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to get documents',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档详情
|
||||
*/
|
||||
export async function getDocumentById(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
id: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
|
||||
const document = await documentService.getDocumentById(MOCK_USER_ID, id);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: document,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get document:', error);
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to get document',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
*/
|
||||
export async function deleteDocument(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
id: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
|
||||
await documentService.deleteDocument(MOCK_USER_ID, id);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
message: 'Document deleted successfully',
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to delete document:', error);
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to delete document',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新处理文档
|
||||
*/
|
||||
export async function reprocessDocument(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
id: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
|
||||
await documentService.reprocessDocument(MOCK_USER_ID, id);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
message: 'Document reprocessing started',
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to reprocess document:', error);
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to reprocess document',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
291
backend/src/controllers/knowledgeBaseController.ts
Normal file
291
backend/src/controllers/knowledgeBaseController.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import * as knowledgeBaseService from '../services/knowledgeBaseService.js';
|
||||
|
||||
// Mock用户ID(实际应从JWT token中获取)
|
||||
const MOCK_USER_ID = 'user-mock-001';
|
||||
|
||||
/**
|
||||
* 创建知识库
|
||||
*/
|
||||
export async function createKnowledgeBase(
|
||||
request: FastifyRequest<{
|
||||
Body: {
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { name, description } = request.body;
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: 'Knowledge base name is required',
|
||||
});
|
||||
}
|
||||
|
||||
const knowledgeBase = await knowledgeBaseService.createKnowledgeBase(
|
||||
MOCK_USER_ID,
|
||||
name,
|
||||
description
|
||||
);
|
||||
|
||||
return reply.status(201).send({
|
||||
success: true,
|
||||
data: knowledgeBase,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to create knowledge base:', error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to create knowledge base',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库列表
|
||||
*/
|
||||
export async function getKnowledgeBases(
|
||||
_request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const knowledgeBases = await knowledgeBaseService.getKnowledgeBases(
|
||||
MOCK_USER_ID
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: knowledgeBases,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get knowledge bases:', error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to get knowledge bases',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库详情
|
||||
*/
|
||||
export async function getKnowledgeBaseById(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
id: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
|
||||
const knowledgeBase = await knowledgeBaseService.getKnowledgeBaseById(
|
||||
MOCK_USER_ID,
|
||||
id
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: knowledgeBase,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get knowledge base:', error);
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to get knowledge base',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识库
|
||||
*/
|
||||
export async function updateKnowledgeBase(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
id: string;
|
||||
};
|
||||
Body: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const updateData = request.body;
|
||||
|
||||
const knowledgeBase = await knowledgeBaseService.updateKnowledgeBase(
|
||||
MOCK_USER_ID,
|
||||
id,
|
||||
updateData
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: knowledgeBase,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update knowledge base:', error);
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to update knowledge base',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识库
|
||||
*/
|
||||
export async function deleteKnowledgeBase(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
id: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
|
||||
await knowledgeBaseService.deleteKnowledgeBase(MOCK_USER_ID, id);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
message: 'Knowledge base deleted successfully',
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to delete knowledge base:', error);
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to delete knowledge base',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检索知识库
|
||||
*/
|
||||
export async function searchKnowledgeBase(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
id: string;
|
||||
};
|
||||
Querystring: {
|
||||
query: string;
|
||||
top_k?: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const { query, top_k } = request.query;
|
||||
|
||||
if (!query || query.trim().length === 0) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: 'Query parameter is required',
|
||||
});
|
||||
}
|
||||
|
||||
const topK = top_k ? parseInt(top_k, 10) : 3;
|
||||
|
||||
const results = await knowledgeBaseService.searchKnowledgeBase(
|
||||
MOCK_USER_ID,
|
||||
id,
|
||||
query,
|
||||
topK
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: results,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to search knowledge base:', error);
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to search knowledge base',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库统计信息
|
||||
*/
|
||||
export async function getKnowledgeBaseStats(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
id: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
|
||||
const stats = await knowledgeBaseService.getKnowledgeBaseStats(
|
||||
MOCK_USER_ID,
|
||||
id
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: stats,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get knowledge base stats:', error);
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || 'Failed to get knowledge base stats',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import Fastify from 'fastify';
|
||||
import cors from '@fastify/cors';
|
||||
import multipart from '@fastify/multipart';
|
||||
import { config, validateEnv } from './config/env.js';
|
||||
import { testDatabaseConnection, prisma } from './config/database.js';
|
||||
import { projectRoutes } from './routes/projects.js';
|
||||
import { agentRoutes } from './routes/agents.js';
|
||||
import { conversationRoutes } from './routes/conversations.js';
|
||||
import knowledgeBaseRoutes from './routes/knowledgeBases.js';
|
||||
|
||||
// 全局处理BigInt序列化
|
||||
(BigInt.prototype as any).toJSON = function() {
|
||||
return Number(this);
|
||||
};
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: {
|
||||
@@ -26,6 +33,13 @@ await fastify.register(cors, {
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// 注册文件上传插件
|
||||
await fastify.register(multipart, {
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB
|
||||
},
|
||||
});
|
||||
|
||||
// 健康检查路由
|
||||
fastify.get('/health', async () => {
|
||||
// 检查数据库连接
|
||||
@@ -63,6 +77,9 @@ await fastify.register(agentRoutes, { prefix: '/api/v1' });
|
||||
// 注册对话管理路由
|
||||
await fastify.register(conversationRoutes, { prefix: '/api/v1' });
|
||||
|
||||
// 注册知识库管理路由
|
||||
await fastify.register(knowledgeBaseRoutes, { prefix: '/api/v1' });
|
||||
|
||||
// 启动服务器
|
||||
const start = async () => {
|
||||
try {
|
||||
|
||||
46
backend/src/routes/knowledgeBases.ts
Normal file
46
backend/src/routes/knowledgeBases.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import * as knowledgeBaseController from '../controllers/knowledgeBaseController.js';
|
||||
import * as documentController from '../controllers/documentController.js';
|
||||
|
||||
export default async function knowledgeBaseRoutes(fastify: FastifyInstance) {
|
||||
// ==================== 知识库管理 API ====================
|
||||
|
||||
// 创建知识库
|
||||
fastify.post('/knowledge-bases', knowledgeBaseController.createKnowledgeBase);
|
||||
|
||||
// 获取知识库列表
|
||||
fastify.get('/knowledge-bases', knowledgeBaseController.getKnowledgeBases);
|
||||
|
||||
// 获取知识库详情
|
||||
fastify.get('/knowledge-bases/:id', knowledgeBaseController.getKnowledgeBaseById);
|
||||
|
||||
// 更新知识库
|
||||
fastify.put('/knowledge-bases/:id', knowledgeBaseController.updateKnowledgeBase);
|
||||
|
||||
// 删除知识库
|
||||
fastify.delete('/knowledge-bases/:id', knowledgeBaseController.deleteKnowledgeBase);
|
||||
|
||||
// 检索知识库
|
||||
fastify.get('/knowledge-bases/:id/search', knowledgeBaseController.searchKnowledgeBase);
|
||||
|
||||
// 获取知识库统计信息
|
||||
fastify.get('/knowledge-bases/:id/stats', knowledgeBaseController.getKnowledgeBaseStats);
|
||||
|
||||
// ==================== 文档管理 API ====================
|
||||
|
||||
// 上传文档
|
||||
fastify.post('/knowledge-bases/:kbId/documents', documentController.uploadDocument);
|
||||
|
||||
// 获取文档列表
|
||||
fastify.get('/knowledge-bases/:kbId/documents', documentController.getDocuments);
|
||||
|
||||
// 获取文档详情
|
||||
fastify.get('/documents/:id', documentController.getDocumentById);
|
||||
|
||||
// 删除文档
|
||||
fastify.delete('/documents/:id', documentController.deleteDocument);
|
||||
|
||||
// 重新处理文档
|
||||
fastify.post('/documents/:id/reprocess', documentController.reprocessDocument);
|
||||
}
|
||||
|
||||
327
backend/src/services/documentService.ts
Normal file
327
backend/src/services/documentService.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import { prisma } from '../config/database.js';
|
||||
import { difyClient } from '../clients/DifyClient.js';
|
||||
|
||||
/**
|
||||
* 文档服务
|
||||
*/
|
||||
|
||||
/**
|
||||
* 上传文档到知识库
|
||||
*/
|
||||
export async function uploadDocument(
|
||||
userId: string,
|
||||
kbId: string,
|
||||
file: Buffer,
|
||||
filename: string,
|
||||
fileType: string,
|
||||
fileSizeBytes: number,
|
||||
fileUrl: string
|
||||
) {
|
||||
// 1. 验证知识库权限
|
||||
const knowledgeBase = await prisma.knowledgeBase.findFirst({
|
||||
where: {
|
||||
id: kbId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!knowledgeBase) {
|
||||
throw new Error('Knowledge base not found or access denied');
|
||||
}
|
||||
|
||||
// 2. 检查文档数量限制(每个知识库最多50个文档)
|
||||
const documentCount = await prisma.document.count({
|
||||
where: { kbId },
|
||||
});
|
||||
|
||||
if (documentCount >= 50) {
|
||||
throw new Error('Document limit exceeded. Maximum 50 documents per knowledge base');
|
||||
}
|
||||
|
||||
// 3. 在数据库中创建文档记录(状态:uploading)
|
||||
const document = await prisma.document.create({
|
||||
data: {
|
||||
kbId,
|
||||
userId,
|
||||
filename,
|
||||
fileType,
|
||||
fileSizeBytes,
|
||||
fileUrl,
|
||||
difyDocumentId: '', // 暂时为空,稍后更新
|
||||
status: 'uploading',
|
||||
progress: 0,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// 4. 上传到Dify
|
||||
const difyResult = await difyClient.uploadDocumentDirectly(
|
||||
knowledgeBase.difyDatasetId,
|
||||
file,
|
||||
filename
|
||||
);
|
||||
|
||||
// 5. 更新文档记录(更新difyDocumentId和状态)
|
||||
const updatedDocument = await prisma.document.update({
|
||||
where: { id: document.id },
|
||||
data: {
|
||||
difyDocumentId: difyResult.document.id,
|
||||
status: difyResult.document.indexing_status,
|
||||
progress: 50,
|
||||
},
|
||||
});
|
||||
|
||||
// 6. 启动后台轮询任务,等待处理完成
|
||||
pollDocumentStatus(userId, kbId, document.id, difyResult.document.id).catch(error => {
|
||||
console.error('Failed to poll document status:', error);
|
||||
});
|
||||
|
||||
// 7. 更新知识库统计
|
||||
await updateKnowledgeBaseStats(kbId);
|
||||
|
||||
// 8. 转换BigInt为Number
|
||||
return {
|
||||
...updatedDocument,
|
||||
fileSizeBytes: Number(updatedDocument.fileSizeBytes),
|
||||
};
|
||||
} catch (error) {
|
||||
// 上传失败,更新状态为error
|
||||
await prisma.document.update({
|
||||
where: { id: document.id },
|
||||
data: {
|
||||
status: 'error',
|
||||
errorMessage: error instanceof Error ? error.message : 'Upload failed',
|
||||
},
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询文档处理状态
|
||||
*/
|
||||
async function pollDocumentStatus(
|
||||
userId: string,
|
||||
kbId: string,
|
||||
documentId: string,
|
||||
difyDocumentId: string,
|
||||
maxAttempts: number = 30
|
||||
) {
|
||||
const knowledgeBase = await prisma.knowledgeBase.findFirst({
|
||||
where: { id: kbId, userId },
|
||||
});
|
||||
|
||||
if (!knowledgeBase) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒
|
||||
|
||||
try {
|
||||
// 查询Dify中的文档状态
|
||||
const difyDocument = await difyClient.getDocument(
|
||||
knowledgeBase.difyDatasetId,
|
||||
difyDocumentId
|
||||
);
|
||||
|
||||
// 更新数据库中的状态
|
||||
await prisma.document.update({
|
||||
where: { id: documentId },
|
||||
data: {
|
||||
status: difyDocument.indexing_status,
|
||||
progress: difyDocument.indexing_status === 'completed' ? 100 : 50 + (i * 2),
|
||||
segmentsCount: difyDocument.indexing_status === 'completed' ? difyDocument.word_count : null,
|
||||
tokensCount: difyDocument.indexing_status === 'completed' ? difyDocument.tokens : null,
|
||||
processedAt: difyDocument.indexing_status === 'completed' ? new Date() : null,
|
||||
errorMessage: difyDocument.error || null,
|
||||
},
|
||||
});
|
||||
|
||||
// 如果完成或失败,退出轮询
|
||||
if (difyDocument.indexing_status === 'completed') {
|
||||
await updateKnowledgeBaseStats(kbId);
|
||||
break;
|
||||
}
|
||||
|
||||
if (difyDocument.indexing_status === 'error') {
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Polling attempt ${i + 1} failed:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档列表
|
||||
*/
|
||||
export async function getDocuments(userId: string, kbId: string) {
|
||||
// 1. 验证权限
|
||||
const knowledgeBase = await prisma.knowledgeBase.findFirst({
|
||||
where: {
|
||||
id: kbId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!knowledgeBase) {
|
||||
throw new Error('Knowledge base not found or access denied');
|
||||
}
|
||||
|
||||
// 2. 查询文档列表
|
||||
const documents = await prisma.document.findMany({
|
||||
where: { kbId },
|
||||
orderBy: { uploadedAt: 'desc' },
|
||||
});
|
||||
|
||||
// 3. 转换BigInt为Number
|
||||
return documents.map(doc => ({
|
||||
...doc,
|
||||
fileSizeBytes: Number(doc.fileSizeBytes),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档详情
|
||||
*/
|
||||
export async function getDocumentById(userId: string, documentId: string) {
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
userId, // 确保只能访问自己的文档
|
||||
},
|
||||
include: {
|
||||
knowledgeBase: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found or access denied');
|
||||
}
|
||||
|
||||
// 转换BigInt为Number
|
||||
return {
|
||||
...document,
|
||||
fileSizeBytes: Number(document.fileSizeBytes),
|
||||
knowledgeBase: {
|
||||
...document.knowledgeBase,
|
||||
totalSizeBytes: Number(document.knowledgeBase.totalSizeBytes),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
*/
|
||||
export async function deleteDocument(userId: string, documentId: string) {
|
||||
// 1. 查询文档信息
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
knowledgeBase: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found or access denied');
|
||||
}
|
||||
|
||||
// 2. 删除Dify中的文档
|
||||
if (document.difyDocumentId) {
|
||||
try {
|
||||
await difyClient.deleteDocument(
|
||||
document.knowledgeBase.difyDatasetId,
|
||||
document.difyDocumentId
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete Dify document:', error);
|
||||
// 继续删除本地记录
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 删除数据库记录
|
||||
await prisma.document.delete({
|
||||
where: { id: documentId },
|
||||
});
|
||||
|
||||
// 4. 更新知识库统计
|
||||
await updateKnowledgeBaseStats(document.kbId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新处理文档
|
||||
*/
|
||||
export async function reprocessDocument(userId: string, documentId: string) {
|
||||
// 1. 查询文档信息
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
knowledgeBase: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found or access denied');
|
||||
}
|
||||
|
||||
// 2. 触发Dify重新索引
|
||||
if (document.difyDocumentId) {
|
||||
try {
|
||||
await difyClient.updateDocument(
|
||||
document.knowledgeBase.difyDatasetId,
|
||||
document.difyDocumentId
|
||||
);
|
||||
|
||||
// 3. 更新状态为processing
|
||||
await prisma.document.update({
|
||||
where: { id: documentId },
|
||||
data: {
|
||||
status: 'parsing',
|
||||
progress: 0,
|
||||
errorMessage: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 4. 启动轮询
|
||||
pollDocumentStatus(
|
||||
userId,
|
||||
document.kbId,
|
||||
documentId,
|
||||
document.difyDocumentId
|
||||
).catch(error => {
|
||||
console.error('Failed to poll document status:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error('Failed to reprocess document');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识库统计信息
|
||||
*/
|
||||
async function updateKnowledgeBaseStats(kbId: string) {
|
||||
const documents = await prisma.document.findMany({
|
||||
where: { kbId },
|
||||
});
|
||||
|
||||
const totalSizeBytes = documents.reduce((sum, d) => sum + Number(d.fileSizeBytes), 0);
|
||||
const fileCount = documents.length;
|
||||
|
||||
await prisma.knowledgeBase.update({
|
||||
where: { id: kbId },
|
||||
data: {
|
||||
fileCount,
|
||||
totalSizeBytes: BigInt(totalSizeBytes),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
261
backend/src/services/knowledgeBaseService.ts
Normal file
261
backend/src/services/knowledgeBaseService.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { prisma } from '../config/database.js';
|
||||
import { difyClient } from '../clients/DifyClient.js';
|
||||
|
||||
/**
|
||||
* 知识库服务
|
||||
*/
|
||||
|
||||
/**
|
||||
* 创建知识库
|
||||
*/
|
||||
export async function createKnowledgeBase(
|
||||
userId: string,
|
||||
name: string,
|
||||
description?: string
|
||||
) {
|
||||
// 1. 检查用户知识库配额
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { kbQuota: true, kbUsed: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
if (user.kbUsed >= user.kbQuota) {
|
||||
throw new Error(`Knowledge base quota exceeded. Maximum: ${user.kbQuota}`);
|
||||
}
|
||||
|
||||
// 2. 在Dify中创建Dataset
|
||||
const difyDataset = await difyClient.createDataset({
|
||||
name: `${userId}_${name}_${Date.now()}`,
|
||||
description: description || `Knowledge base for user ${userId}`,
|
||||
indexing_technique: 'high_quality',
|
||||
});
|
||||
|
||||
// 3. 在数据库中创建记录
|
||||
const knowledgeBase = await prisma.knowledgeBase.create({
|
||||
data: {
|
||||
userId,
|
||||
name,
|
||||
description,
|
||||
difyDatasetId: difyDataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
// 4. 更新用户的知识库使用计数
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
kbUsed: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
// 5. 转换BigInt为Number
|
||||
return {
|
||||
...knowledgeBase,
|
||||
totalSizeBytes: Number(knowledgeBase.totalSizeBytes),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的知识库列表
|
||||
*/
|
||||
export async function getKnowledgeBases(userId: string) {
|
||||
const knowledgeBases = await prisma.knowledgeBase.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
_count: {
|
||||
select: { documents: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 转换BigInt为Number
|
||||
return knowledgeBases.map(kb => ({
|
||||
...kb,
|
||||
totalSizeBytes: Number(kb.totalSizeBytes),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库详情
|
||||
*/
|
||||
export async function getKnowledgeBaseById(userId: string, kbId: string) {
|
||||
const knowledgeBase = await prisma.knowledgeBase.findFirst({
|
||||
where: {
|
||||
id: kbId,
|
||||
userId, // 确保只能访问自己的知识库
|
||||
},
|
||||
include: {
|
||||
documents: {
|
||||
orderBy: { uploadedAt: 'desc' },
|
||||
},
|
||||
_count: {
|
||||
select: { documents: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!knowledgeBase) {
|
||||
throw new Error('Knowledge base not found or access denied');
|
||||
}
|
||||
|
||||
// 转换BigInt为Number
|
||||
const result = {
|
||||
...knowledgeBase,
|
||||
totalSizeBytes: Number(knowledgeBase.totalSizeBytes),
|
||||
documents: knowledgeBase.documents.map(doc => ({
|
||||
...doc,
|
||||
fileSizeBytes: Number(doc.fileSizeBytes),
|
||||
})),
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识库
|
||||
*/
|
||||
export async function updateKnowledgeBase(
|
||||
userId: string,
|
||||
kbId: string,
|
||||
data: { name?: string; description?: string }
|
||||
) {
|
||||
// 1. 验证权限
|
||||
const existingKb = await prisma.knowledgeBase.findFirst({
|
||||
where: {
|
||||
id: kbId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingKb) {
|
||||
throw new Error('Knowledge base not found or access denied');
|
||||
}
|
||||
|
||||
// 2. 更新数据库
|
||||
const knowledgeBase = await prisma.knowledgeBase.update({
|
||||
where: { id: kbId },
|
||||
data,
|
||||
});
|
||||
|
||||
// 3. 转换BigInt为Number
|
||||
return {
|
||||
...knowledgeBase,
|
||||
totalSizeBytes: Number(knowledgeBase.totalSizeBytes),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识库
|
||||
*/
|
||||
export async function deleteKnowledgeBase(userId: string, kbId: string) {
|
||||
// 1. 验证权限
|
||||
const knowledgeBase = await prisma.knowledgeBase.findFirst({
|
||||
where: {
|
||||
id: kbId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!knowledgeBase) {
|
||||
throw new Error('Knowledge base not found or access denied');
|
||||
}
|
||||
|
||||
// 2. 删除Dify中的Dataset
|
||||
try {
|
||||
await difyClient.deleteDataset(knowledgeBase.difyDatasetId);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete Dify dataset:', error);
|
||||
// 继续删除本地记录,即使Dify删除失败
|
||||
}
|
||||
|
||||
// 3. 删除数据库记录(会级联删除documents)
|
||||
await prisma.knowledgeBase.delete({
|
||||
where: { id: kbId },
|
||||
});
|
||||
|
||||
// 4. 更新用户的知识库使用计数
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
kbUsed: { decrement: 1 },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检索知识库
|
||||
*/
|
||||
export async function searchKnowledgeBase(
|
||||
userId: string,
|
||||
kbId: string,
|
||||
query: string,
|
||||
topK: number = 3
|
||||
) {
|
||||
// 1. 验证权限
|
||||
const knowledgeBase = await prisma.knowledgeBase.findFirst({
|
||||
where: {
|
||||
id: kbId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!knowledgeBase) {
|
||||
throw new Error('Knowledge base not found or access denied');
|
||||
}
|
||||
|
||||
// 2. 调用Dify检索API
|
||||
const results = await difyClient.retrieveKnowledge(
|
||||
knowledgeBase.difyDatasetId,
|
||||
query,
|
||||
{
|
||||
retrieval_model: {
|
||||
search_method: 'semantic_search',
|
||||
top_k: topK,
|
||||
score_threshold_enabled: true,
|
||||
score_threshold: 0.3,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库统计信息
|
||||
*/
|
||||
export async function getKnowledgeBaseStats(userId: string, kbId: string) {
|
||||
// 1. 验证权限
|
||||
const knowledgeBase = await prisma.knowledgeBase.findFirst({
|
||||
where: {
|
||||
id: kbId,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
documents: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!knowledgeBase) {
|
||||
throw new Error('Knowledge base not found or access denied');
|
||||
}
|
||||
|
||||
// 2. 统计信息
|
||||
const stats = {
|
||||
totalDocuments: knowledgeBase.documents.length,
|
||||
completedDocuments: knowledgeBase.documents.filter(d => d.status === 'completed').length,
|
||||
processingDocuments: knowledgeBase.documents.filter(d =>
|
||||
['uploading', 'parsing', 'indexing'].includes(d.status)
|
||||
).length,
|
||||
errorDocuments: knowledgeBase.documents.filter(d => d.status === 'error').length,
|
||||
totalSizeBytes: knowledgeBase.totalSizeBytes,
|
||||
totalTokens: knowledgeBase.documents.reduce((sum, d) => sum + (d.tokensCount || 0), 0),
|
||||
};
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
111
backend/test-kb-api.ps1
Normal file
111
backend/test-kb-api.ps1
Normal file
@@ -0,0 +1,111 @@
|
||||
# 测试知识库API脚本
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " 测试知识库API" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 测试1: 创建知识库
|
||||
Write-Host "测试1: 创建知识库..." -ForegroundColor Yellow
|
||||
$createBody = @{
|
||||
name = "Test Knowledge Base"
|
||||
description = "This is a test knowledge base"
|
||||
} | ConvertTo-Json
|
||||
|
||||
try {
|
||||
$result1 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases" `
|
||||
-Method Post `
|
||||
-Body $createBody `
|
||||
-ContentType "application/json"
|
||||
|
||||
Write-Host "✓ 创建成功" -ForegroundColor Green
|
||||
Write-Host " 知识库ID: $($result1.data.id)" -ForegroundColor Gray
|
||||
Write-Host " 名称: $($result1.data.name)" -ForegroundColor Gray
|
||||
$kbId = $result1.data.id
|
||||
} catch {
|
||||
Write-Host "✗ 创建失败: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# 测试2: 获取知识库列表
|
||||
Write-Host "测试2: 获取知识库列表..." -ForegroundColor Yellow
|
||||
try {
|
||||
$result2 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases" `
|
||||
-Method Get
|
||||
|
||||
Write-Host "✓ 获取成功" -ForegroundColor Green
|
||||
Write-Host " 知识库数量: $($result2.data.Count)" -ForegroundColor Gray
|
||||
} catch {
|
||||
Write-Host "✗ 获取失败: $_" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# 测试3: 获取知识库详情
|
||||
Write-Host "测试3: 获取知识库详情..." -ForegroundColor Yellow
|
||||
try {
|
||||
$result3 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases/$kbId" `
|
||||
-Method Get
|
||||
|
||||
Write-Host "✓ 获取成功" -ForegroundColor Green
|
||||
Write-Host " 名称: $($result3.data.name)" -ForegroundColor Gray
|
||||
Write-Host " 文档数: $($result3.data._count.documents)" -ForegroundColor Gray
|
||||
} catch {
|
||||
Write-Host "✗ 获取失败: $_" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# 测试4: 更新知识库
|
||||
Write-Host "测试4: 更新知识库..." -ForegroundColor Yellow
|
||||
$updateBody = @{
|
||||
name = "Updated Test KB"
|
||||
description = "Updated description"
|
||||
} | ConvertTo-Json
|
||||
|
||||
try {
|
||||
$result4 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases/$kbId" `
|
||||
-Method Put `
|
||||
-Body $updateBody `
|
||||
-ContentType "application/json"
|
||||
|
||||
Write-Host "✓ 更新成功" -ForegroundColor Green
|
||||
Write-Host " 新名称: $($result4.data.name)" -ForegroundColor Gray
|
||||
} catch {
|
||||
Write-Host "✗ 更新失败: $_" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# 测试5: 检索知识库
|
||||
Write-Host "测试5: 检索知识库..." -ForegroundColor Yellow
|
||||
try {
|
||||
$result5 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases/$kbId/search?query=test" `
|
||||
-Method Get
|
||||
|
||||
Write-Host "✓ 检索成功" -ForegroundColor Green
|
||||
Write-Host " 结果数: $($result5.data.records.Count)" -ForegroundColor Gray
|
||||
} catch {
|
||||
Write-Host "✗ 检索失败: $_" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# 测试6: 删除知识库
|
||||
Write-Host "测试6: 删除知识库..." -ForegroundColor Yellow
|
||||
try {
|
||||
$result6 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases/$kbId" `
|
||||
-Method Delete
|
||||
|
||||
Write-Host "✓ 删除成功" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "✗ 删除失败: $_" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " 测试完成!" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
|
||||
Reference in New Issue
Block a user