Compare commits

..

2 Commits

Author SHA1 Message Date
c1c3f9a5fd feat:优化表情包模块 2025-09-05 13:51:30 +08:00
dc37a45990 fix:标记潜在问题 2025-09-05 13:02:02 +08:00
19 changed files with 145 additions and 56 deletions

View File

@ -13,4 +13,4 @@
## 贡献 ## 贡献
- fork到自己的储存库 - fork到自己的储存库
- 在自己的储存库内推送更新 - 在自己的储存库内推送更新
- 提交pr等待合并 - 提交pr,等待合并

View File

@ -30,6 +30,7 @@
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"simple-git": "^3.28.0", "simple-git": "^3.28.0",
"ssh2": "^1.16.0", "ssh2": "^1.16.0",
"stream-throttle": "^0.1.3",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"ws": "^8.18.3" "ws": "^8.18.3"
}, },
@ -44,6 +45,7 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.16.4", "@types/node": "^22.16.4",
"@types/stream-throttle": "^0.1.4",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"eslint": "^9.18.0", "eslint": "^9.18.0",

39
pnpm-lock.yaml generated
View File

@ -53,6 +53,9 @@ importers:
ssh2: ssh2:
specifier: ^1.16.0 specifier: ^1.16.0
version: 1.16.0 version: 1.16.0
stream-throttle:
specifier: ^0.1.3
version: 0.1.3
uuid: uuid:
specifier: ^11.1.0 specifier: ^11.1.0
version: 11.1.0 version: 11.1.0
@ -90,6 +93,9 @@ importers:
'@types/node': '@types/node':
specifier: ^22.16.4 specifier: ^22.16.4
version: 22.16.5 version: 22.16.5
'@types/stream-throttle':
specifier: ^0.1.4
version: 0.1.4
'@types/supertest': '@types/supertest':
specifier: ^6.0.2 specifier: ^6.0.2
version: 6.0.3 version: 6.0.3
@ -699,49 +705,42 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-arm64-musl@1.0.4': '@napi-rs/nice-linux-arm64-musl@1.0.4':
resolution: {integrity: sha512-4b1KYG+sriufhFrpUS9uNOEYYJqSfcbnwGx6uGX7JjrH8tELG90cOpCawz5THNIwlS3DhLgnCOcn0+4p6z26QA==} resolution: {integrity: sha512-4b1KYG+sriufhFrpUS9uNOEYYJqSfcbnwGx6uGX7JjrH8tELG90cOpCawz5THNIwlS3DhLgnCOcn0+4p6z26QA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@napi-rs/nice-linux-ppc64-gnu@1.0.4': '@napi-rs/nice-linux-ppc64-gnu@1.0.4':
resolution: {integrity: sha512-iaf3vMRgr23oe1PUaKpxaH3DS0IMN0+N9iEiWVwYPm/U15vZFYdqVegGfN2PzrZLUl5lc8ZxbmEKDfuqslhAMA==} resolution: {integrity: sha512-iaf3vMRgr23oe1PUaKpxaH3DS0IMN0+N9iEiWVwYPm/U15vZFYdqVegGfN2PzrZLUl5lc8ZxbmEKDfuqslhAMA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-riscv64-gnu@1.0.4': '@napi-rs/nice-linux-riscv64-gnu@1.0.4':
resolution: {integrity: sha512-UXoREY6Yw6rHrGuTwQgBxpfjK34t6mTjibE9/cXbefL9AuUCJ9gEgwNKZiONuR5QGswChqo9cnthjdKkYyAdDg==} resolution: {integrity: sha512-UXoREY6Yw6rHrGuTwQgBxpfjK34t6mTjibE9/cXbefL9AuUCJ9gEgwNKZiONuR5QGswChqo9cnthjdKkYyAdDg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-s390x-gnu@1.0.4': '@napi-rs/nice-linux-s390x-gnu@1.0.4':
resolution: {integrity: sha512-eFbgYCRPmsqbYPAlLYU5hYTNbogmIDUvknilehHsFhCH1+0/kN87lP+XaLT0Yeq4V/rpwChSd9vlz4muzFArtw==} resolution: {integrity: sha512-eFbgYCRPmsqbYPAlLYU5hYTNbogmIDUvknilehHsFhCH1+0/kN87lP+XaLT0Yeq4V/rpwChSd9vlz4muzFArtw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-x64-gnu@1.0.4': '@napi-rs/nice-linux-x64-gnu@1.0.4':
resolution: {integrity: sha512-4T3E6uTCwWT6IPnwuPcWVz3oHxvEp/qbrCxZhsgzwTUBEwu78EGNXGdHfKJQt3soth89MLqZJw+Zzvnhrsg1mQ==} resolution: {integrity: sha512-4T3E6uTCwWT6IPnwuPcWVz3oHxvEp/qbrCxZhsgzwTUBEwu78EGNXGdHfKJQt3soth89MLqZJw+Zzvnhrsg1mQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-x64-musl@1.0.4': '@napi-rs/nice-linux-x64-musl@1.0.4':
resolution: {integrity: sha512-NtbBkAeyBPLvCBkWtwkKXkNSn677eaT0cX3tygq+2qVv71TmHgX4gkX6o9BXjlPzdgPGwrUudavCYPT9tzkEqQ==} resolution: {integrity: sha512-NtbBkAeyBPLvCBkWtwkKXkNSn677eaT0cX3tygq+2qVv71TmHgX4gkX6o9BXjlPzdgPGwrUudavCYPT9tzkEqQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@napi-rs/nice-win32-arm64-msvc@1.0.4': '@napi-rs/nice-win32-arm64-msvc@1.0.4':
resolution: {integrity: sha512-vubOe3i+YtSJGEk/++73y+TIxbuVHi+W8ZzrRm2eETCjCRwNlgbfToQZ85dSA+4iBB/NJRGNp+O4hfdbbttZWA==} resolution: {integrity: sha512-vubOe3i+YtSJGEk/++73y+TIxbuVHi+W8ZzrRm2eETCjCRwNlgbfToQZ85dSA+4iBB/NJRGNp+O4hfdbbttZWA==}
@ -986,28 +985,24 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@swc/core-linux-arm64-musl@1.13.1': '@swc/core-linux-arm64-musl@1.13.1':
resolution: {integrity: sha512-JaqFdBCarIBKiMu5bbAp+kWPMNGg97ej+7KzbKOzWP5pRptqKi86kCDZT3WmjPe8hNG6dvBwbm7Y8JNry5LebQ==} resolution: {integrity: sha512-JaqFdBCarIBKiMu5bbAp+kWPMNGg97ej+7KzbKOzWP5pRptqKi86kCDZT3WmjPe8hNG6dvBwbm7Y8JNry5LebQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@swc/core-linux-x64-gnu@1.13.1': '@swc/core-linux-x64-gnu@1.13.1':
resolution: {integrity: sha512-t4cLkku10YECDaakWUH0452WJHIZtrLPRwezt6BdoMntVMwNjvXRX7C8bGuYcKC3YxRW7enZKFpozLhQIQ37oA==} resolution: {integrity: sha512-t4cLkku10YECDaakWUH0452WJHIZtrLPRwezt6BdoMntVMwNjvXRX7C8bGuYcKC3YxRW7enZKFpozLhQIQ37oA==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@swc/core-linux-x64-musl@1.13.1': '@swc/core-linux-x64-musl@1.13.1':
resolution: {integrity: sha512-fSMwZOaG+3ukUucbEbzz9GhzGhUhXoCPqHe9qW0/Vc2IZRp538xalygKyZynYweH5d9EHux1aj3+IO8/xBaoiA==} resolution: {integrity: sha512-fSMwZOaG+3ukUucbEbzz9GhzGhUhXoCPqHe9qW0/Vc2IZRp538xalygKyZynYweH5d9EHux1aj3+IO8/xBaoiA==}
engines: {node: '>=10'} engines: {node: '>=10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@swc/core-win32-arm64-msvc@1.13.1': '@swc/core-win32-arm64-msvc@1.13.1':
resolution: {integrity: sha512-tweCXK/79vAwj1NhAsYgICy8T1z2QEairmN2BFEBYFBFNMEB1iI1YlXwBkBtuihRvgZrTh1ORusKa4jLYzLCZA==} resolution: {integrity: sha512-tweCXK/79vAwj1NhAsYgICy8T1z2QEairmN2BFEBYFBFNMEB1iI1YlXwBkBtuihRvgZrTh1ORusKa4jLYzLCZA==}
@ -1152,6 +1147,9 @@ packages:
'@types/stack-utils@2.0.3': '@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
'@types/stream-throttle@0.1.4':
resolution: {integrity: sha512-VxXIHGjVuK8tYsVm60rIQMmF/0xguCeen5OmK5S4Y6K64A+z+y4/GI6anRnVzaUZaJB9Ah9IfbDcO0o1gZCc/w==}
'@types/superagent@8.1.9': '@types/superagent@8.1.9':
resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==}
@ -2619,6 +2617,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
limiter@1.1.5:
resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==}
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@ -3262,6 +3263,11 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
stream-throttle@0.1.3:
resolution: {integrity: sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==}
engines: {node: '>= 0.10.0'}
hasBin: true
streamsearch@1.1.0: streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@ -4828,6 +4834,10 @@ snapshots:
'@types/stack-utils@2.0.3': {} '@types/stack-utils@2.0.3': {}
'@types/stream-throttle@0.1.4':
dependencies:
'@types/node': 22.16.5
'@types/superagent@8.1.9': '@types/superagent@8.1.9':
dependencies: dependencies:
'@types/cookiejar': 2.1.5 '@types/cookiejar': 2.1.5
@ -6611,6 +6621,8 @@ snapshots:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
type-check: 0.4.0 type-check: 0.4.0
limiter@1.1.5: {}
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
load-esm@1.0.2: {} load-esm@1.0.2: {}
@ -7209,6 +7221,11 @@ snapshots:
statuses@2.0.2: {} statuses@2.0.2: {}
stream-throttle@0.1.3:
dependencies:
commander: 2.20.3
limiter: 1.1.5
streamsearch@1.1.0: {} streamsearch@1.1.0: {}
streamx@2.22.1: streamx@2.22.1:

View File

@ -25,7 +25,7 @@ export class AppConfigService implements OnModuleInit {
if (defaultValue !== undefined) { if (defaultValue !== undefined) {
return defaultValue; return defaultValue;
} }
this.logger.error(`环境变量 ${key} 未定义`); this.logger.error(`环境变量 ${key} 未定义!`);
} }
return value; return value;
} }

View File

@ -33,17 +33,17 @@ export class AutoUpdateService {
label = '子仓库', label = '子仓库',
): Promise<boolean> { ): Promise<boolean> {
try { try {
this.logger.log(`[${label}] 检查仓库更新中...`); this.logger.log(`[${label}] 检查仓库更新中..`);
const repoGit = simpleGit(folderPath); const repoGit = simpleGit(folderPath);
const status = await repoGit.status(); const status = await repoGit.status();
if (status.ahead > 0) { if (status.ahead > 0) {
this.logger.warn(`[${label}] 检测到本地仓库有未提交的更改,跳过更新`); this.logger.warn(`[${label}] 检测到本地仓库有未提交的更改,跳过更新..`);
return false; return false;
} }
this.logger.log(`[${label}] 正在获取远程仓库信息...`); this.logger.log(`[${label}] 正在获取远程仓库信息..`);
await repoGit.fetch(); await repoGit.fetch();
const localBranch = status.current; const localBranch = status.current;
@ -52,22 +52,22 @@ export class AutoUpdateService {
]); ]);
if (diffSummary.files.length > 0) { if (diffSummary.files.length > 0) {
this.logger.log(`[${label}] 检测到远程仓库有更新`); this.logger.log(`[${label}] 检测到远程仓库有更新!`);
if (localBranch) { if (localBranch) {
this.logger.log(`[${label}] 正在拉取远程代码...`); this.logger.log(`[${label}] 正在拉取远程代码..`);
await repoGit.pull('origin', localBranch); await repoGit.pull('origin', localBranch);
} else { } else {
this.logger.error(`[${label}] 当前分支名称未知,无法执行拉取操作。`); this.logger.error(`[${label}] 当前分支名称未知,无法执行拉取操作..`);
return false; return false;
} }
this.logger.log(`[${label}] 代码更新成功,开始更新依赖...`); this.logger.log(`[${label}] 代码更新成功,开始更新依赖..`);
await this.updateDependencies(folderPath, label); await this.updateDependencies(folderPath, label);
this.logger.log(`[${label}] 自动更新流程完成`); this.logger.log(`[${label}] 自动更新流程完成!`);
return true; return true;
} else { } else {
this.logger.log(`[${label}] 远程仓库没有新变化`); this.logger.log(`[${label}] 远程仓库没有新变化..`);
return false; return false;
} }
} catch (error) { } catch (error) {
@ -94,16 +94,16 @@ export class AutoUpdateService {
try { try {
pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8')); pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
} catch { } catch {
this.logger.warn(`[${label}] 未找到 package.json跳过依赖构建`); this.logger.warn(`[${label}] 未找到 package.json,跳过依赖构建`);
return; return;
} }
if (pkgJson.scripts?.build) { if (pkgJson.scripts?.build) {
this.logger.log(`[${label}] 检测到 build 脚本,执行 pnpm build...`); this.logger.log(`[${label}] 检测到 build 脚本,执行 pnpm build..`);
await execAsync('pnpm build', { cwd: folderPath }); await execAsync('pnpm build', { cwd: folderPath });
this.logger.log(`[${label}] 构建完成`); this.logger.log(`[${label}] 构建完成!`);
} else { } else {
this.logger.log(`[${label}] 未检测到 build 脚本,跳过构建`); this.logger.log(`[${label}] 未检测到 build 脚本,跳过构建..`);
} }
} catch (error) { } catch (error) {
this.logger.error(`[${label}] 更新依赖或构建失败:`, error); this.logger.error(`[${label}] 更新依赖或构建失败:`, error);

View File

@ -178,8 +178,29 @@ export class RedisService implements OnModuleInit {
await this.Persistence.writeDataLocal(key, data, fileName); await this.Persistence.writeDataLocal(key, data, fileName);
} }
public async test(): Promise<void> { /**
const user = await this.fetch<IUser>('Jerry', 'IUser'); * IP
this.logger.debug('User:', user); * @param ip IP
* @param bytes
* @param window
*/
public async incrementIpTraffic(
ip: string,
bytes: number,
window = 1,
): Promise<number> {
const key = `traffic:${ip}`;
const total = await this.client.incrby(key, bytes);
await this.client.expire(key, window);
return total;
}
/**
* IP
*/
public async getIpTraffic(ip: string): Promise<number> {
const key = `traffic:${ip}`;
const value = await this.client.get(key);
return value ? parseInt(value, 10) : 0;
} }
} }

View File

@ -42,7 +42,7 @@ export class SystemService {
const prev = Number(fs.readFileSync(this.restartFile, 'utf-8')); const prev = Number(fs.readFileSync(this.restartFile, 'utf-8'));
const duration = ((Date.now() - prev) / 1000 - 5).toFixed(2); const duration = ((Date.now() - prev) / 1000 - 5).toFixed(2);
fs.unlinkSync(this.restartFile); fs.unlinkSync(this.restartFile);
this.logger.debug(`检测到重启耗时: ${duration}`); this.logger.debug(`检测到重启,耗时: ${duration}`);
return Number(duration); return Number(duration);
} }
return null; return null;
@ -65,7 +65,7 @@ export class SystemService {
this.logger.debug('检查系统代码更新..'); this.logger.debug('检查系统代码更新..');
const updated = await this.autoUpdateService.checkForUpdates(); const updated = await this.autoUpdateService.checkForUpdates();
if (updated) { if (updated) {
this.logger.warn('系统代码已更新正在重启..'); this.logger.warn('系统代码已更新,正在重启..');
process.exit(1); process.exit(1);
} }
} }

View File

@ -65,8 +65,8 @@ export class ToolsService {
public checkToken(token: string): boolean { public checkToken(token: string): boolean {
const expected = this.config.get<string>('TOKEN'); const expected = this.config.get<string>('TOKEN');
if (!expected) { if (!expected) {
this.logger.error('环境变量 TOKEN 未配置,无法进行验证!'); this.logger.error('环境变量 TOKEN 未配置,无法进行验证!');
throw new UnauthorizedException('系统配置错误缺少 TOKEN'); throw new UnauthorizedException('系统配置错误,缺少 TOKEN');
} }
return token === expected; return token === expected;
} }

View File

@ -6,7 +6,7 @@ export class WsTools {
static async send(socket: WebSocket, data: unknown): Promise<boolean> { static async send(socket: WebSocket, data: unknown): Promise<boolean> {
if (socket.readyState !== 1) { if (socket.readyState !== 1) {
this.logger.warn('尝试向非 OPEN 状态的 socket 发送消息已丢弃'); this.logger.warn('尝试向非 OPEN 状态的 socket 发送消息,已丢弃');
return false; return false;
} }

View File

@ -16,10 +16,10 @@ async function bootstrap() {
if (!fs.existsSync(envPath)) { if (!fs.existsSync(envPath)) {
if (fs.existsSync(envExamplePath)) { if (fs.existsSync(envExamplePath)) {
fs.copyFileSync(envExamplePath, envPath); fs.copyFileSync(envExamplePath, envPath);
Logger.warn(`.env 文件已自动生成请修改配置后重启核心..`, '', 'ENV'); Logger.warn(`.env 文件已自动生成,请修改配置后重启核心..`, '', 'ENV');
process.exit(1); process.exit(1);
} else { } else {
Logger.error('配置模块初始化出错请重新拉取应用!', '', 'ENV'); Logger.error('配置模块初始化出错,请重新拉取应用!', '', 'ENV');
process.exit(1); process.exit(1);
} }
} }
@ -39,7 +39,7 @@ async function bootstrap() {
const systemService = app.get(SystemService); const systemService = app.get(SystemService);
const restartDuration = systemService.checkRestartTime(); const restartDuration = systemService.checkRestartTime();
if (restartDuration) { if (restartDuration) {
Logger.warn(`重启完成耗时 ${restartDuration}`, '', 'System'); Logger.warn(`重启完成!耗时 ${restartDuration}`, '', 'System');
} }
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('晶灵核心') .setTitle('晶灵核心')

View File

@ -65,6 +65,6 @@ export class BotController {
@ApiBody({ type: BroadcastDto }) @ApiBody({ type: BroadcastDto })
public async smartBroadcast(@Body() dto: BroadcastDto) { public async smartBroadcast(@Body() dto: BroadcastDto) {
await this.botService.broadcastToAllGroups(dto.message); await this.botService.broadcastToAllGroups(dto.message);
return { message: '广播任务已开始正在后台执行..' }; return { message: '广播任务已开始,正在后台执行..' };
} }
} }

View File

@ -18,7 +18,7 @@ export class SendMessageDto extends GroupInfoDto {
export class BroadcastDto extends TokenDto { export class BroadcastDto extends TokenDto {
@ApiProperty({ @ApiProperty({
description: '要广播的消息', description: '要广播的消息',
example: '全体目光向我看齐我宣布个事儿..', example: '全体目光向我看齐!我宣布个事儿..',
}) })
message: string; message: string;
} }

View File

@ -179,7 +179,7 @@ export class BotService {
}, },
}; };
this.logger.log( this.logger.log(
`[广播] 向群 ${groupId} 使用Bot ${selectedBotId}(客户端 ${selectedClientId})发送消息${message}延迟 ${ `[广播] 向群 ${groupId} 使用Bot ${selectedBotId}(客户端 ${selectedClientId})发送消息${message},延迟 ${
delay / 1000 delay / 1000
} `, } `,
); );

View File

@ -26,7 +26,7 @@ export class CdnController {
const filePath = await this.fileService.getFile(relativePath); const filePath = await this.fileService.getFile(relativePath);
if (!filePath) { if (!filePath) {
this.logger.warn(`${relativePath}:文件不存在..`); this.logger.warn(`${relativePath}:文件不存在..`);
throw new HttpException('文件不存在啦', HttpStatus.NOT_FOUND); throw new HttpException('文件不存在啦!', HttpStatus.NOT_FOUND);
} }
res.sendFile(filePath, (err) => { res.sendFile(filePath, (err) => {

View File

@ -7,11 +7,15 @@ import {
HttpStatus, HttpStatus,
Logger, Logger,
Inject, Inject,
Ip,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBody, ApiProperty } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiBody, ApiProperty } from '@nestjs/swagger';
import { MemeService } from './meme.service'; import { MemeService } from './meme.service';
import { Response } from 'express'; import { Response } from 'express';
import * as fs from 'fs'; import * as fs from 'fs';
import { Throttle } from 'stream-throttle';
import { ToolsService } from '../../core/tools/tools.service';
import { RedisService } from '../../core/redis/redis.service';
class MemeRequestDto { class MemeRequestDto {
@ApiProperty({ description: '角色名称', example: 'zhenxun', required: false }) @ApiProperty({ description: '角色名称', example: 'zhenxun', required: false })
@ -19,6 +23,13 @@ class MemeRequestDto {
@ApiProperty({ description: '状态', example: 'happy', required: false }) @ApiProperty({ description: '状态', example: 'happy', required: false })
status?: string; status?: string;
@ApiProperty({
description: '可选访问令牌',
example: 'token',
required: false,
})
token?: string;
} }
@Controller('meme') @Controller('meme')
@ -26,7 +37,14 @@ class MemeRequestDto {
export class MemeController { export class MemeController {
private readonly logger = new Logger(MemeController.name); private readonly logger = new Logger(MemeController.name);
constructor(@Inject(MemeService) private readonly memeService: MemeService) {} constructor(
@Inject(MemeService)
private readonly memeService: MemeService,
@Inject(ToolsService)
private readonly toolsService: ToolsService,
@Inject(RedisService)
private readonly redisService: RedisService,
) {}
@Post('getRandom') @Post('getRandom')
@ApiOperation({ summary: '获取随机表情包' }) @ApiOperation({ summary: '获取随机表情包' })
@ -34,8 +52,13 @@ export class MemeController {
public async getRandomMeme( public async getRandomMeme(
@Body() dto: MemeRequestDto, @Body() dto: MemeRequestDto,
@Res() res: Response, @Res() res: Response,
@Ip() ip: string,
) { ) {
try { try {
const realToken = dto.token;
const hasValidToken =
realToken && this.toolsService.checkToken(realToken);
const memePath = await this.memeService.getRandomMemePath( const memePath = await this.memeService.getRandomMemePath(
dto.character, dto.character,
dto.status, dto.status,
@ -48,14 +71,6 @@ export class MemeController {
); );
} }
const stream = fs.createReadStream(memePath);
stream.on('error', () => {
throw new HttpException(
'读取表情包失败',
HttpStatus.INTERNAL_SERVER_ERROR,
);
});
const ext = memePath.split('.').pop()?.toLowerCase(); const ext = memePath.split('.').pop()?.toLowerCase();
let contentType = 'image/jpeg'; let contentType = 'image/jpeg';
if (ext === 'png') contentType = 'image/png'; if (ext === 'png') contentType = 'image/png';
@ -63,9 +78,39 @@ export class MemeController {
if (ext === 'webp') contentType = 'image/webp'; if (ext === 'webp') contentType = 'image/webp';
res.setHeader('Content-Type', contentType); res.setHeader('Content-Type', contentType);
const stream = fs.createReadStream(memePath);
stream.on('error', () => {
throw new HttpException(
'读取表情包失败',
HttpStatus.INTERNAL_SERVER_ERROR,
);
});
if (hasValidToken) {
this.logger.log(`有token入不限速: ${memePath}`);
stream.pipe(res); stream.pipe(res);
} else {
stream.on('data', async (chunk) => {
const bytes = chunk.length;
const total = await this.redisService.incrementIpTraffic(
ip,
bytes,
1,
);
if (total > 100 * 1024) {
this.logger.warn(`IP ${ip} 超过速率限制,断开连接..`);
stream.destroy();
res.end();
}
});
const throttle = new Throttle({ rate: 100 * 1024 });
this.logger.log(`白嫖的入限速!(${ip}) => ${memePath}`);
stream.pipe(throttle).pipe(res);
}
} catch (e) { } catch (e) {
this.logger.error(`获取表情包失败: ${e.message}`); this.logger.error(`获取表情包失败:${e.message}`);
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
} }
} }

View File

@ -3,9 +3,11 @@ import { MemeService } from './meme.service';
import { MemeController } from './meme.controller'; import { MemeController } from './meme.controller';
import { PathModule } from '../../core/path/path.module'; import { PathModule } from '../../core/path/path.module';
import { AutoUpdateModule } from '../../core/auto-update/auto-update.module'; import { AutoUpdateModule } from '../../core/auto-update/auto-update.module';
import { ToolsModule } from '../../core/tools/tools.module';
import { RedisModule } from '../../core/redis/redis.module';
@Module({ @Module({
imports: [PathModule, AutoUpdateModule], imports: [PathModule, AutoUpdateModule, ToolsModule, RedisModule],
providers: [MemeService], providers: [MemeService],
controllers: [MemeController], controllers: [MemeController],
}) })

View File

@ -21,6 +21,7 @@ export class MemeService {
private startAutoUpdate() { private startAutoUpdate() {
setInterval(async () => { setInterval(async () => {
const memePath = this.pathService.get('meme'); const memePath = this.pathService.get('meme');
//const memePath = path.join(this.pathService.get('meme'),'..'); TODO 需确认检查src更新是否影响正常运行
this.logger.log('定时检查表情仓库更新..'); this.logger.log('定时检查表情仓库更新..');
const updated = await this.autoUpdateService.checkRepoForUpdates( const updated = await this.autoUpdateService.checkRepoForUpdates(
memePath, memePath,

View File

@ -31,6 +31,7 @@ export class WordsService {
private startAutoUpdate() { private startAutoUpdate() {
setInterval(async () => { setInterval(async () => {
//const wordsPath = path.join(this.paths.get('words'),'..'); TODO 需确认检查src更新是否影响正常运行
const wordsPath = this.paths.get('words'); const wordsPath = this.paths.get('words');
this.logger.log('定时检查文案仓库更新..'); this.logger.log('定时检查文案仓库更新..');
const updated = await this.autoUpdateService.checkRepoForUpdates( const updated = await this.autoUpdateService.checkRepoForUpdates(
@ -38,7 +39,7 @@ export class WordsService {
'words 仓库', 'words 仓库',
); );
if (updated) { if (updated) {
this.logger.log('文案仓库已更新清理缓存..'); this.logger.log('文案仓库已更新,清理缓存..');
this.wordCache = {}; this.wordCache = {};
} }
}, this.updateMs); }, this.updateMs);

View File

@ -1,6 +1,6 @@
while true; do while true; do
echo "启动服务..." echo "启动核心.."
pnpm start pnpm start
echo "服务退出5秒后重启..." echo "核心退出,5秒后重启.."
sleep 5 sleep 5
done done