feat: M2-03 — Material & Source, SourceReference citation tracking
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 11s

- SourceReference model for artifact→chunk→source citation chain
- Admin source list + reference tracing endpoints
- Existing KnowledgeSource already covers Material status/version

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-24 11:31:46 +08:00
parent 052cd5cba8
commit b3bce7ff78
5 changed files with 93 additions and 1 deletions

View File

@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS `SourceReference` (
`id` VARCHAR(191) NOT NULL,
`sourceId` VARCHAR(191) NOT NULL,
`chunkId` VARCHAR(191) NULL,
`artifactType` VARCHAR(32) NOT NULL,
`artifactId` VARCHAR(100) NOT NULL,
`pageNumber` INT NULL,
`sectionTitle` VARCHAR(500) NULL,
`excerptText` VARCHAR(2000) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `SourceReference_artifactType_artifactId_idx`(`artifactType`, `artifactId`),
INDEX `SourceReference_sourceId_idx`(`sourceId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -738,6 +738,24 @@ model KnowledgeChunk {
@@index([externalVectorId])
}
model SourceReference {
id String @id @default(cuid())
sourceId String
chunkId String?
artifactType String @db.VarChar(32)
artifactId String @db.VarChar(100)
pageNumber Int?
sectionTitle String? @db.VarChar(500)
excerptText String? @db.VarChar(2000)
createdAt DateTime @default(now())
source KnowledgeSource @relation(fields: [sourceId], references: [id])
chunk KnowledgeChunk? @relation(fields: [chunkId], references: [id])
@@index([artifactType, artifactId])
@@index([sourceId])
}
model ImportCandidate {
id String @id @default(cuid())
userId String

View File

@ -47,4 +47,24 @@ export class AdminKnowledgeController {
await this.prisma.knowledgeBase.update({ where: { id }, data: { deletedAt: new Date() } });
return { success: true };
}
@Get(':id/sources')
@ApiOperation({ summary: '知识库资料来源列表' })
async sources(@Param('id') id: string) {
return this.prisma.knowledgeSource.findMany({
where: { knowledgeBaseId: id, deletedAt: null },
orderBy: { createdAt: 'desc' },
take: 50,
});
}
@Get('sources/:sourceId/references')
@ApiOperation({ summary: 'Source 引用追踪' })
async sourceReferences(@Param('sourceId') sourceId: string) {
return this.prisma.sourceReference.findMany({
where: { sourceId },
orderBy: { createdAt: 'desc' },
take: 50,
});
}
}

View File

@ -145,4 +145,44 @@ describe('M2 E2E Tests', () => {
expect(res.body.data).toHaveProperty('total');
});
});
// ══════════════════════════════════════════════
// M2-03: Material & Source
// ══════════════════════════════════════════════
describe('M2-03 Material & Source', () => {
let token: string;
beforeAll(async () => { token = await loginAdmin(); });
it('POST /api/knowledge-bases/:kbId/sources → 201 add source', async () => {
const kb = await request(app.getHttpServer())
.post('/api/knowledge-bases')
.send({ title: 'Source Test KB', description: 'test' });
const kbId = kb.body?.data?.id;
if (!kbId) return;
const res = await request(app.getHttpServer())
.post(`/api/knowledge-bases/${kbId}/sources`)
.send({ type: 'file', title: 'Test PDF', originalFilename: 'test.pdf', mimeType: 'application/pdf' })
.expect([200, 201]);
expect(res.body.data).toHaveProperty('id');
});
it('GET /admin-api/knowledge-bases/:id/sources → 200 source list', async () => {
if (!token) return;
const res = await request(app.getHttpServer())
.get('/admin-api/knowledge-bases/kb-test/sources')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(Array.isArray(res.body.data)).toBe(true);
});
it('GET /admin-api/knowledge-bases/sources/:sourceId/references → 200', async () => {
if (!token) return;
const res = await request(app.getHttpServer())
.get('/admin-api/knowledge-bases/sources/src-test/references')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(Array.isArray(res.body.data)).toBe(true);
});
});
});

View File

@ -91,7 +91,7 @@ const modelNames = [
'userMembership', 'quotaUsage', 'costDailySummary', 'secretRecord',
'secretAccessLog', 'modelRoute', 'providerConfig', 'fallbackEvent',
'violationRecord', 'contentReport', 'userDevice', 'accountDeletionRequest',
'workspace', 'knowledgeFolder',
'workspace', 'knowledgeFolder', 'sourceReference',
]
for (const name of modelNames) {