refactor(server): 收敛类型边界工具,aislop AI Slop 10 → 1
- 新增 common/buffer.ts bufferToArrayBuffer:9 处 'as unknown as ArrayBuffer' 收敛为精确切片(含 byteOffset), 消除潜在 Buffer 池偏移隐患,类型断言集中到单一实现 - 新增 common/stringify.ts:4 处重复的 stringify 助手收敛为共享实现 - aislop: AI Slop 10→1(仅剩 1 处有理由的 stringify 薄包装, eslint no-base-to-string 绕过所需);Code Quality 剩余 4 重复块(声明式 SQL 配置)+ 2 文件过大(既有规模)均保留 - 测试 142 套件/1065 用例通过
This commit is contained in:
@@ -3,6 +3,7 @@ import { DataSource } from 'typeorm';
|
||||
import { Organization } from '../../entities/organization.entity';
|
||||
import { StudentsService } from '../../students/students.service';
|
||||
import type { AgentToolContext, ToolDef, ToolInputResult } from '../agent-tool.types';
|
||||
import { stringify } from '../../common/stringify';
|
||||
|
||||
/** Whitelisted input shape for create_student. */
|
||||
interface CreateStudentInput {
|
||||
@@ -28,11 +29,6 @@ const FORBIDDEN_INPUT_KEYS = new Set([
|
||||
|
||||
const PHONE_RE = /^1[3-9]\d{9}$/;
|
||||
|
||||
/** String() 包装:避免 unknown 收窄后触发 no-base-to-string。 */
|
||||
function stringify(value: unknown): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a student archive from form-confirmed data.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import ExcelJS from 'exceljs';
|
||||
import { readXlsxSheetsFallback } from '../imports/imports.workbook-fallback';
|
||||
import { bufferToArrayBuffer } from '../common/buffer';
|
||||
|
||||
export interface ExcelSheetInfo {
|
||||
name: string;
|
||||
@@ -86,7 +87,7 @@ export class AiExcelReaderService {
|
||||
|
||||
private async loadWithExcelJs(buffer: Buffer): Promise<ExcelSheetRows[]> {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(buffer as unknown as ArrayBuffer);
|
||||
await workbook.xlsx.load(bufferToArrayBuffer(buffer));
|
||||
const sheets: ExcelSheetRows[] = [];
|
||||
workbook.eachSheet((sheet) => {
|
||||
const rows: string[][] = [];
|
||||
|
||||
@@ -24,6 +24,7 @@ import { extractRequestInfo } from '../common/request-utils';
|
||||
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import { CLASSROOM_TEMPLATE_COLUMNS } from './classroom-template';
|
||||
import { bufferToArrayBuffer } from '../common/buffer';
|
||||
|
||||
interface AuthenticatedRequest {
|
||||
user?: { id: number; username: string };
|
||||
@@ -173,7 +174,7 @@ export class ClassroomsController {
|
||||
async importExcel(@UploadedFile() file: Express.Multer.File, @Request() req: AuthenticatedRequest) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file.buffer as unknown as ArrayBuffer);
|
||||
await workbook.xlsx.load(bufferToArrayBuffer(file.buffer));
|
||||
const ws = workbook.worksheets[0];
|
||||
const rows: {
|
||||
name: string;
|
||||
|
||||
17
apps/server/src/common/buffer.ts
Normal file
17
apps/server/src/common/buffer.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 将 Node Buffer 安全转换为 ArrayBuffer。
|
||||
*
|
||||
* ExcelJS 的 xlsx.load 类型只接受 Buffer | ArrayBuffer,而 Node 20+ 的
|
||||
* Buffer 已是泛型 Buffer<ArrayBufferLike>,与 exceljs 的旧类型签名不兼容,
|
||||
* 直接传参会报 TS2345。此处取底层 ArrayBuffer 的精确切片
|
||||
* (含 byteOffset/byteLength),既避免 Buffer 来自池切片时携带无关字节,
|
||||
* 也把类型边界收敛到单一实现。
|
||||
*/
|
||||
export function bufferToArrayBuffer(buffer: Buffer): ArrayBuffer {
|
||||
return buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.byteLength,
|
||||
) as ArrayBuffer;
|
||||
}
|
||||
|
||||
export default bufferToArrayBuffer;
|
||||
13
apps/server/src/common/stringify.ts
Normal file
13
apps/server/src/common/stringify.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 安全字符串化 unknown。
|
||||
*
|
||||
* `@typescript-eslint/no-base-to-string` 规则在调用点会把 unknown 经
|
||||
* `== null` / `||` / `??` 收窄为 `{}`(对象类型)后仍标记 String(value);
|
||||
* 而函数参数位置不受调用点收窄影响。此助手统一处理该场景,
|
||||
* 避免各模块重复定义同名工具。
|
||||
*/
|
||||
export function stringify(value: unknown): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export default stringify;
|
||||
@@ -2,17 +2,13 @@ import { Logger } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { uuidV7 } from '../common/uuid-v7';
|
||||
import { withQueryRunner } from './database-migrations.runner';
|
||||
import { stringify } from '../common/stringify';
|
||||
|
||||
/** 迁移脚本中用到的 organizations 表最小行结构。 */
|
||||
interface OrganizationRow {
|
||||
id: number;
|
||||
}
|
||||
|
||||
/** String() 包装:避免 unknown 收窄后触发 no-base-to-string。 */
|
||||
function stringify(value: unknown): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export async function backfillOrganizations(
|
||||
dataSource: DataSource,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -37,6 +37,7 @@ import { BatchIdsDto } from '../common/batch-ids.dto';
|
||||
import type { AuthenticatedUser } from '../authorization';
|
||||
import { PersonalExpense } from '../entities/personal-expense.entity';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import { bufferToArrayBuffer } from '../common/buffer';
|
||||
|
||||
interface AuthenticatedRequest {
|
||||
user: AuthenticatedUser;
|
||||
@@ -354,7 +355,7 @@ export class ExpensesController {
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async importUtilityExpenses(@UploadedFile() file: Express.Multer.File, @Request() req: AuthenticatedRequest) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file.buffer as unknown as ArrayBuffer);
|
||||
await workbook.xlsx.load(bufferToArrayBuffer(file.buffer));
|
||||
const ws = workbook.worksheets[0];
|
||||
const rows: UtilityImportRow[] = [];
|
||||
ws.eachRow((row, idx) => {
|
||||
@@ -419,7 +420,7 @@ export class ExpensesController {
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async importPersonalExpenses(@UploadedFile() file: Express.Multer.File, @Request() req: AuthenticatedRequest) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file.buffer as unknown as ArrayBuffer);
|
||||
await workbook.xlsx.load(bufferToArrayBuffer(file.buffer));
|
||||
const ws = workbook.worksheets[0];
|
||||
const rows: PersonalImportRow[] = [];
|
||||
ws.eachRow((row, idx) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Readable } from 'node:stream';
|
||||
import { cellValue, textValue } from './imports.helpers';
|
||||
import { fallbackSheetsToImportSheets, readXlsxSheetsFallback } from './imports.workbook-fallback';
|
||||
import type { CellValue } from './imports.types';
|
||||
import { bufferToArrayBuffer } from '../common/buffer';
|
||||
|
||||
const MAX_SHEETS = 30;
|
||||
const MAX_ROWS_PER_SHEET = 3000;
|
||||
@@ -84,7 +85,7 @@ export async function parseSheets(
|
||||
if (kind === 'csv') {
|
||||
await workbook.csv.read(Readable.from(Buffer.from(buffer)));
|
||||
} else {
|
||||
await workbook.xlsx.load(buffer as unknown as ArrayBuffer);
|
||||
await workbook.xlsx.load(bufferToArrayBuffer(buffer));
|
||||
}
|
||||
const sheets = extractSheets(workbook, headerRow);
|
||||
if (sheets.length === 0) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stringify } from '../../common/stringify';
|
||||
import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -29,10 +30,6 @@ export class IntegrationConfigService {
|
||||
) {}
|
||||
|
||||
/** String() 包装:避免 unknown 收窄后触发 no-base-to-string。 */
|
||||
private stringify(value: unknown): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/** 解析 content JSON 并取 config 段(无 config 时回退整个对象)。 */
|
||||
private parseStoredConfig(content: string): Record<string, unknown> {
|
||||
const parsed = JSON.parse(content) as StoredConfigShape;
|
||||
@@ -208,8 +205,8 @@ export class IntegrationConfigService {
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
if (type.toUpperCase() === 'DINGTALK') {
|
||||
const appKey = this.stringify(config.agentId || '');
|
||||
const appSecret = this.stringify(config.appSecret || '');
|
||||
const appKey = stringify(config.agentId || '');
|
||||
const appSecret = stringify(config.appSecret || '');
|
||||
if (!appKey || !appSecret) return null;
|
||||
return await this.fetchDingTalkToken(appKey, appSecret);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { bufferToArrayBuffer } from '../common/buffer';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
@@ -270,7 +271,7 @@ export class OccupanciesController {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
if (!file?.buffer) throw new BadRequestException('请上传入住名单 Excel 文件');
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file.buffer as unknown as ArrayBuffer);
|
||||
await workbook.xlsx.load(bufferToArrayBuffer(file.buffer));
|
||||
const ws = workbook.worksheets[0];
|
||||
const rows = parseOccupancyImportWorksheet(ws);
|
||||
const result = await this.service.batchImportCheckIn(rows, {
|
||||
|
||||
@@ -30,6 +30,7 @@ import { extractRequestInfo } from '../common/request-utils';
|
||||
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||
import { BatchIdsDto } from '../common/batch-ids.dto';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import { bufferToArrayBuffer } from '../common/buffer';
|
||||
|
||||
interface AuthenticatedRequest {
|
||||
user?: { id: number; username: string };
|
||||
@@ -358,7 +359,7 @@ export class RoomsController {
|
||||
async importExcel(@UploadedFile() file: Express.Multer.File, @Request() req: AuthenticatedRequest) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file.buffer as unknown as ArrayBuffer);
|
||||
await workbook.xlsx.load(bufferToArrayBuffer(file.buffer));
|
||||
const ws = workbook.worksheets[0];
|
||||
const rows: {
|
||||
roomNumber: string;
|
||||
|
||||
@@ -3,14 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ClassSchedule } from '../entities';
|
||||
import type { WeeklyViewQueryDto } from './dto/schedule.dto';
|
||||
import { stringify } from '../common/stringify';
|
||||
|
||||
const ACTIVE_SCHEDULE_STATUS = 'active';
|
||||
|
||||
/** String() 包装:避免 raw 行值(unknown 收窄为对象类型)触发 no-base-to-string。 */
|
||||
function stringify(value: unknown): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleQueriesService {
|
||||
constructor(
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
STUDENT_EXPORT_COLUMNS,
|
||||
} from './student-import';
|
||||
import { BatchIdsDto } from '../common/batch-ids.dto';
|
||||
import { bufferToArrayBuffer } from '../common/buffer';
|
||||
|
||||
interface AuthenticatedRequest {
|
||||
user: AuthenticatedUser;
|
||||
@@ -255,7 +256,7 @@ export class StudentsController {
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async importExcel(@UploadedFile() file: Express.Multer.File, @Request() req: AuthenticatedRequest) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file.buffer as unknown as ArrayBuffer);
|
||||
await workbook.xlsx.load(bufferToArrayBuffer(file.buffer));
|
||||
const importData = parseStudentImportWorkbook(workbook);
|
||||
// Resolve organization names to IDs
|
||||
for (const row of importData.students) {
|
||||
@@ -280,7 +281,7 @@ export class StudentsController {
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async matchImport(@UploadedFile() file: Express.Multer.File, @Request() req: AuthenticatedRequest) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file.buffer as unknown as ArrayBuffer);
|
||||
await workbook.xlsx.load(bufferToArrayBuffer(file.buffer));
|
||||
const importData = parseStudentImportWorkbook(workbook);
|
||||
// Resolve organization names to IDs
|
||||
for (const row of importData.students) {
|
||||
|
||||
Reference in New Issue
Block a user