feat: 类型规范

This commit is contained in:
手瓜一十雪
2025-02-02 20:16:11 +08:00
parent dec9b477e0
commit d626f872e6
16 changed files with 119 additions and 173 deletions

View File

@@ -1,70 +1,30 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin"; import eslint from '@eslint/js';
import _import from "eslint-plugin-import"; import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
import { fixupPluginRules } from "@eslint/compat"; import tsEslintParser from '@typescript-eslint/parser';
import globals from "globals"; import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
const compat = new FlatCompat({
baseDirectory: dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [{
ignores: ["src/core/proto/"],
}, ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), {
plugins: {
"@typescript-eslint": typescriptEslint,
import: fixupPluginRules(_import),
},
const customTsFlatConfig = [
{
name: 'typescript-eslint/base',
languageOptions: { languageOptions: {
parser: tsEslintParser,
sourceType: 'module',
globals: { globals: {
...globals.browser, ...globals.browser,
...globals.node, ...globals.node,
}, },
parser: tsParser,
ecmaVersion: "latest",
sourceType: "module",
}, },
files: ['**/*.{ts,tsx}'],
settings: {
"import/parsers": {
"@typescript-eslint/parser": [".ts"],
},
"import/resolver": {
typescript: {
alwaysTryTypes: true,
},
},
},
rules: { rules: {
indent: ["error", 4], ...tsEslintPlugin.configs.recommended.rules,
semi: ["error", "always"], 'quotes': ['error', 'single'], // 使用单引号
"no-unused-vars": "off", 'semi': ['error', 'always'], // 强制使用分号
"no-async-promise-executor": "off", 'indent': ['error', 4], // 使用 4 空格缩进
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-var-requires": "off",
"object-curly-spacing": ["error", "always"],
}, },
}, { plugins: {
files: ["**/.eslintrc.{js,cjs}"], '@typescript-eslint': tsEslintPlugin,
},
},
];
languageOptions: { export default [eslint.configs.recommended, ...customTsFlatConfig];
globals: {
...globals.node,
},
ecmaVersion: 5,
sourceType: "commonjs",
},
}];

View File

@@ -28,11 +28,11 @@ async function handleWavFile(
file: Buffer, file: Buffer,
filePath: string, filePath: string,
pcmPath: string, pcmPath: string,
logger: LogWrapper _logger: LogWrapper
): Promise<{ input: Buffer; sampleRate: number }> { ): Promise<{ input: Buffer; sampleRate: number }> {
const { fmt } = getWavFileInfo(file); const { fmt } = getWavFileInfo(file);
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) { if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
return { input: await FFmpegService.convert(filePath, pcmPath, logger), sampleRate: 24000 }; return { input: await FFmpegService.convert(filePath, pcmPath), sampleRate: 24000 };
} }
return { input: file, sampleRate: fmt.sampleRate }; return { input: file, sampleRate: fmt.sampleRate };
} }
@@ -46,7 +46,7 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
const pcmPath = `${pttPath}.pcm`; const pcmPath = `${pttPath}.pcm`;
const { input, sampleRate } = isWav(file) const { input, sampleRate } = isWav(file)
? (await handleWavFile(file, filePath, pcmPath, logger)) ? (await handleWavFile(file, filePath, pcmPath, logger))
: { input: await FFmpegService.convert(filePath, pcmPath, logger), sampleRate: 24000 }; : { input: await FFmpegService.convert(filePath, pcmPath), sampleRate: 24000 };
const silk = await piscina.run({ input: input, sampleRate: sampleRate }); const silk = await piscina.run({ input: input, sampleRate: sampleRate });
await fsPromise.writeFile(pttPath, Buffer.from(silk.data)); await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration); logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);

View File

@@ -73,40 +73,3 @@ export class CancelableTask<T> {
}; };
} }
} }
async function demoAwait() {
const executor: TaskExecutor<number> = async (resolve, reject, onCancel) => {
let count = 0;
const intervalId = setInterval(() => {
count++;
console.log(`Task is running... Count: ${count}`);
if (count === 5) {
clearInterval(intervalId);
resolve(count);
}
}, 1000);
onCancel(() => {
clearInterval(intervalId);
console.log('Task has been canceled.');
reject(new Error('Task was canceled'));
});
};
const task = new CancelableTask(executor);
task.onCancel(() => {
console.log('Cancel listener triggered.');
});
setTimeout(() => {
task.cancel(); // 取消任务
}, 6000);
try {
const result = await task;
console.log(`Task completed with result: ${result}`);
} catch (error) {
console.error('Task failed:', error);
}
}

View File

@@ -60,12 +60,16 @@ export class NTEventWrapper {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }; [key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> };
}; };
if (eventNameArr.length > 1) { if (eventNameArr.length > 1) {
const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', ''); const serviceName = 'get' + (eventNameArr[0]?.replace('NodeIKernel', '') ?? '');
const eventName = eventNameArr[1]; const eventName = eventNameArr[1];
const services = (this.WrapperSession as unknown as eventType)[serviceName](); const services = (this.WrapperSession as unknown as eventType)[serviceName]?.();
if (!services || !eventName) {
return undefined;
}
let event = services[eventName]; let event = services[eventName];
//重新绑定this //重新绑定this
event = event.bind(services); event = event?.bind(services);
if (event) { if (event) {
return event as T; return event as T;
} }
@@ -126,8 +130,8 @@ export class NTEventWrapper {
) { ) {
return new Promise<Parameters<ListenerType>>((resolve, reject) => { return new Promise<Parameters<ListenerType>>((resolve, reject) => {
const ListenerNameList = listenerAndMethod.split('/'); const ListenerNameList = listenerAndMethod.split('/');
const ListenerMainName = ListenerNameList[0]; const ListenerMainName = ListenerNameList[0] ?? '';
const ListenerSubName = ListenerNameList[1]; const ListenerSubName = ListenerNameList[1] ?? '';
const id = randomUUID(); const id = randomUUID();
let complete = 0; let complete = 0;
let retData: Parameters<ListenerType> | undefined = undefined; let retData: Parameters<ListenerType> | undefined = undefined;
@@ -205,8 +209,8 @@ export class NTEventWrapper {
} }
const ListenerNameList = listenerAndMethod.split('/'); const ListenerNameList = listenerAndMethod.split('/');
const ListenerMainName = ListenerNameList[0]; const ListenerMainName = ListenerNameList[0]??"";
const ListenerSubName = ListenerNameList[1]; const ListenerSubName = ListenerNameList[1]??"";
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>( return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
(resolve, reject) => { (resolve, reject) => {

View File

@@ -1,7 +1,6 @@
import { FFmpeg } from '@ffmpeg.wasm/main'; import { FFmpeg } from '@ffmpeg.wasm/main';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { readFileSync, statSync, writeFileSync } from 'fs'; import { readFileSync, statSync, writeFileSync } from 'fs';
import type { LogWrapper } from './log';
import type { VideoInfo } from './video'; import type { VideoInfo } from './video';
import { fileTypeFromFile } from 'file-type'; import { fileTypeFromFile } from 'file-type';
import imageSize from 'image-size'; import imageSize from 'image-size';
@@ -12,7 +11,7 @@ import imageSize from 'image-size';
const outputFileName = `${randomUUID()}.jpg`; const outputFileName = `${randomUUID()}.jpg`;
try { try {
ffmpegInstance.fs.writeFile(videoFileName, readFileSync(videoPath)); ffmpegInstance.fs.writeFile(videoFileName, readFileSync(videoPath));
let code = await ffmpegInstance.run('-i', videoFileName, '-ss', '00:00:01.000', '-vframes', '1', outputFileName); const code = await ffmpegInstance.run('-i', videoFileName, '-ss', '00:00:01.000', '-vframes', '1', outputFileName);
if (code !== 0) { if (code !== 0) {
throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code); throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
} }
@@ -44,7 +43,7 @@ import imageSize from 'image-size';
const params = format === 'amr' const params = format === 'amr'
? ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, '-ar', '8000', '-b:a', '12.2k', outputFileName] ? ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, '-ar', '8000', '-b:a', '12.2k', outputFileName]
: ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, outputFileName]; : ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, outputFileName];
let code = await ffmpegInstance.run(...params); const code = await ffmpegInstance.run(...params);
if (code !== 0) { if (code !== 0) {
throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code); throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
} }
@@ -74,7 +73,7 @@ import imageSize from 'image-size';
try { try {
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(filePath)); ffmpegInstance.fs.writeFile(inputFileName, readFileSync(filePath));
const params = ['-y', '-i', inputFileName, '-ar', '24000', '-ac', '1', '-f', 's16le', outputFileName]; const params = ['-y', '-i', inputFileName, '-ar', '24000', '-ac', '1', '-f', 's16le', outputFileName];
let code = await ffmpegInstance.run(...params); const code = await ffmpegInstance.run(...params);
if (code !== 0) { if (code !== 0) {
throw new Error('FFmpeg process exited with code ' + code); throw new Error('FFmpeg process exited with code ' + code);
} }
@@ -99,24 +98,24 @@ import imageSize from 'image-size';
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> { public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
await FFmpegService.extractThumbnail(videoPath, thumbnailPath); await FFmpegService.extractThumbnail(videoPath, thumbnailPath);
let fileType = (await fileTypeFromFile(videoPath))?.ext ?? 'mp4'; const fileType = (await fileTypeFromFile(videoPath))?.ext ?? 'mp4';
const inputFileName = `${randomUUID()}.${fileType}`; const inputFileName = `${randomUUID()}.${fileType}`;
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }); const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(videoPath)); ffmpegInstance.fs.writeFile(inputFileName, readFileSync(videoPath));
ffmpegInstance.setLogging(true); ffmpegInstance.setLogging(true);
let duration = 60; let duration = 60;
ffmpegInstance.setLogger((level, ...msg) => { ffmpegInstance.setLogger((_level, ...msg) => {
const message = msg.join(' '); const message = msg.join(' ');
const durationMatch = message.match(/Duration: (\d+):(\d+):(\d+\.\d+)/); const durationMatch = message.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
if (durationMatch) { if (durationMatch) {
const hours = parseInt(durationMatch[1], 10); const hours = parseInt(durationMatch[1] ?? "0", 10);
const minutes = parseInt(durationMatch[2], 10); const minutes = parseInt(durationMatch[2] ?? "0", 10);
const seconds = parseFloat(durationMatch[3]); const seconds = parseFloat(durationMatch[3] ?? "0");
duration = hours * 3600 + minutes * 60 + seconds; duration = hours * 3600 + minutes * 60 + seconds;
} }
}); });
await ffmpegInstance.run('-i', inputFileName); await ffmpegInstance.run('-i', inputFileName);
let image = imageSize(thumbnailPath); const image = imageSize(thumbnailPath);
ffmpegInstance.fs.unlink(inputFileName); ffmpegInstance.fs.unlink(inputFileName);
const fileSize = statSync(videoPath).size; const fileSize = statSync(videoPath).size;
return { return {
@@ -126,7 +125,7 @@ import imageSize from 'image-size';
format: fileType, format: fileType,
size: fileSize, size: fileSize,
filePath: videoPath filePath: videoPath
} };
} }
} }
type FFmpegMethod = 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo'; type FFmpegMethod = 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';

View File

@@ -1,6 +1,5 @@
import Piscina from "piscina"; import Piscina from "piscina";
import { VideoInfo } from "./video"; import { VideoInfo } from "./video";
import type { LogWrapper } from "./log";
type EncodeArgs = { type EncodeArgs = {
method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo'; method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';

View File

@@ -190,7 +190,7 @@ export async function uriToLocalFile(dir: string, uri: string, filename: string
} }
case FileUriType.Remote: { case FileUriType.Remote: {
const buffer = await httpDownload({ url: HandledUri, headers: headers }); const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
fs.writeFileSync(filePath, buffer); fs.writeFileSync(filePath, buffer);
return { success: true, errMsg: '', fileName: filename, path: filePath }; return { success: true, errMsg: '', fileName: filename, path: filePath };
} }

View File

@@ -166,7 +166,7 @@ export function calcQQLevel(level?: QQLevel) {
} }
export function stringifyWithBigInt(obj: any) { export function stringifyWithBigInt(obj: any) {
return JSON.stringify(obj, (key, value) => return JSON.stringify(obj, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value typeof value === 'bigint' ? value.toString() : value
); );
} }

View File

@@ -68,8 +68,11 @@ export class LimitedHashTable<K, V> {
const listSize = Math.min(size, keyList.length); const listSize = Math.min(size, keyList.length);
for (let i = 0; i < listSize; i++) { for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i]; const key = keyList[listSize - i];
if (key !== undefined) {
result.push({ key, value: this.keyToValue.get(key)! }); result.push({ key, value: this.keyToValue.get(key)! });
} }
}
return result; return result;
} }
} }
@@ -96,8 +99,10 @@ class MessageUniqueWrapper {
createUniqueMsgId(peer: Peer, msgId: string) { createUniqueMsgId(peer: Peer, msgId: string) {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`; const key = `${msgId}|${peer.chatType}|${peer.peerUid}`;
const hash = crypto.createHash('md5').update(key).digest(); const hash = crypto.createHash('md5').update(key).digest();
if (hash[0]) {
//设置第一个bit为0 保证shortId为正数 //设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f; hash[0] &= 0x7f;
}
const shortId = hash.readInt32BE(0); const shortId = hash.readInt32BE(0);
//减少性能损耗 //减少性能损耗
this.msgIdMap.set(msgId, shortId); this.msgIdMap.set(msgId, shortId);
@@ -110,11 +115,11 @@ class MessageUniqueWrapper {
if (data) { if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|'); const [msgId, chatTypeStr, peerUid] = data.split('|');
const peer: Peer = { const peer: Peer = {
chatType: parseInt(chatTypeStr), chatType: parseInt(chatTypeStr ?? '0'),
peerUid, peerUid: peerUid ?? '',
guildId: '', guildId: '',
}; };
return { MsgId: msgId, Peer: peer }; return { MsgId: msgId ?? '0', Peer: peer };
} }
return undefined; return undefined;
} }

View File

@@ -41,12 +41,14 @@ export class RequestUtil {
private static extractCookies(setCookieHeaders: string[], cookies: { [key: string]: string }) { private static extractCookies(setCookieHeaders: string[], cookies: { [key: string]: string }) {
setCookieHeaders.forEach((cookie) => { setCookieHeaders.forEach((cookie) => {
const parts = cookie.split(';')[0].split('='); const parts = cookie.split(';')[0]?.split('=');
if (parts) {
const key = parts[0]; const key = parts[0];
const value = parts[1]; const value = parts[1];
if (key && value && key.length > 0 && value.length > 0) { if (key && value && key.length > 0 && value.length > 0) {
cookies[key] = value; cookies[key] = value;
} }
}
}); });
} }

View File

@@ -1,12 +1,11 @@
import { OB11EmitEventContent } from './index'; import { OB11EmitEventContent } from './index';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { OB11Response } from '@/onebot/action/OneBotAction';
import { OB11HttpServerAdapter } from './http-server'; import { OB11HttpServerAdapter } from './http-server';
export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter { export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter {
private sseClients: Response[] = []; private sseClients: Response[] = [];
async handleRequest(req: Request, res: Response): Promise<any> { override async handleRequest(req: Request, res: Response): Promise<any> {
if (req.path === '/_events') { if (req.path === '/_events') {
return this.createSseSupport(req, res); return this.createSseSupport(req, res);
} else { } else {
@@ -26,7 +25,7 @@ export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter {
}); });
} }
onEvent<T extends OB11EmitEventContent>(event: T) { override onEvent<T extends OB11EmitEventContent>(event: T) {
this.sseClients.forEach((res) => { this.sseClients.forEach((res) => {
res.write(`data: ${JSON.stringify(event)}\n\n`); res.write(`data: ${JSON.stringify(event)}\n\n`);
}); });

View File

@@ -18,7 +18,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
super(name, config, core, obContext, actions); super(name, config, core, obContext, actions);
} }
onEvent<T extends OB11EmitEventContent>(event: T) { onEvent<T extends OB11EmitEventContent>(_event: T) {
// http server is passive, no need to emit event // http server is passive, no need to emit event
} }
@@ -65,7 +65,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
return res.status(400).send('Invalid JSON'); return res.status(400).send('Invalid JSON');
} }
}); });
req.on('error', (err) => { req.on('error', (_err) => {
return res.status(400).send('Invalid JSON'); return res.status(400).send('Invalid JSON');
}); });
}); });

View File

@@ -1,6 +0,0 @@
declare global {
namespace globalThis {
var LiteLoader: symbol;
}
}
export {};

View File

@@ -1,6 +1,13 @@
import { NCoreInitShell } from "@/shell/base"; import { NCoreInitShell } from "@/shell/base";
export * from "@/framework/napcat"; export * from "@/framework/napcat";
export * from "@/shell/base"; export * from "@/shell/base";
if (global.LiteLoader == undefined) {
interface LiteLoaderGlobal extends Global {
LiteLoader?: unknown;
}
declare const global: LiteLoaderGlobal;
if (global.LiteLoader === undefined) {
NCoreInitShell(); NCoreInitShell();
} }

View File

@@ -18,8 +18,8 @@
"noEmit": true, "noEmit": true,
"jsx": "preserve", "jsx": "preserve",
"strict": true, "strict": true,
"noUnusedLocals": false, "noUnusedLocals": true,
"noUnusedParameters": false, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"sourceMap": true, "sourceMap": true,
"paths": { "paths": {
@@ -28,8 +28,22 @@
], ],
"@webapi/*": [ "@webapi/*": [
"./src/webui/src/*" "./src/webui/src/*"
], ]
} },
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"alwaysStrict": true,
"noImplicitThis": true,
"noImplicitReturns": false, // false for now
"noPropertyAccessFromIndexSignature": false, // false for now
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"useUnknownInCatchVariables": true,
"noImplicitOverride": true,
"strictPropertyInitialization": true
}, },
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts"