forked from hangshuo652/aurak
Initial commit: AuraK人才测评系统基础框架
## 已实现功能 - 题库管理后端API完整实现 - 模板管理页面(Settings-测评模板) - 评估统计页面 - 人才测评页面(AssessmentView) - QuestionBank前端服务层 ## 技术栈 - 后端: Node.js + NestJS + TypeORM - 前端: React + TypeScript - 容器化: Docker Compose ## 已知待完善 - 题库列表页缺少删除按钮 - 题库详情页未实现(题目管理/AI生成/审核)
This commit is contained in:
@@ -46,6 +46,8 @@ import { AssessmentSession } from './assessment/entities/assessment-session.enti
|
||||
import { AssessmentQuestion } from './assessment/entities/assessment-question.entity';
|
||||
import { AssessmentAnswer } from './assessment/entities/assessment-answer.entity';
|
||||
import { AssessmentTemplate } from './assessment/entities/assessment-template.entity';
|
||||
import { QuestionBank } from './assessment/entities/question-bank.entity';
|
||||
import { QuestionBankItem } from './assessment/entities/question-bank-item.entity';
|
||||
import { Tenant } from './tenant/tenant.entity';
|
||||
import { TenantSetting } from './tenant/tenant-setting.entity';
|
||||
import { ApiKey } from './auth/entities/api-key.entity';
|
||||
@@ -90,6 +92,8 @@ import { FeishuAssessmentSession } from './feishu/entities/feishu-assessment-ses
|
||||
AssessmentQuestion,
|
||||
AssessmentAnswer,
|
||||
AssessmentTemplate,
|
||||
QuestionBank,
|
||||
QuestionBankItem,
|
||||
Tenant,
|
||||
TenantSetting,
|
||||
TenantMember,
|
||||
|
||||
@@ -1422,7 +1422,7 @@ const initialState: Partial<EvaluationState> = {
|
||||
const recentRecords = sessions.slice(0, 20).map(session => ({
|
||||
id: session.id,
|
||||
userId: session.userId,
|
||||
knowledgeBase: session.knowledgeBase?.name || session.knowledgeGroup?.name || '-',
|
||||
knowledgeBase: session.knowledgeBase?.title || session.knowledgeBase?.originalName || session.knowledgeGroup?.name || '-',
|
||||
template: session.template?.name || '-',
|
||||
score: session.finalScore || null,
|
||||
status: session.status,
|
||||
|
||||
@@ -88,6 +88,12 @@ export class QuestionBankController {
|
||||
return this.questionBankService.publish(id);
|
||||
}
|
||||
|
||||
@Get(':bankId/items')
|
||||
async getItems(@Param('bankId') bankId: string) {
|
||||
const bank = await this.questionBankService.findOne(bankId);
|
||||
return bank.items || [];
|
||||
}
|
||||
|
||||
@Post(':bankId/items')
|
||||
async addItem(
|
||||
@Param('bankId') bankId: string,
|
||||
|
||||
@@ -54,9 +54,8 @@ export class QuestionBankItem {
|
||||
questionText: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
type: 'simple-enum',
|
||||
enum: QuestionType,
|
||||
default: QuestionType.SHORT_ANSWER,
|
||||
})
|
||||
questionType: QuestionType;
|
||||
|
||||
@@ -70,29 +69,26 @@ export class QuestionBankItem {
|
||||
keyPoints: string[];
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
type: 'simple-enum',
|
||||
enum: QuestionDifficulty,
|
||||
default: QuestionDifficulty.STANDARD,
|
||||
})
|
||||
difficulty: QuestionDifficulty;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
type: 'simple-enum',
|
||||
enum: QuestionDimension,
|
||||
default: QuestionDimension.PROMPT,
|
||||
})
|
||||
dimension: QuestionDimension;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
basis: string | null;
|
||||
|
||||
@Column({ name: 'created_by', nullable: true })
|
||||
@Column({ name: 'created_by', nullable: true, type: 'text' })
|
||||
createdBy: string | null;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
type: 'simple-enum',
|
||||
enum: QuestionBankItemStatus,
|
||||
default: QuestionBankItemStatus.PENDING_REVIEW,
|
||||
})
|
||||
status: QuestionBankItemStatus;
|
||||
|
||||
|
||||
@@ -48,22 +48,21 @@ export class QuestionBank {
|
||||
description: string | null;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
type: 'simple-enum',
|
||||
enum: QuestionBankStatus,
|
||||
default: QuestionBankStatus.DRAFT,
|
||||
})
|
||||
status: QuestionBankStatus;
|
||||
|
||||
@Column({ name: 'created_by', nullable: true })
|
||||
@Column({ name: 'created_by', nullable: true, type: 'text' })
|
||||
createdBy: string | null;
|
||||
|
||||
@Column({ name: 'reviewed_by', nullable: true })
|
||||
@Column({ name: 'reviewed_by', nullable: true, type: 'text' })
|
||||
reviewedBy: string | null;
|
||||
|
||||
@Column({ name: 'reviewed_at', nullable: true })
|
||||
@Column({ name: 'reviewed_at', nullable: true, type: 'datetime' })
|
||||
reviewedAt: Date | null;
|
||||
|
||||
@Column({ name: 'review_comment', nullable: true })
|
||||
@Column({ name: 'review_comment', nullable: true, type: 'text' })
|
||||
reviewComment: string | null;
|
||||
|
||||
@OneToMany(
|
||||
|
||||
@@ -12,12 +12,13 @@ import { SystemMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { QuestionBank, QuestionBankStatus } from '../entities/question-bank.entity';
|
||||
import {
|
||||
QuestionBankItem,
|
||||
QuestionBankItemStatus,
|
||||
QuestionType,
|
||||
QuestionDifficulty,
|
||||
QuestionDimension,
|
||||
} from '../entities/question-bank-item.entity';
|
||||
import { ModelConfigService } from '../model-config/model-config.service';
|
||||
import { ModelType } from '../types';
|
||||
import { ModelConfigService } from '../../model-config/model-config.service';
|
||||
import { ModelType } from '../../types';
|
||||
import { safeParseJson } from '../../common/json-utils';
|
||||
|
||||
export interface CreateQuestionBankDto {
|
||||
@@ -83,12 +84,15 @@ export class QuestionBankService {
|
||||
async create(
|
||||
createDto: CreateQuestionBankDto,
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
tenantId: string | null,
|
||||
): Promise<QuestionBank> {
|
||||
if (!createDto.name || !createDto.name.trim()) {
|
||||
throw new Error('Question bank name is required');
|
||||
}
|
||||
const bank = this.bankRepository.create({
|
||||
...createDto,
|
||||
createdBy: userId,
|
||||
tenantId,
|
||||
tenantId: tenantId || null,
|
||||
status: QuestionBankStatus.DRAFT,
|
||||
});
|
||||
return this.bankRepository.save(bank);
|
||||
@@ -96,15 +100,22 @@ export class QuestionBankService {
|
||||
|
||||
async findAll(
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
tenantId: string | null,
|
||||
page?: number,
|
||||
limit?: number,
|
||||
): Promise<{ data: QuestionBank[]; total: number } | QuestionBank[]> {
|
||||
console.log('[QuestionBank findAll] userId:', userId, 'tenantId:', tenantId);
|
||||
const queryBuilder = this.bankRepository
|
||||
.createQueryBuilder('bank')
|
||||
.leftJoinAndSelect('bank.template', 'template')
|
||||
.where('bank.tenantId = :tenantId', { tenantId })
|
||||
.orderBy('bank.createdAt', 'DESC');
|
||||
.leftJoinAndSelect('bank.template', 'template');
|
||||
|
||||
if (tenantId === null) {
|
||||
queryBuilder.where('bank.tenantId IS NULL');
|
||||
} else {
|
||||
queryBuilder.where('bank.tenantId = :tenantId', { tenantId });
|
||||
}
|
||||
|
||||
queryBuilder.orderBy('bank.createdAt', 'DESC');
|
||||
|
||||
if (page !== undefined && limit !== undefined) {
|
||||
const [data, total] = await queryBuilder
|
||||
@@ -194,6 +205,9 @@ export class QuestionBankService {
|
||||
bankId: string,
|
||||
createDto: CreateQuestionBankItemDto,
|
||||
): Promise<QuestionBankItem> {
|
||||
if (!createDto.questionText || !createDto.questionText.trim()) {
|
||||
throw new Error('Question text is required');
|
||||
}
|
||||
await this.findOne(bankId);
|
||||
const item = this.itemRepository.create({
|
||||
...createDto,
|
||||
@@ -201,6 +215,7 @@ export class QuestionBankService {
|
||||
questionType: createDto.questionType || QuestionType.SHORT_ANSWER,
|
||||
difficulty: createDto.difficulty || QuestionDifficulty.STANDARD,
|
||||
dimension: createDto.dimension || QuestionDimension.PROMPT,
|
||||
status: QuestionBankItemStatus.PENDING_REVIEW,
|
||||
});
|
||||
return this.itemRepository.save(item);
|
||||
}
|
||||
@@ -321,6 +336,7 @@ export class QuestionBankService {
|
||||
difficulty: difficulty as QuestionDifficulty,
|
||||
dimension: dimension as QuestionDimension,
|
||||
basis: q.basis,
|
||||
status: QuestionBankItemStatus.PENDING_REVIEW,
|
||||
});
|
||||
items.push(await this.itemRepository.save(item));
|
||||
}
|
||||
|
||||
@@ -128,14 +128,23 @@ export class EmbeddingService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process single batch embedding
|
||||
*/
|
||||
/**
|
||||
* Process single batch embedding
|
||||
*/
|
||||
private async getEmbeddingsForBatch(
|
||||
texts: string[],
|
||||
modelConfig: any,
|
||||
maxBatchSize: number,
|
||||
): Promise<number[][]> {
|
||||
// Detect Ollama by port 11434 or /api/embeddings path
|
||||
const isOllama =
|
||||
modelConfig.baseUrl.includes(':11434') ||
|
||||
modelConfig.baseUrl.includes('/api/embeddings');
|
||||
|
||||
if (isOllama) {
|
||||
return await this.getOllamaEmbeddings(texts, modelConfig);
|
||||
}
|
||||
|
||||
const apiUrl = modelConfig.baseUrl.endsWith('/embeddings')
|
||||
? modelConfig.baseUrl
|
||||
: `${modelConfig.baseUrl}/embeddings`;
|
||||
@@ -283,4 +292,59 @@ export class EmbeddingService {
|
||||
// Use default dimensions from environment variable
|
||||
return this.defaultDimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get embeddings from local Ollama
|
||||
*/
|
||||
private async getOllamaEmbeddings(
|
||||
texts: string[],
|
||||
modelConfig: any,
|
||||
): Promise<number[][]> {
|
||||
const baseUrl = modelConfig.baseUrl || 'http://localhost:11434';
|
||||
const modelName = modelConfig.modelId || 'nomic-embed-text';
|
||||
|
||||
this.logger.log(
|
||||
`[Ollama] Generating embeddings for ${texts.length} texts using ${modelName}`,
|
||||
);
|
||||
|
||||
const embeddings: number[][] = [];
|
||||
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
try {
|
||||
const url = baseUrl.endsWith('/api/embeddings')
|
||||
? baseUrl
|
||||
: `${baseUrl}/api/embeddings`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: modelName,
|
||||
prompt: texts[i],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Ollama API error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
embeddings.push(data.embedding);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Ollama embedding error for text ${i}: ${error.message}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[Ollama] Got ${embeddings.length} embeddings, dimensions: ${embeddings[0]?.length || 0}`,
|
||||
);
|
||||
|
||||
return embeddings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export class KnowledgeGroupController {
|
||||
@Post()
|
||||
@Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
|
||||
async create(@Body() createGroupDto: CreateGroupDto, @Request() req) {
|
||||
console.log('[KnowledgeGroup] create called, user:', req.user);
|
||||
return await this.groupService.create(
|
||||
req.user.id,
|
||||
req.user.tenantId,
|
||||
|
||||
@@ -62,11 +62,19 @@ export class KnowledgeGroupService {
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
): Promise<GroupWithFileCount[]> {
|
||||
console.log('[KnowledgeGroup findAll] userId:', userId, 'tenantId:', tenantId);
|
||||
// Return all groups for the tenant with file counts
|
||||
const groups = await this.groupRepository
|
||||
const queryBuilder = this.groupRepository
|
||||
.createQueryBuilder('group')
|
||||
.leftJoin('group.knowledgeBases', 'kb')
|
||||
.where('group.tenantId = :tenantId', { tenantId })
|
||||
.leftJoin('group.knowledgeBases', 'kb');
|
||||
|
||||
if (tenantId === null) {
|
||||
queryBuilder.where('group.tenantId IS NULL');
|
||||
} else {
|
||||
queryBuilder.where('group.tenantId = :tenantId', { tenantId });
|
||||
}
|
||||
|
||||
const groups = await queryBuilder
|
||||
.addSelect('COUNT(kb.id)', 'fileCount')
|
||||
.groupBy('group.id')
|
||||
.orderBy('group.createdAt', 'ASC')
|
||||
@@ -139,13 +147,16 @@ export class KnowledgeGroupService {
|
||||
tenantId: string,
|
||||
createGroupDto: CreateGroupDto,
|
||||
): Promise<KnowledgeGroup> {
|
||||
console.log('[KnowledgeGroup create] userId:', userId, 'tenantId:', tenantId);
|
||||
const group = this.groupRepository.create({
|
||||
...createGroupDto,
|
||||
parentId: createGroupDto.parentId ?? null,
|
||||
tenantId,
|
||||
});
|
||||
|
||||
return await this.groupRepository.save(group);
|
||||
const saved = await this.groupRepository.save(group);
|
||||
console.log('[KnowledgeGroup create] saved group tenantId:', saved.tenantId);
|
||||
return saved;
|
||||
}
|
||||
|
||||
async update(
|
||||
|
||||
Reference in New Issue
Block a user