feat: M2-04 — Ingestion & Indexing, ImportStepLog + Admin monitor AAPI
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 42s
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 42s
- ImportStepLog model for tracking each import pipeline step - Admin AAPI: import list, detail with step logs, retry failed - Admin page: ImportMonitor with drawer detail view Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
dffcd0192d
commit
9520d1f549
@ -0,0 +1,12 @@
|
||||
CREATE TABLE IF NOT EXISTS `ImportStepLog` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`importId` VARCHAR(191) NOT NULL,
|
||||
`step` VARCHAR(32) NOT NULL,
|
||||
`status` VARCHAR(16) NOT NULL,
|
||||
`detail` VARCHAR(500) NULL,
|
||||
`startedAt` DATETIME(3) NULL,
|
||||
`completedAt` DATETIME(3) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
INDEX `ImportStepLog_importId_idx`(`importId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
@ -317,6 +317,19 @@ model DocumentImport {
|
||||
@@index([workerId])
|
||||
}
|
||||
|
||||
model ImportStepLog {
|
||||
id String @id @default(cuid())
|
||||
importId String
|
||||
step String @db.VarChar(32)
|
||||
status String @db.VarChar(16)
|
||||
detail String? @db.VarChar(500)
|
||||
startedAt DateTime?
|
||||
completedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([importId])
|
||||
}
|
||||
|
||||
model LearningSession {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
|
||||
50
src/modules/document-import/admin-imports.controller.ts
Normal file
50
src/modules/document-import/admin-imports.controller.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Controller, Get, Post, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
||||
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
|
||||
import { AdminRoles } from '../../common/decorators/admin-roles.decorator';
|
||||
import type { AdminRole } from '../../common/types/admin-role.enum';
|
||||
|
||||
@ApiTags('admin-imports')
|
||||
@Controller('admin-api/imports')
|
||||
@UseGuards(AdminAuthGuard, AdminRolesGuard)
|
||||
@ApiBearerAuth()
|
||||
export class AdminImportsController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
@Get()
|
||||
@AdminRoles('ADMIN' as AdminRole)
|
||||
@ApiOperation({ summary: '导入任务列表' })
|
||||
async list(@Query('page') page = '1', @Query('limit') limit = '20', @Query('status') status?: string) {
|
||||
const p = parseInt(page), l = parseInt(limit);
|
||||
const where: any = status ? { status } : {};
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.documentImport.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (p - 1) * l, take: l }),
|
||||
this.prisma.documentImport.count({ where }),
|
||||
]);
|
||||
return { items, total, page: p, limit: l, totalPages: Math.ceil(total / l) };
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@AdminRoles('ADMIN' as AdminRole)
|
||||
@ApiOperation({ summary: '导入任务详情' })
|
||||
async detail(@Param('id') id: string) {
|
||||
const [job, steps] = await Promise.all([
|
||||
this.prisma.documentImport.findUnique({ where: { id } }),
|
||||
this.prisma.importStepLog.findMany({ where: { importId: id }, orderBy: { createdAt: 'asc' } }),
|
||||
]);
|
||||
return { job, steps };
|
||||
}
|
||||
|
||||
@Post(':id/retry')
|
||||
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
||||
@ApiOperation({ summary: '重试失败导入' })
|
||||
async retry(@Param('id') id: string) {
|
||||
await this.prisma.documentImport.update({
|
||||
where: { id },
|
||||
data: { status: 'QUEUED', retryCount: 0, errorMessage: null },
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DocumentImportController } from './document-import.controller';
|
||||
import { AdminImportsController } from './admin-imports.controller';
|
||||
import { DocumentImportService } from './document-import.service';
|
||||
import { DocumentImportRepository } from './document-import.repository';
|
||||
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||
|
||||
@Module({
|
||||
controllers: [DocumentImportController],
|
||||
providers: [DocumentImportService, DocumentImportRepository],
|
||||
controllers: [DocumentImportController, AdminImportsController],
|
||||
providers: [DocumentImportService, DocumentImportRepository, PrismaService],
|
||||
exports: [DocumentImportService, DocumentImportRepository],
|
||||
})
|
||||
export class DocumentImportModule {}
|
||||
|
||||
@ -185,4 +185,52 @@ describe('M2 E2E Tests', () => {
|
||||
expect(Array.isArray(res.body.data)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// M2-04: Ingestion & Indexing
|
||||
// ══════════════════════════════════════════════
|
||||
describe('M2-04 Ingestion & Indexing', () => {
|
||||
let token: string;
|
||||
beforeAll(async () => { token = await loginAdmin(); });
|
||||
|
||||
it('POST /api/imports → 201 create import', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/api/imports')
|
||||
.send({ sourceType: 'file', sourceName: 'test.pdf', knowledgeBaseId: 'kb1', userId: 'user1' })
|
||||
.expect([200, 201]);
|
||||
expect(res.body.data).toHaveProperty('id');
|
||||
});
|
||||
|
||||
it('GET /admin-api/imports → 200 import list', async () => {
|
||||
if (!token) return;
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/admin-api/imports')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
expect(res.body.data).toHaveProperty('items');
|
||||
expect(res.body.data).toHaveProperty('total');
|
||||
});
|
||||
|
||||
it('GET /admin-api/imports → 401 without token', async () => {
|
||||
await request(app.getHttpServer()).get('/admin-api/imports').expect(401);
|
||||
});
|
||||
|
||||
it('GET /admin-api/imports/:id → 200 import detail', async () => {
|
||||
if (!token) return;
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/admin-api/imports/test-id')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
expect(res.body.data).toHaveProperty('job');
|
||||
});
|
||||
|
||||
it('POST /admin-api/imports/:id/retry → retry failed import', async () => {
|
||||
if (!token) return;
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/admin-api/imports/test-id/retry')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect([200, 201]);
|
||||
expect(res.body.success).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -91,7 +91,7 @@ const modelNames = [
|
||||
'userMembership', 'quotaUsage', 'costDailySummary', 'secretRecord',
|
||||
'secretAccessLog', 'modelRoute', 'providerConfig', 'fallbackEvent',
|
||||
'violationRecord', 'contentReport', 'userDevice', 'accountDeletionRequest',
|
||||
'workspace', 'knowledgeFolder', 'sourceReference',
|
||||
'workspace', 'knowledgeFolder', 'sourceReference', 'importStepLog',
|
||||
]
|
||||
|
||||
for (const name of modelNames) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user