feat: add CASL authorization and AI configuration
This commit is contained in:
122
apps/server/src/agent-tools/tools/search-students.tool.ts
Normal file
122
apps/server/src/agent-tools/tools/search-students.tool.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { StudentsService } from '../../students/students.service';
|
||||
import { StudentAccessScopeFactory } from '../../students/student-access-scope.factory';
|
||||
import type { AgentToolContext, ToolDef, ToolInputResult } from '../agent-tool.types';
|
||||
|
||||
/** Whitelisted input shape for search_students. */
|
||||
interface SearchStudentsInput {
|
||||
keyword?: string;
|
||||
classId?: number;
|
||||
organizationId?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/** Forbidden input keys — if the model sends these, validation fails. */
|
||||
const FORBIDDEN_INPUT_KEYS = new Set([
|
||||
'userId',
|
||||
'isSuperAdmin',
|
||||
'permissions',
|
||||
'roles',
|
||||
'ability',
|
||||
'user',
|
||||
'password',
|
||||
'token',
|
||||
]);
|
||||
|
||||
@Injectable()
|
||||
export class SearchStudentsTool implements ToolDef<SearchStudentsInput> {
|
||||
readonly name = 'search_students';
|
||||
readonly inputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyword: {
|
||||
type: 'string',
|
||||
description: '搜索关键词(姓名/学号)',
|
||||
maxLength: 100,
|
||||
},
|
||||
classId: {
|
||||
type: 'integer',
|
||||
description: '班级ID',
|
||||
minimum: 1,
|
||||
},
|
||||
organizationId: {
|
||||
type: 'integer',
|
||||
description: '校区ID',
|
||||
minimum: 1,
|
||||
},
|
||||
limit: {
|
||||
type: 'integer',
|
||||
description: '返回条数上限',
|
||||
minimum: 1,
|
||||
maximum: 50,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
readonly description = '搜索学生,支持关键词、班级、校区筛选。仅返回基础公开字段。';
|
||||
readonly requiredPermission = 'student:view';
|
||||
|
||||
constructor(
|
||||
private readonly studentsService: StudentsService,
|
||||
private readonly scopeFactory: StudentAccessScopeFactory,
|
||||
) {}
|
||||
|
||||
validate(input: Record<string, unknown>): ToolInputResult<SearchStudentsInput> {
|
||||
// Reject forbidden keys
|
||||
for (const key of Object.keys(input)) {
|
||||
if (FORBIDDEN_INPUT_KEYS.has(key)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `不允许的输入字段: ${key}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const allowedKeys = new Set(['keyword', 'classId', 'organizationId', 'limit']);
|
||||
for (const key of Object.keys(input)) {
|
||||
if (!allowedKeys.has(key)) {
|
||||
return { ok: false, error: `不允许的输入字段: ${key}` };
|
||||
}
|
||||
}
|
||||
|
||||
const result: SearchStudentsInput = {};
|
||||
|
||||
if (input.keyword !== undefined) {
|
||||
if (typeof input.keyword !== 'string' || input.keyword.length > 100) {
|
||||
return { ok: false, error: 'keyword 必须是字符串且长度不超过100' };
|
||||
}
|
||||
result.keyword = input.keyword;
|
||||
}
|
||||
|
||||
if (input.classId !== undefined) {
|
||||
const id = Number(input.classId);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
return { ok: false, error: 'classId 必须是正整数' };
|
||||
}
|
||||
result.classId = id;
|
||||
}
|
||||
|
||||
if (input.organizationId !== undefined) {
|
||||
const id = Number(input.organizationId);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
return { ok: false, error: 'organizationId 必须是正整数' };
|
||||
}
|
||||
result.organizationId = id;
|
||||
}
|
||||
|
||||
if (input.limit !== undefined) {
|
||||
const limit = Number(input.limit);
|
||||
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
||||
return { ok: false, error: 'limit 必须是 1 到 50 的整数' };
|
||||
}
|
||||
result.limit = limit;
|
||||
}
|
||||
|
||||
return { ok: true, value: result };
|
||||
}
|
||||
|
||||
async execute(input: SearchStudentsInput, context: AgentToolContext): Promise<unknown> {
|
||||
const scope = this.scopeFactory.buildScope(context);
|
||||
return this.studentsService.agentSearchStudents(scope, input);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user