mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
644060ca25 | ||
![]() |
02038483d4 | ||
![]() |
417c025dbf | ||
![]() |
2852996f18 | ||
![]() |
1a257e03fc | ||
![]() |
7b89e1fcb0 | ||
![]() |
c126820d40 | ||
![]() |
e316657cd7 | ||
![]() |
d8a9413627 | ||
![]() |
5c98623f2b | ||
![]() |
d6485b220e | ||
![]() |
c19c106266 | ||
![]() |
ea3d069e49 | ||
![]() |
3e6024f183 | ||
![]() |
337871693a | ||
![]() |
2d921c4577 | ||
![]() |
9accff7323 | ||
![]() |
88b1ee8c31 | ||
![]() |
3ac618bb4e | ||
![]() |
0051df3741 | ||
![]() |
7eb4e010b0 | ||
![]() |
33cc23ada3 | ||
![]() |
e5aee372e3 | ||
![]() |
6b6ce4a761 | ||
![]() |
8c4ea7f8f2 | ||
![]() |
c8b268b806 | ||
![]() |
cf5e0e0f14 | ||
![]() |
7b79f9cc17 | ||
![]() |
708d599966 | ||
![]() |
1ecd5b78e6 | ||
![]() |
fca2e3c51a | ||
![]() |
95ea761b2d | ||
![]() |
6b3bfa1ee9 | ||
![]() |
cea900ca2a | ||
![]() |
df3e302a9d | ||
![]() |
c88a68c9a8 | ||
![]() |
92d01b9cdd |
115
.vscode/launch.json
vendored
Normal file
115
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "dev:shell",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev:shell"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "build:shell",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"build:shell"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "build:universal",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"build:universal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "build:framework",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"build:framework"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "build:webui",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"build:webui"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "dev:universal",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev:universal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "dev:framework",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev:framework"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "dev:webui",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev:webui"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "lint",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"lint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "depend",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"depend"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "dev:depend",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev:depend"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.5.21",
|
||||
"version": "4.6.0",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
BIN
napcat.webui/public/fonts/NotoSerifSC-VariableFont_wght.ttf
Normal file
BIN
napcat.webui/public/fonts/NotoSerifSC-VariableFont_wght.ttf
Normal file
Binary file not shown.
@@ -1,19 +1,21 @@
|
||||
import { PlayMode } from '@/const/enum'
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
import type {
|
||||
FinalMusic,
|
||||
Music163ListResponse,
|
||||
Music163URLResponse
|
||||
} from '@/types/music'
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
/**
|
||||
* 获取网易云音乐歌单
|
||||
* @param id 歌单id
|
||||
* @returns 歌单信息
|
||||
*/
|
||||
export const get163MusicList = async (id: string) => {
|
||||
let res = await WebUIManager.proxy<Music163ListResponse>('https://wavesgame.top/playlist/track/all?id=' + id);
|
||||
let res = await WebUIManager.proxy<Music163ListResponse>(
|
||||
'https://wavesgame.top/playlist/track/all?id=' + id
|
||||
)
|
||||
// const res = await request.get<Music163ListResponse>(
|
||||
// `https://wavesgame.top/playlist/track/all?id=${id}`
|
||||
// )
|
||||
@@ -71,7 +73,7 @@ export const get163MusicListSongs = async (id: string) => {
|
||||
if (songURL) {
|
||||
finalMusic.push({
|
||||
id: song.id,
|
||||
url: songURL,
|
||||
url: songURL.replace(/http:\/\//, '//').replace(/https:\/\//, '//'),
|
||||
title: song.name,
|
||||
artist: song.ar.map((p) => p.name).join('/'),
|
||||
cover: song.al.picUrl
|
||||
|
11
package.json
11
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.5.21",
|
||||
"version": "4.6.0",
|
||||
"scripts": {
|
||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||
@@ -42,7 +42,6 @@
|
||||
"ajv": "^8.13.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"commander": "^13.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"esbuild": "0.25.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
@@ -59,13 +58,15 @@
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-tsconfig-paths": "^5.1.0",
|
||||
"winston": "^3.17.0"
|
||||
"napcat.protobuf": "^1.1.3",
|
||||
"winston": "^3.17.0",
|
||||
"compressing": "^1.10.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg.wasm/core-mt": "^0.13.2",
|
||||
"compressing": "^1.10.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.0.0",
|
||||
"piscina": "^4.7.0",
|
||||
"openai": "^4.85.4",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
|
@@ -1,9 +1,20 @@
|
||||
import { encode } from 'silk-wasm';
|
||||
import { parentPort } from 'worker_threads';
|
||||
|
||||
export interface EncodeArgs {
|
||||
input: ArrayBufferView | ArrayBuffer
|
||||
sampleRate: number
|
||||
}
|
||||
export default async ({ input, sampleRate }: EncodeArgs) => {
|
||||
export function recvTask<T>(cb: (taskData: T) => Promise<unknown>) {
|
||||
parentPort?.on('message', async (taskData: T) => {
|
||||
try {
|
||||
let ret = await cb(taskData);
|
||||
parentPort?.postMessage(ret);
|
||||
} catch (error: unknown) {
|
||||
parentPort?.postMessage({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
}
|
||||
recvTask<EncodeArgs>(async ({ input, sampleRate }) => {
|
||||
return await encode(input, sampleRate);
|
||||
};
|
||||
});
|
@@ -1,4 +1,3 @@
|
||||
import Piscina from 'piscina';
|
||||
import fsPromise from 'fs/promises';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'crypto';
|
||||
@@ -6,16 +5,16 @@ import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-w
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { EncodeArgs } from '@/common/audio-worker';
|
||||
import { FFmpegService } from '@/common/ffmpeg';
|
||||
import { runTask } from './worker';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
|
||||
|
||||
async function getWorkerPath() {
|
||||
return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
|
||||
function getWorkerPath() {
|
||||
//return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
|
||||
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'audio-worker.mjs');
|
||||
}
|
||||
|
||||
const piscina = new Piscina<EncodeArgs, EncodeResult>({
|
||||
filename: await getWorkerPath(),
|
||||
});
|
||||
|
||||
async function guessDuration(pttPath: string, logger: LogWrapper) {
|
||||
const pttFileInfo = await fsPromise.stat(pttPath);
|
||||
@@ -46,7 +45,7 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
|
||||
const { input, sampleRate } = isWav(file)
|
||||
? await handleWavFile(file, filePath, pcmPath)
|
||||
: { input: await FFmpegService.convert(filePath, pcmPath), sampleRate: 24000 };
|
||||
const silk = await piscina.run({ input: input, sampleRate: sampleRate });
|
||||
const silk = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { input: input, sampleRate: sampleRate });
|
||||
fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e));
|
||||
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
|
||||
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
|
||||
|
@@ -5,6 +5,17 @@ import { readFileSync, statSync, writeFileSync } from 'fs';
|
||||
import type { VideoInfo } from './video';
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import imageSize from 'image-size';
|
||||
import { parentPort } from 'worker_threads';
|
||||
export function recvTask<T>(cb: (taskData: T) => Promise<unknown>) {
|
||||
parentPort?.on('message', async (taskData: T) => {
|
||||
try {
|
||||
let ret = await cb(taskData);
|
||||
parentPort?.postMessage(ret);
|
||||
} catch (error: unknown) {
|
||||
parentPort?.postMessage({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
}
|
||||
class FFmpegService {
|
||||
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
|
||||
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
|
||||
@@ -137,15 +148,18 @@ interface FFmpegTask {
|
||||
}
|
||||
export default async function handleFFmpegTask({ method, args }: FFmpegTask): Promise<any> {
|
||||
switch (method) {
|
||||
case 'extractThumbnail':
|
||||
return await FFmpegService.extractThumbnail(...args as [string, string]);
|
||||
case 'convertFile':
|
||||
return await FFmpegService.convertFile(...args as [string, string, string]);
|
||||
case 'convert':
|
||||
return await FFmpegService.convert(...args as [string, string]);
|
||||
case 'getVideoInfo':
|
||||
return await FFmpegService.getVideoInfo(...args as [string, string]);
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`);
|
||||
case 'extractThumbnail':
|
||||
return await FFmpegService.extractThumbnail(...args as [string, string]);
|
||||
case 'convertFile':
|
||||
return await FFmpegService.convertFile(...args as [string, string, string]);
|
||||
case 'convert':
|
||||
return await FFmpegService.convert(...args as [string, string]);
|
||||
case 'getVideoInfo':
|
||||
return await FFmpegService.getVideoInfo(...args as [string, string]);
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
recvTask<FFmpegTask>(async ({ method, args }: FFmpegTask) => {
|
||||
return await handleFFmpegTask({ method, args });
|
||||
});
|
@@ -1,6 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import Piscina from 'piscina';
|
||||
import { VideoInfo } from './video';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { runTask } from './worker';
|
||||
|
||||
type EncodeArgs = {
|
||||
method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
|
||||
@@ -9,42 +11,26 @@ type EncodeArgs = {
|
||||
|
||||
type EncodeResult = any;
|
||||
|
||||
async function getWorkerPath() {
|
||||
return new URL(/* @vite-ignore */ './ffmpeg-worker.mjs', import.meta.url).href;
|
||||
function getWorkerPath() {
|
||||
return path.join(path.dirname(fileURLToPath(import.meta.url)), './ffmpeg-worker.mjs');
|
||||
}
|
||||
|
||||
export class FFmpegService {
|
||||
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
|
||||
const piscina = new Piscina<EncodeArgs, EncodeResult>({
|
||||
filename: await getWorkerPath(),
|
||||
});
|
||||
await piscina.run({ method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
|
||||
await piscina.destroy();
|
||||
await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
|
||||
}
|
||||
|
||||
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
|
||||
const piscina = new Piscina<EncodeArgs, EncodeResult>({
|
||||
filename: await getWorkerPath(),
|
||||
});
|
||||
await piscina.run({ method: 'convertFile', args: [inputFile, outputFile, format] });
|
||||
await piscina.destroy();
|
||||
await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'convertFile', args: [inputFile, outputFile, format] });
|
||||
}
|
||||
|
||||
public static async convert(filePath: string, pcmPath: string): Promise<Buffer> {
|
||||
const piscina = new Piscina<EncodeArgs, EncodeResult>({
|
||||
filename: await getWorkerPath(),
|
||||
});
|
||||
const result = await piscina.run({ method: 'convert', args: [filePath, pcmPath] });
|
||||
await piscina.destroy();
|
||||
const result = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'convert', args: [filePath, pcmPath] });
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
|
||||
const piscina = new Piscina<EncodeArgs, EncodeResult>({
|
||||
filename: await getWorkerPath(),
|
||||
});
|
||||
const result = await piscina.run({ method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
|
||||
await piscina.destroy();
|
||||
const result = await await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@@ -232,7 +232,7 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
||||
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
|
||||
}
|
||||
if (msg.senderUin !== '0') {
|
||||
tokens.push(`[${msg.sendMemberName ?? msg.sendRemarkName ?? msg.sendNickName}(${msg.senderUin})]`);
|
||||
tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
|
||||
}
|
||||
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
|
||||
tokens.push('移动设备');
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.5.21';
|
||||
export const napCatVersion = '4.6.0';
|
||||
|
29
src/common/worker.ts
Normal file
29
src/common/worker.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Worker } from 'worker_threads';
|
||||
|
||||
export async function runTask<T, R>(workerScript: string, taskData: T): Promise<R> {
|
||||
let worker = new Worker(workerScript);
|
||||
try {
|
||||
return await new Promise<R>((resolve, reject) => {
|
||||
worker.on('message', (result: R) => {
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
worker.on('error', (error) => {
|
||||
reject(new Error(`Worker error: ${error.message}`));
|
||||
});
|
||||
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Worker stopped with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
worker.postMessage(taskData);
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
throw new Error(`Failed to run task: ${(error as Error).message}`);
|
||||
} finally {
|
||||
// Ensure the worker is terminated after the promise is settled
|
||||
worker.terminate();
|
||||
}
|
||||
}
|
||||
|
@@ -41,7 +41,8 @@ export class NTQQFileApi {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
this.rkeyManager = new RkeyManager([
|
||||
'https://rkey.napneko.icu/rkeys'
|
||||
'https://ss.xingzhige.com/music_card/rkey', // 国内
|
||||
'https://rkey.napneko.icu/rkeys' // Cloudflare
|
||||
],
|
||||
this.context.logger
|
||||
);
|
||||
|
@@ -165,7 +165,13 @@ export class NTQQGroupApi {
|
||||
|
||||
return this.groupMemberCache.get(groupCode);
|
||||
}
|
||||
|
||||
async refreshGroupMemberCachePartial(groupCode: string, uid: string) {
|
||||
const member = await this.getGroupMemberEx(groupCode, uid, true);
|
||||
if (member) {
|
||||
this.groupMemberCache.get(groupCode)?.set(uid, member);
|
||||
}
|
||||
return member;
|
||||
}
|
||||
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
|
||||
const groupCodeStr = groupCode.toString();
|
||||
const memberUinOrUidStr = memberUinOrUid.toString();
|
||||
|
@@ -12,7 +12,7 @@ export class NTQQMsgApi {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
|
||||
async clickInlineKeyboardButton(...params: Parameters<NodeIKernelMsgService['clickInlineKeyboardButton']>) {
|
||||
return this.context.session.getMsgService().clickInlineKeyboardButton(...params);
|
||||
}
|
||||
@@ -136,6 +136,20 @@ export class NTQQMsgApi {
|
||||
});
|
||||
}
|
||||
|
||||
async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) {
|
||||
console.log(peer, SendersUid);
|
||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
|
||||
chatInfo: peer,
|
||||
filterMsgType: [],
|
||||
filterSendersUid: SendersUid,
|
||||
filterMsgToTime: '0',
|
||||
filterMsgFromTime: '0',
|
||||
isReverseOrder: true,
|
||||
isIncludeCurrent: true,
|
||||
pageLimit: 20000,
|
||||
});
|
||||
}
|
||||
|
||||
async setMsgRead(peer: Peer) {
|
||||
return this.context.session.getMsgService().setMsgRead(peer);
|
||||
}
|
||||
|
2
src/core/external/offset.json
vendored
2
src/core/external/offset.json
vendored
@@ -175,7 +175,7 @@
|
||||
"send": "713A318",
|
||||
"recv": "713DB50"
|
||||
},
|
||||
"6.9.63.30851-x64": {
|
||||
"6.9.63-30851-x64": {
|
||||
"send": "46C8040",
|
||||
"recv": "46CA8AC"
|
||||
},
|
||||
|
@@ -1,22 +1,22 @@
|
||||
import * as crypto from 'crypto';
|
||||
import {PacketContext} from '@/core/packet/context/packetContext';
|
||||
import { PacketContext } from '@/core/packet/context/packetContext';
|
||||
import * as trans from '@/core/packet/transformer';
|
||||
import {PacketMsg} from '@/core/packet/message/message';
|
||||
import { PacketMsg } from '@/core/packet/message/message';
|
||||
import {
|
||||
PacketMsgFileElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgVideoElement
|
||||
} from '@/core/packet/message/element';
|
||||
import {ChatType, MsgSourceType, NTMsgType, RawMessage} from '@/core';
|
||||
import {MiniAppRawData, MiniAppReqParams} from '@/core/packet/entities/miniApp';
|
||||
import {AIVoiceChatType} from '@/core/packet/entities/aiChat';
|
||||
import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core';
|
||||
import {IndexNode, LongMsgResult, MsgInfo} from '@/core/packet/transformer/proto';
|
||||
import {OidbPacket} from '@/core/packet/transformer/base';
|
||||
import {ImageOcrResult} from '@/core/packet/entities/ocrResult';
|
||||
import {gunzipSync} from 'zlib';
|
||||
import {PacketMsgConverter} from '@/core/packet/message/converter';
|
||||
import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
|
||||
import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp';
|
||||
import { AIVoiceChatType } from '@/core/packet/entities/aiChat';
|
||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { IndexNode, LongMsgResult, MsgInfo } from '@/core/packet/transformer/proto';
|
||||
import { OidbPacket } from '@/core/packet/transformer/base';
|
||||
import { ImageOcrResult } from '@/core/packet/entities/ocrResult';
|
||||
import { gunzipSync } from 'zlib';
|
||||
import { PacketMsgConverter } from '@/core/packet/message/converter';
|
||||
|
||||
export class PacketOperationContext {
|
||||
private readonly context: PacketContext;
|
||||
@@ -59,10 +59,10 @@ export class PacketOperationContext {
|
||||
const res = trans.GetStrangerInfo.parse(resp);
|
||||
const extBigInt = BigInt(res.data.status.value);
|
||||
if (extBigInt <= 10n) {
|
||||
return {status: Number(extBigInt) * 10, ext_status: 0};
|
||||
return { status: Number(extBigInt) * 10, ext_status: 0 };
|
||||
}
|
||||
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn));
|
||||
return {status: 10, ext_status: status};
|
||||
return { status: 10, ext_status: status };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
@@ -79,13 +79,13 @@ export class PacketOperationContext {
|
||||
const reqList = msg.flatMap(m =>
|
||||
m.msg.map(e => {
|
||||
if (e instanceof PacketMsgPicElement) {
|
||||
return this.context.highway.uploadImage({chatType, peerUid}, e);
|
||||
return this.context.highway.uploadImage({ chatType, peerUid }, e);
|
||||
} else if (e instanceof PacketMsgVideoElement) {
|
||||
return this.context.highway.uploadVideo({chatType, peerUid}, e);
|
||||
return this.context.highway.uploadVideo({ chatType, peerUid }, e);
|
||||
} else if (e instanceof PacketMsgPttElement) {
|
||||
return this.context.highway.uploadPtt({chatType, peerUid}, e);
|
||||
return this.context.highway.uploadPtt({ chatType, peerUid }, e);
|
||||
} else if (e instanceof PacketMsgFileElement) {
|
||||
return this.context.highway.uploadFile({chatType, peerUid}, e);
|
||||
return this.context.highway.uploadFile({ chatType, peerUid }, e);
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean)
|
||||
@@ -160,6 +160,12 @@ export class PacketOperationContext {
|
||||
const res = trans.DownloadGroupFile.parse(resp);
|
||||
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
|
||||
}
|
||||
async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string) {
|
||||
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const res = trans.DownloadPrivateFile.parse(resp);
|
||||
return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`;
|
||||
}
|
||||
|
||||
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||
const req = trans.DownloadGroupPtt.build(groupUin, node);
|
||||
|
@@ -144,7 +144,7 @@ export class PacketHighwayContext {
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
||||
@@ -181,7 +181,7 @@ export class PacketHighwayContext {
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
||||
@@ -219,7 +219,7 @@ export class PacketHighwayContext {
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
@@ -244,16 +244,16 @@ export class PacketHighwayContext {
|
||||
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
const subFile = preRespData.upload.subFileInfos[0];
|
||||
if (subFile.uKey && subFile.uKey != '') {
|
||||
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
|
||||
if (subFile!.uKey && subFile!.uKey != '') {
|
||||
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile!.uKey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[1]!.index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
uKey: subFile.uKey,
|
||||
uKey: subFile!.uKey,
|
||||
network: {
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S)
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(subFile!.ipv4S)
|
||||
},
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
@@ -269,7 +269,7 @@ export class PacketHighwayContext {
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
|
||||
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile!.uKey}, don't need upload!`);
|
||||
}
|
||||
video.msgInfo = preRespData.upload.msgInfo;
|
||||
}
|
||||
@@ -284,7 +284,7 @@ export class PacketHighwayContext {
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
@@ -309,16 +309,16 @@ export class PacketHighwayContext {
|
||||
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
const subFile = preRespData.upload.subFileInfos[0];
|
||||
if (subFile.uKey && subFile.uKey != '') {
|
||||
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
|
||||
if (subFile!.uKey && subFile!.uKey != '') {
|
||||
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile!.uKey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[1]!.index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
uKey: subFile.uKey,
|
||||
uKey: subFile!.uKey,
|
||||
network: {
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S)
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(subFile!.ipv4S)
|
||||
},
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
@@ -334,7 +334,7 @@ export class PacketHighwayContext {
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
|
||||
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile!.uKey}, don't need upload!`);
|
||||
}
|
||||
video.msgInfo = preRespData.upload.msgInfo;
|
||||
}
|
||||
@@ -347,7 +347,7 @@ export class PacketHighwayContext {
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadGroupPttReq get upload ptt ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
||||
@@ -383,7 +383,7 @@ export class PacketHighwayContext {
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != '') {
|
||||
this.logger.debug(`[Highway] uploadC2CPttReq get upload ptt ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
||||
|
@@ -465,14 +465,14 @@ export interface NodeIKernelMsgService {
|
||||
setMsgEmojiLikesForRole(...args: unknown[]): unknown;
|
||||
|
||||
clickInlineKeyboardButton(params: {
|
||||
guildId: string,
|
||||
guildId?: string,
|
||||
peerId: string,
|
||||
botAppid: string,
|
||||
msgSeq: string,
|
||||
buttonId: string,
|
||||
callback_data: string,
|
||||
dmFlag: number,
|
||||
chatType: number
|
||||
chatType: number // 1私聊 2群
|
||||
}): Promise<GeneralCallResult & { status: number, promptText: string, promptType: number, promptIcon: number }>;
|
||||
|
||||
setCurOnScreenMsg(...args: unknown[]): unknown;
|
||||
|
@@ -7,6 +7,7 @@ const SchemaData = Type.Object({
|
||||
bot_appid: Type.String(),
|
||||
button_id: Type.String({ default: '' }),
|
||||
callback_data: Type.String({ default: '' }),
|
||||
msg_seq: Type.String({ default: '10086' }),
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
@@ -18,13 +19,12 @@ export class ClickInlineKeyboardButton extends OneBotAction<Payload, unknown> {
|
||||
async _handle(payload: Payload) {
|
||||
return await this.core.apis.MsgApi.clickInlineKeyboardButton({
|
||||
buttonId: payload.button_id,
|
||||
guildId: '',// 频道使用
|
||||
peerId: payload.group_id.toString(),
|
||||
botAppid: payload.bot_appid,
|
||||
msgSeq: '10086',
|
||||
msgSeq: payload.msg_seq,
|
||||
callback_data: payload.callback_data,
|
||||
dmFlag: 0,
|
||||
chatType: 1
|
||||
chatType: 2
|
||||
})
|
||||
}
|
||||
}
|
||||
|
56
src/onebot/action/extends/GetUnidirectionalFriendList.ts
Normal file
56
src/onebot/action/extends/GetUnidirectionalFriendList.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { PacketHexStr } from '@/core/packet/transformer/base';
|
||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { ProtoBuf, ProtoBufBase, PBUint32, PBString } from 'napcat.protobuf';
|
||||
|
||||
interface Friend {
|
||||
uin: number;
|
||||
uid: string;
|
||||
nick_name: string;
|
||||
age: number;
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface Block {
|
||||
str_uid: string;
|
||||
bytes_source: string;
|
||||
uint32_sex: number;
|
||||
uint32_age: number;
|
||||
bytes_nick: string;
|
||||
uint64_uin: number;
|
||||
}
|
||||
|
||||
export class GetUnidirectionalFriendList extends OneBotAction<void, Friend[]> {
|
||||
override actionName = ActionName.GetUnidirectionalFriendList;
|
||||
|
||||
async pack_data(data: string): Promise<Uint8Array> {
|
||||
return ProtoBuf(class extends ProtoBufBase {
|
||||
type = PBUint32(2, false, 0);
|
||||
data = PBString(3, false, data);
|
||||
}).encode();
|
||||
}
|
||||
|
||||
async _handle(): Promise<Friend[]> {
|
||||
const self_id = this.core.selfInfo.uin;
|
||||
const req_json = {
|
||||
uint64_uin: self_id,
|
||||
uint64_top: 0,
|
||||
uint32_req_num: 99,
|
||||
bytes_cookies: ""
|
||||
};
|
||||
const packed_data = await this.pack_data(JSON.stringify(req_json));
|
||||
const data = Buffer.from(packed_data).toString('hex');
|
||||
const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as PacketHexStr };
|
||||
const rsp_data = await this.core.apis.PacketApi.pkt.operation.sendPacket(rsq, true);
|
||||
const block_json = ProtoBuf(class extends ProtoBufBase { data = PBString(4); }).decode(rsp_data);
|
||||
const block_list: Block[] = JSON.parse(block_json.data).rpt_block_list;
|
||||
|
||||
return block_list.map((block) => ({
|
||||
uin: block.uint64_uin,
|
||||
uid: block.str_uid,
|
||||
nick_name: Buffer.from(block.bytes_nick, 'base64').toString(),
|
||||
age: block.uint32_age,
|
||||
source: Buffer.from(block.bytes_source, 'base64').toString()
|
||||
}));
|
||||
}
|
||||
}
|
36
src/onebot/action/file/GetPrivateFileUrl.ts
Normal file
36
src/onebot/action/file/GetPrivateFileUrl.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
file_id: Type.String(),
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
interface GetPrivateFileUrlResponse {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export class GetPrivateFileUrl extends GetPacketStatusDepends<Payload, GetPrivateFileUrlResponse> {
|
||||
override actionName = ActionName.NapCat_GetPrivateFileUrl;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id);
|
||||
|
||||
if (contextMsgFile?.fileUUID && contextMsgFile.msgId) {
|
||||
let msg = await this.core.apis.MsgApi.getMsgsByMsgId(contextMsgFile.peer, [contextMsgFile.msgId]);
|
||||
let self_id = this.core.selfInfo.uid;
|
||||
let file_hash = msg.msgList[0]?.elements.map(ele => ele.fileElement?.file10MMd5)[0];
|
||||
if (file_hash) {
|
||||
return {
|
||||
url: await this.core.apis.PacketApi.pkt.operation.GetPrivateFileUrl(self_id, contextMsgFile.fileUUID, file_hash)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
throw new Error('real fileUUID not found!');
|
||||
}
|
||||
}
|
@@ -106,6 +106,8 @@ import { SendPoke } from '@/onebot/action/packet/SendPoke';
|
||||
import { SetDiyOnlineStatus } from './extends/SetDiyOnlineStatus';
|
||||
import { BotExit } from './extends/BotExit';
|
||||
import { ClickInlineKeyboardButton } from './extends/ClickInlineKeyboardButton';
|
||||
import { GetPrivateFileUrl } from './file/GetPrivateFileUrl';
|
||||
import { GetUnidirectionalFriendList } from './extends/GetUnidirectionalFriendList';
|
||||
|
||||
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
|
||||
|
||||
@@ -225,6 +227,8 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
||||
new GetGroupSystemMsg(obContext, core),
|
||||
new BotExit(obContext, core),
|
||||
new ClickInlineKeyboardButton(obContext, core),
|
||||
new GetPrivateFileUrl(obContext,core),
|
||||
new GetUnidirectionalFriendList(obContext,core),
|
||||
];
|
||||
|
||||
type HandlerUnion = typeof actionHandlers[number];
|
||||
|
@@ -10,6 +10,7 @@ export interface InvalidCheckResult {
|
||||
}
|
||||
|
||||
export const ActionName = {
|
||||
NapCat_GetPrivateFileUrl: 'get_private_file_url',
|
||||
ClickInlineKeyboardButton: 'click_inline_keyboard_button',
|
||||
GetUnidirectionalFriendList: 'get_unidirectional_friend_list',
|
||||
// onebot 11
|
||||
|
@@ -49,6 +49,7 @@ export class OneBotGroupApi {
|
||||
duration = -1;
|
||||
}
|
||||
}
|
||||
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(GroupCode, memberUid);
|
||||
const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, adminUid))?.uin;
|
||||
if (memberUin && adminUin) {
|
||||
return new OB11GroupBanEvent(
|
||||
@@ -113,12 +114,16 @@ export class OneBotGroupApi {
|
||||
async parseCardChangedEvent(msg: RawMessage) {
|
||||
if (msg.senderUin && msg.senderUin !== '0') {
|
||||
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin);
|
||||
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
|
||||
if (member && member.cardName !== msg.sendMemberName) {
|
||||
const newCardName = msg.sendMemberName ?? '';
|
||||
const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName);
|
||||
member.cardName = newCardName;
|
||||
return event;
|
||||
}
|
||||
if (member && member.nick !== msg.sendNickName) {
|
||||
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -132,7 +132,6 @@ export class OneBotMsgApi {
|
||||
file: element.fileName,
|
||||
sub_type: element.picSubType,
|
||||
url: await this.core.apis.FileApi.getImageUrl(element),
|
||||
path: element.filePath,
|
||||
file_size: element.fileSize,
|
||||
},
|
||||
};
|
||||
@@ -148,13 +147,13 @@ export class OneBotMsgApi {
|
||||
peerUid: msg.peerUid,
|
||||
guildId: '',
|
||||
};
|
||||
const file = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
|
||||
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileUuid);
|
||||
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
|
||||
return {
|
||||
type: OB11MessageDataType.file,
|
||||
data: {
|
||||
file: file,
|
||||
path: element.filePath,
|
||||
file_id: file,
|
||||
file: element.fileName,
|
||||
file_id: element.fileUuid,
|
||||
file_size: element.fileSize,
|
||||
},
|
||||
};
|
||||
@@ -216,7 +215,6 @@ export class OneBotMsgApi {
|
||||
data: {
|
||||
summary: _.faceName, // 商城表情名称
|
||||
file: filename,
|
||||
path: url,
|
||||
url: url,
|
||||
key: _.key,
|
||||
emoji_id: _.emojiId,
|
||||
@@ -339,7 +337,6 @@ export class OneBotMsgApi {
|
||||
type: OB11MessageDataType.video,
|
||||
data: {
|
||||
file: fileCode,
|
||||
path: videoDownUrl,
|
||||
url: videoDownUrl,
|
||||
file_size: element.fileSize,
|
||||
},
|
||||
@@ -357,7 +354,6 @@ export class OneBotMsgApi {
|
||||
type: OB11MessageDataType.voice,
|
||||
data: {
|
||||
file: fileCode,
|
||||
path: element.filePath,
|
||||
file_size: element.fileSize,
|
||||
},
|
||||
};
|
||||
@@ -658,6 +654,19 @@ export class OneBotMsgApi {
|
||||
[OB11MessageDataType.node]: async () => undefined,
|
||||
|
||||
[OB11MessageDataType.forward]: async ({ data }, context) => {
|
||||
// let id = data.id.toString();
|
||||
// let peer: Peer | undefined = context.peer;
|
||||
// if (isNumeric(id)) {
|
||||
// let msgid = '';
|
||||
// if (BigInt(data.id) > 2147483647n) {
|
||||
// peer = MessageUnique.getPeerByMsgId(id)?.Peer;
|
||||
// msgid = id;
|
||||
// } else {
|
||||
// let data = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
|
||||
// msgid = data?.MsgId ?? '';
|
||||
// peer = data?.Peer;
|
||||
// }
|
||||
// }
|
||||
const jsonData = ForwardMsgBuilder.fromResId(data.id);
|
||||
return this.ob11ToRawConverters.json({
|
||||
data: { data: JSON.stringify(jsonData) },
|
||||
|
@@ -3,7 +3,7 @@ import { NapCatCore } from '@/core';
|
||||
|
||||
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'group_admin';
|
||||
sub_type: 'set' | 'unset';
|
||||
sub_type: 'set' | 'unset';
|
||||
|
||||
constructor(core: NapCatCore, group_id: number, user_id: number, sub_type: 'set' | 'unset') {
|
||||
super(core, group_id, user_id);
|
||||
|
@@ -50,6 +50,7 @@ import {
|
||||
import { OB11Message } from './types';
|
||||
import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
|
||||
import { OB11HttpSSEServerAdapter } from './network/http-server-sse';
|
||||
import { OB11PluginAdapter } from './network/plugin';
|
||||
|
||||
//OneBot实现类
|
||||
export class NapCatOneBot11Adapter {
|
||||
@@ -113,9 +114,9 @@ export class NapCatOneBot11Adapter {
|
||||
//创建NetWork服务
|
||||
|
||||
// 注册Plugin 如果需要基于NapCat进行快速开发
|
||||
// this.networkManager.registerAdapter(
|
||||
// new OB11PluginAdapter('myPlugin', this.core, this,this.actions)
|
||||
// );
|
||||
this.networkManager.registerAdapter(
|
||||
new OB11PluginAdapter('myPlugin', this.core, this,this.actions)
|
||||
);
|
||||
for (const key of ob11Config.network.httpServers) {
|
||||
if (key.enable) {
|
||||
this.networkManager.registerAdapter(
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
|
||||
import { NapCatOneBot11Adapter, OB11Message } from '@/onebot';
|
||||
import { NapCatOneBot11Adapter, OB11ArrayMessage } from '@/onebot';
|
||||
import { NapCatCore } from '@/core';
|
||||
import { PluginConfig } from '../config/config';
|
||||
import { plugin_onmessage } from '@/plugin';
|
||||
@@ -15,14 +15,14 @@ export class OB11PluginAdapter extends IOB11NetworkAdapter<PluginConfig> {
|
||||
messagePostFormat: 'array',
|
||||
reportSelfMessage: false,
|
||||
enable: true,
|
||||
debug: false,
|
||||
debug: true,
|
||||
};
|
||||
super(name, config, core, obContext, actions);
|
||||
}
|
||||
|
||||
onEvent<T extends OB11EmitEventContent>(event: T) {
|
||||
if (event.post_type === 'message') {
|
||||
plugin_onmessage(this.config.name, this.core, this.obContext, event as OB11Message, this.actions, this).then().catch();
|
||||
plugin_onmessage(this.config.name, this.core, this.obContext, event as OB11ArrayMessage, this.actions, this).then().catch();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -30,6 +30,10 @@ export interface OB11Message {
|
||||
post_type?: EventType;
|
||||
raw?: RawMessage;
|
||||
}
|
||||
export interface OB11ArrayMessage extends OB11Message {
|
||||
message_format: 'array';
|
||||
message: OB11MessageData[];
|
||||
}
|
||||
|
||||
// 合并转发消息接口定义
|
||||
export interface OB11ForwardMessage extends OB11Message {
|
||||
|
57
src/plugin/chathot.ts
Normal file
57
src/plugin/chathot.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Mutex } from "async-mutex";
|
||||
|
||||
export class ChatHotManager {
|
||||
// 存储群组的热度信息,键为群组ID,值为使用时间和使用计数
|
||||
private chatHot: Map<string, { usetime: number, usecount: number }> = new Map();
|
||||
// 互斥锁,确保热度信息的读写操作是安全的
|
||||
private chatHotMutex = new Mutex();
|
||||
|
||||
/**
|
||||
* 获取群组是否需要回复
|
||||
* @param groupId 群组ID
|
||||
* @returns 是否需要回复
|
||||
*/
|
||||
async getHot(groupId: string): Promise<boolean> {
|
||||
return await this.chatHotMutex.runExclusive(async () => {
|
||||
const chatHotData = this.chatHot.get(groupId);
|
||||
const currentTime = Date.now();
|
||||
if (chatHotData) {
|
||||
console.log("原始热度", chatHotData?.usecount, currentTime - chatHotData.usetime > 30000);
|
||||
if (currentTime - chatHotData.usetime > 30000) {
|
||||
chatHotData.usetime = currentTime;
|
||||
chatHotData.usecount = 0;
|
||||
this.chatHot.set(groupId, chatHotData);
|
||||
// 超出时间段重置计数
|
||||
return false;
|
||||
} else if (currentTime - chatHotData.usetime < 30000 && chatHotData.usecount > 0 && chatHotData.usecount < 2) {
|
||||
// 在短时间内没请求,回复
|
||||
return true;
|
||||
}
|
||||
// 在时间段内有请求,回复
|
||||
return false;
|
||||
}
|
||||
// 初始化,不回复
|
||||
this.chatHot.set(groupId, { usetime: currentTime, usecount: 0 });
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加群组的热度计数
|
||||
* @param groupId 群组ID
|
||||
*/
|
||||
async incrementHot(groupId: string) {
|
||||
await this.chatHotMutex.runExclusive(() => {
|
||||
const chatHotData = this.chatHot.get(groupId);
|
||||
const currentTime = Date.now();
|
||||
if (chatHotData) {
|
||||
// 引用增加
|
||||
chatHotData.usecount += 1;
|
||||
this.chatHot.set(groupId, chatHotData);
|
||||
} else {
|
||||
// 初始化
|
||||
this.chatHot.set(groupId, { usetime: currentTime, usecount: 1 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
20
src/plugin/config.ts
Normal file
20
src/plugin/config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const PROMPT_MEMROY = `
|
||||
你是合并、更新和组织记忆的专家。当提供现有记忆和新信息时,你的任务是合并和更新记忆列表,以反映最准确和最新的信息。你还会得到每个现有记忆与新信息的匹配分数。确保利用这些信息做出明智的决定,决定哪些记忆需要更新或合并。
|
||||
指南:
|
||||
- 消除重复的记忆,合并相关记忆,以确保列表简洁和更新。
|
||||
- 记忆根据人物区分,同时不必每次重复人物账号,只需在记忆中提及一次即可。
|
||||
- 如果一个记忆直接与新信息矛盾,请批判性地评估两条信息:
|
||||
- 如果新记忆提供了更近期或更准确的更新,用新记忆替换旧记忆。
|
||||
- 如果新记忆看起来不准确或细节较少,保留旧记忆并丢弃新记忆。
|
||||
- 注意区分对应人物的记忆和印象, 不要产生混淆人物的印象和记忆。
|
||||
- 在所有记忆中保持一致且清晰的风格,确保每个条目简洁而信息丰富。
|
||||
- 如果新记忆是现有记忆的变体或扩展,更新现有记忆以反映新信息。
|
||||
`;
|
||||
export const API_KEY = 'sk-xxxx';//需要配置
|
||||
export const BASE_URL = 'https://vip.bili2233.work/v1';
|
||||
export const MODEL = 'gemini-2.0-flash-thinking-exp';
|
||||
export const BOT_NAME = '千千';
|
||||
export const BOT_ADMIN = '1627126029';
|
||||
export const PROMPT = `你的名字叫千千`;
|
||||
export const CQCODE = `增加一下能力通过不同昵称和QQ进行区分哦,注意理清回复消息的人物, At人直接发送 [CQ:at,qq=1234] 这样可以直接at某个人喵这 回复消息需要发送[CQ:reply,id=xxx]这种格式叫CQ码,发送图片等操作你可以从聊天记录中学习哦, 如果聊天记录的image CQ码 maface类型你可以直接复制使用`;
|
||||
export const MEMORY_FILE = 'F:/Qian/memory.json';
|
9
src/plugin/helper.ts
Normal file
9
src/plugin/helper.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources";
|
||||
|
||||
export async function toSingleRole(msg: Array<any>) {
|
||||
let ret = { role: 'user', content: new Array<ChatCompletionContentPart>() };
|
||||
for (const m of msg) {
|
||||
ret.content.push(...m.content as any)
|
||||
}
|
||||
return [ret] as Array<ChatCompletionMessageParam>;
|
||||
}
|
@@ -1,11 +1,168 @@
|
||||
import { NapCatOneBot11Adapter, OB11Message } from '@/onebot';
|
||||
import { NapCatOneBot11Adapter, OB11ArrayMessage, OB11MessageData } from '@/onebot';
|
||||
import { NapCatCore } from '@/core';
|
||||
import { ActionMap } from '@/onebot/action';
|
||||
import { OB11PluginAdapter } from '@/onebot/network/plugin';
|
||||
import { OpenAI } from 'openai';
|
||||
import { ChatCompletionContentPart, ChatCompletionMessageParam } from 'openai/resources';
|
||||
import { MemoryManager } from './memory';
|
||||
import { ChatHotManager } from './chathot';
|
||||
import { API_KEY, BASE_URL, BOT_ADMIN, BOT_NAME, CQCODE, MODEL, PROMPT, PROMPT_MEMROY } from './config';
|
||||
import { toSingleRole } from './helper';
|
||||
|
||||
export const plugin_onmessage = async (adapter: string, _core: NapCatCore, _obCtx: NapCatOneBot11Adapter, message: OB11Message, action: ActionMap, instance: OB11PluginAdapter) => {
|
||||
if (message.raw_message === 'ping') {
|
||||
const ret = await action.get('send_group_msg')?.handle({ group_id: String(message.group_id), message: 'pong' }, adapter, instance.config);
|
||||
console.log(ret);
|
||||
const client = new OpenAI({ apiKey: API_KEY, baseURL: BASE_URL });
|
||||
const chatHotManager = new ChatHotManager();
|
||||
const memoryManager = new MemoryManager(mergeAndUpdateMemory);
|
||||
|
||||
async function createChatCompletionWithRetry(params: any, retries: number = 5): Promise<any> {
|
||||
for (let attempt = 0; attempt < retries; attempt++) {
|
||||
try {
|
||||
return await client.chat.completions.create(params);
|
||||
} catch (error) {
|
||||
console.error(`Ai会话 ${attempt + 1} failed:`, error);
|
||||
if (attempt === retries - 1) throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function messageToOpenAi(adapter: string, msg: OB11MessageData[], groupId: string, action: ActionMap, plugin: OB11PluginAdapter, message: OB11ArrayMessage) {
|
||||
const msgArray: Array<ChatCompletionContentPart> = [];
|
||||
let ret = '';
|
||||
for (const m of msg) {
|
||||
if (m.type === 'reply') {
|
||||
ret += `[CQ:reply,id=${m.data.id}]`;
|
||||
} else if (m.type === 'text') {
|
||||
ret += m.data.text;
|
||||
} else if (m.type === 'at') {
|
||||
const memberInfo = await action.get('get_group_member_info')
|
||||
?.handle({ group_id: groupId, user_id: m.data.qq }, adapter, plugin.config);
|
||||
ret += `[CQ:at=${m.data.qq},name=${memberInfo?.data?.nickname}]`;
|
||||
} else if (m.type === 'image') {
|
||||
ret += `[CQ:image,file=${m.data.url}]`;
|
||||
msgArray.push({ type: 'image_url', image_url: { url: m.data.url?.replace('https://', 'http://') || '' } });
|
||||
} else if (m.type === 'face') {
|
||||
ret += '[CQ:face,id=' + m.data.id + ']';
|
||||
}
|
||||
}
|
||||
msgArray.push({ type: 'text', text: `${message.sender.nickname}(${message.sender.user_id})发送了消息(消息id:${message.message_id}) :` + ret });
|
||||
return msgArray.reverse();
|
||||
}
|
||||
|
||||
async function mergeAndUpdateMemory(existingMemories: Array<ChatCompletionContentPart>[], newMemory: Array<ChatCompletionContentPart>[]): Promise<string> {
|
||||
const completion = await createChatCompletionWithRetry({
|
||||
messages: await toSingleRole([
|
||||
{ role: 'user', content: [{ type: 'text', text: PROMPT_MEMROY }] },
|
||||
{ role: 'user', content: [{ type: 'text', text: '接下来是旧记忆' }] },
|
||||
...(existingMemories.map(msg => ({ role: 'user', content: msg.filter(e => e.type === 'text') }))),
|
||||
{ role: 'user', content: [{ type: 'text', text: '接下来是新记忆' }] },
|
||||
...(newMemory.map(msg => ({ role: 'user', content: msg.filter(e => e.type === 'text') })))]),
|
||||
model: MODEL
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message.content || '';
|
||||
}
|
||||
|
||||
async function generateChatCompletion(contentData: Array<ChatCompletionMessageParam>): Promise<string> {
|
||||
const chatCompletion = await createChatCompletionWithRetry({ messages: contentData, model: MODEL });
|
||||
return chatCompletion.choices[0]?.message.content || '';
|
||||
}
|
||||
|
||||
async function handleClearMemoryCommand(groupId: string, type: 'short' | 'long', action: ActionMap, adapter: string, instance: OB11PluginAdapter) {
|
||||
await memoryManager.clearMemory(groupId, type);
|
||||
const message = type === 'short' ? '短期上下文已清理' : '长期上下文已清理';
|
||||
await sendGroupMessage(groupId, message, action, adapter, instance);
|
||||
}
|
||||
|
||||
async function sendGroupMessage(groupId: string, text: string, action: ActionMap, adapter: string, instance: OB11PluginAdapter) {
|
||||
return await action.get('send_group_msg')?.handle({ group_id: String(groupId), message: text }, adapter, instance.config);
|
||||
}
|
||||
|
||||
|
||||
async function prepareContentData(message: OB11ArrayMessage, msgArray: Array<ChatCompletionContentPart>, prompt: string, reply?: Array<ChatCompletionContentPart>) {
|
||||
const group_id = message.group_id?.toString()!;
|
||||
const longTermMemoryList = memoryManager.getLongTermMemory(group_id);
|
||||
let shortTermMemoryList = memoryManager.getShortTermMemory(group_id);
|
||||
let data = shortTermMemoryList.map(msg => ({ role: 'user' as const, content: msg.filter(e => e.type === 'text') }));
|
||||
return await toSingleRole([
|
||||
{ role: 'user', content: [{ type: 'text', text: prompt }] },
|
||||
{ role: 'user', content: [{ type: 'text', text: '接下来是长时间记忆' }] },
|
||||
{ role: 'user', content: [{ type: 'text', text: longTermMemoryList }] },
|
||||
{ role: 'user', content: [{ type: 'text', text: '接下来是短时间记忆' }] },
|
||||
...data,
|
||||
{ role: 'user', content: [{ type: 'text' as const, text: '接下来是本次引用消息' }] },
|
||||
...(reply ? [{ role: 'user' as const, content: reply }] : []),
|
||||
{ role: 'user', content: [{ type: 'text' as const, text: '接下来是当前对话' }] },
|
||||
{ role: 'user', content: msgArray }
|
||||
]);
|
||||
}
|
||||
|
||||
async function handleChatResponse(message: OB11ArrayMessage, msgArray: Array<ChatCompletionContentPart>, adapter: string, action: ActionMap, instance: OB11PluginAdapter, _core: NapCatCore, reply?: Array<ChatCompletionContentPart>) {
|
||||
const prompt = `请根据下面聊天内容,继续与 ${message?.sender?.card || message?.sender?.nickname} 进行对话。${CQCODE},注意回复内容只用输出内容,不要提及此段话,注意一定不要使用markdown,请采用纯文本回复。你的人设:${PROMPT}`;
|
||||
const contentData = await prepareContentData(message, msgArray, prompt, reply);
|
||||
const msgRet = await generateChatCompletion(contentData);
|
||||
const sentMsg = await sendGroupMessage(message.group_id?.toString()!, msgRet, action, adapter, instance);
|
||||
return { id: sentMsg?.data?.message_id, text: msgRet };
|
||||
}
|
||||
|
||||
async function shouldRespond(message: OB11ArrayMessage, core: NapCatCore, oriMsg: any, currentHot: boolean, msgArray: Array<ChatCompletionContentPart>, reply?: Array<ChatCompletionContentPart>): Promise<boolean> {
|
||||
if (
|
||||
!message.raw_message.startsWith(BOT_NAME) &&
|
||||
!message.message.find(e => e.type == 'at' && e.data.qq == core.selfInfo.uin) &&
|
||||
oriMsg?.sender.user_id.toString() !== core.selfInfo.uin
|
||||
) {
|
||||
console.log("聊天热度", currentHot ? '热度高' : '热度低');
|
||||
if (currentHot && msgArray.length > 0) {
|
||||
const prompt = `请根据在群内聊天与 ${message.sender.card || message.sender?.nickname} 发送的聊天消息推测本次消息是否应该回复。在上下文关系并非强相关的话题和图片不要随意回复,根据上下文非常明显需要时才进行回复,否则不回复,注意尤其减少对图片消息的回应可能性, 注意回复内容只用输出2 - 3个字, 一定注意不想回复请输出不回复三个字即可, 想回复输出回复即可,一定不要给出现任何多余的字, 你的人设:${PROMPT}`;
|
||||
const contentData = await prepareContentData(message, msgArray, prompt, reply);
|
||||
const msgRet = await generateChatCompletion(contentData);
|
||||
console.log('Ai回应判断:' + msgRet)
|
||||
if (msgRet.indexOf('不回复') !== -1) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleClearMemory(message: OB11ArrayMessage, action: ActionMap, adapter: string, instance: OB11PluginAdapter) {
|
||||
if (message.raw_message === '/清除短期上下文' && message.sender.user_id.toString() === BOT_ADMIN) {
|
||||
await handleClearMemoryCommand(message.group_id?.toString()!, 'short', action, adapter, instance);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.raw_message === '/清除长期上下文' && message.sender.user_id.toString() === BOT_ADMIN) {
|
||||
await handleClearMemoryCommand(message.group_id?.toString()!, 'long', action, adapter, instance);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export const plugin_onmessage = async (
|
||||
adapter: string,
|
||||
core: NapCatCore,
|
||||
_obCtx: NapCatOneBot11Adapter,
|
||||
message: OB11ArrayMessage,
|
||||
action: ActionMap,
|
||||
instance: OB11PluginAdapter
|
||||
) => {
|
||||
const currentHot = await chatHotManager.getHot(message.group_id?.toString()!);
|
||||
const oriMsgId = message.message.find(e => e.type == 'reply')?.data.id;
|
||||
const oriMsg = (oriMsgId ? await action.get('get_msg')?._handle({ message_id: oriMsgId }, adapter, instance.config) : undefined) as OB11ArrayMessage | undefined;
|
||||
const msgArray = await messageToOpenAi(adapter, message.message, message.group_id?.toString()!, action, instance, message);
|
||||
if (!msgArray) return;
|
||||
await memoryManager.updateMemory(message.group_id?.toString()!, [msgArray], core.selfInfo.uin);
|
||||
|
||||
if (await handleClearMemory(message, action, adapter, instance)) return;
|
||||
|
||||
const oriMsgOpenai = oriMsg ? await messageToOpenAi(adapter, oriMsg.message, oriMsg.group_id?.toString()!, action, instance, oriMsg) : undefined;
|
||||
|
||||
if (await shouldRespond(message, core, oriMsg, currentHot, msgArray, oriMsgOpenai)) {
|
||||
const sentMsg = await handleChatResponse(message, msgArray, adapter, action, instance, core, oriMsgOpenai);
|
||||
await memoryManager.updateMemory(message.group_id?.toString()!, [[{
|
||||
type: 'text',
|
||||
text: `我(群昵称: 乔千)(${core.selfInfo.uin})发送了消息(消息id: ${sentMsg.id}) : ` + sentMsg.text
|
||||
}]], core.selfInfo.uin);
|
||||
await chatHotManager.incrementHot(message.group_id?.toString()!);
|
||||
}
|
||||
};
|
102
src/plugin/memory.ts
Normal file
102
src/plugin/memory.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Mutex } from "async-mutex";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { ChatCompletionContentPart } from "openai/resources";
|
||||
import { MEMORY_FILE } from "./config";
|
||||
|
||||
export class MemoryManager {
|
||||
private longTermMemory: Map<string, string> = new Map();
|
||||
private shortTermMemory: Map<string, Array<ChatCompletionContentPart>[]> = new Map();
|
||||
private memoryCount: Map<string, number> = new Map();
|
||||
private memMutex = new Mutex();
|
||||
private SHORT_TERM_MEMORY_LIMIT = 100;
|
||||
private mergeAndUpdateMemory: (currentMemory: Array<ChatCompletionContentPart>[], newMessages: Array<ChatCompletionContentPart>[]) => Promise<string>;
|
||||
|
||||
constructor(mergeAndUpdateMemory: (currentMemory: Array<ChatCompletionContentPart>[], newMessages: Array<ChatCompletionContentPart>[]) => Promise<string>) {
|
||||
this.mergeAndUpdateMemory = mergeAndUpdateMemory;
|
||||
this.loadFromJson(MEMORY_FILE);
|
||||
setInterval(() => this.saveFromJson(MEMORY_FILE), 1000 * 60 * 5);
|
||||
}
|
||||
|
||||
async updateMemory(
|
||||
groupId: string,
|
||||
newMessages: Array<ChatCompletionContentPart>[],
|
||||
selfuin: string
|
||||
) {
|
||||
const currentMemory = this.shortTermMemory.get(groupId) || [];
|
||||
const memCount = await this.incrementMemoryCount(groupId);
|
||||
currentMemory.push(...newMessages);
|
||||
|
||||
if (memCount > this.SHORT_TERM_MEMORY_LIMIT) {
|
||||
await this.handleMemoryOverflow(groupId, currentMemory, newMessages, selfuin);
|
||||
}
|
||||
this.shortTermMemory.set(groupId, currentMemory);
|
||||
}
|
||||
|
||||
async incrementMemoryCount(groupId: string): Promise<number> {
|
||||
return this.memMutex.runExclusive(() => {
|
||||
const memCount = (this.memoryCount.get(groupId) || 0) + 1;
|
||||
this.memoryCount.set(groupId, memCount);
|
||||
return memCount;
|
||||
});
|
||||
}
|
||||
|
||||
async handleMemoryOverflow(
|
||||
groupId: string,
|
||||
currentMemory: Array<ChatCompletionContentPart>[],
|
||||
newMessages: Array<ChatCompletionContentPart>[],
|
||||
selfuin: string
|
||||
) {
|
||||
await this.memMutex.runExclusive(async () => {
|
||||
const containsBotName = currentMemory.some(messages =>
|
||||
messages.some(msg => msg.type === 'text' && msg.text.includes(selfuin))
|
||||
);
|
||||
|
||||
if (containsBotName) {
|
||||
const mergedMemory = await this.mergeAndUpdateMemory(currentMemory, newMessages);
|
||||
this.longTermMemory.set(groupId, mergedMemory);
|
||||
}
|
||||
|
||||
this.shortTermMemory.set(groupId, currentMemory.slice(-this.SHORT_TERM_MEMORY_LIMIT));
|
||||
this.memoryCount.set(groupId, 0);
|
||||
});
|
||||
}
|
||||
|
||||
async clearMemory(groupId: string, type: 'short' | 'long') {
|
||||
if (type === 'short') {
|
||||
this.shortTermMemory.set(groupId, []);
|
||||
} else {
|
||||
this.longTermMemory.set(groupId, '');
|
||||
}
|
||||
}
|
||||
|
||||
getLongTermMemory(groupId: string): string {
|
||||
return this.longTermMemory.get(groupId) || '';
|
||||
}
|
||||
|
||||
getShortTermMemory(groupId: string): Array<ChatCompletionContentPart>[] {
|
||||
return this.shortTermMemory.get(groupId) || [];
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
longTermMemory: Array.from(this.longTermMemory.entries()),
|
||||
shortTermMemory: Array.from(this.shortTermMemory.entries()),
|
||||
memoryCount: Array.from(this.memoryCount.entries())
|
||||
}
|
||||
}
|
||||
|
||||
saveFromJson(file: string) {
|
||||
let json = JSON.stringify(this.toJson(), null, 2);
|
||||
writeFileSync(file, json);
|
||||
}
|
||||
|
||||
loadFromJson(file: string) {
|
||||
if (existsSync(file)) {
|
||||
let json = readFileSync(file, { encoding: 'utf-8' });
|
||||
let obj = JSON.parse(json);
|
||||
this.longTermMemory = new Map(obj.longTermMemory);
|
||||
this.shortTermMemory = new Map(obj.shortTermMemory);
|
||||
this.memoryCount = new Map(obj.memoryCount);
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,7 +9,7 @@ const external = [
|
||||
'ws',
|
||||
'express',
|
||||
'@ffmpeg.wasm/core-mt',
|
||||
'piscina'
|
||||
'openai'
|
||||
];
|
||||
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
|
||||
|
||||
|
Reference in New Issue
Block a user