Merge pull request #404 from LLOneBot/dev

3.31.10
This commit is contained in:
idranme 2024-09-07 21:55:04 +08:00 committed by GitHub
commit 5c68d4de84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 241 additions and 537 deletions

View File

@ -4,7 +4,7 @@
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用于 QQ 机器人开发", "description": "实现 OneBot 11 协议,用于 QQ 机器人开发",
"version": "3.31.9", "version": "3.31.10",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {

View File

@ -1,234 +0,0 @@
import { NodeIQQNTWrapperSession } from '@/ntqqapi/wrapper'
import { randomUUID } from 'node:crypto'
interface Internal_MapKey {
timeout: number
createtime: number
func: (...arg: any[]) => unknown
checker?: (...args: any[]) => boolean
}
export class ListenerClassBase {
[key: string]: string
}
export interface ListenerIBase {
new(listener: unknown): ListenerClassBase
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/EventTask.ts#L20
export class NTEventWrapper {
private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数
private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession
private ListenerManger: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>() //ListenerName-Unique -> Listener实例
private EventTask = new Map<string, Map<string, Map<string, Internal_MapKey>>>()//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
public initialised = false
constructor() {
}
createProxyDispatch(ListenerMainName: string) {
const current = this
return new Proxy({}, {
get(target: any, prop: string, receiver: unknown) {
// console.log('get', prop, typeof target[prop])
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: unknown[]) => {
current.dispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
}
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver)
}
})
}
init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) {
this.ListenerMap = ListenerMap
this.WrapperSession = WrapperSession
this.initialised = true
}
createEventFunction<T extends (...args: any) => unknown>(eventName: string): T | undefined {
const eventNameArr = eventName.split('/')
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
}
if (eventNameArr.length > 1) {
const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', '')
const eventName = eventNameArr[1]
//getNodeIKernelGroupListener,GroupService
//console.log('2', eventName)
const services = (this.WrapperSession as unknown as eventType)[serviceName]()
let event = services[eventName]
//重新绑定this
event = event.bind(services)
if (event) {
return event as T
}
return undefined
}
}
createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const ListenerType = this.ListenerMap![listenerMainName]
let Listener = this.ListenerManger.get(listenerMainName + uniqueCode)
if (!Listener && ListenerType) {
Listener = new ListenerType(this.createProxyDispatch(listenerMainName))
const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]
const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener'
const addfunc = this.createEventFunction<(listener: T) => number>(Service)
addfunc!(Listener as T)
//console.log(addfunc!(Listener as T))
this.ListenerManger.set(listenerMainName + uniqueCode, Listener)
}
return Listener as T
}
//统一回调清理事件
async dispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: unknown[]) {
//console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args)
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => {
//console.log(task.func, uuid, task.createtime, task.timeout)
if (task.createtime + task.timeout < Date.now()) {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid)
return
}
if (task.checker && task.checker(...args)) {
task.func(...args)
}
})
}
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any>>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => {
const EventFunc = this.createEventFunction<EventType>(EventName)
let complete = false
const Timeouter = setTimeout(() => {
if (!complete) {
reject(new Error('NTEvent EventName:' + EventName + ' timeout'))
}
}, timeout)
const retData = await EventFunc!(...args)
complete = true
resolve(retData)
})
}
async RegisterListen<ListenerType extends (...args: any[]) => void>(ListenerName = '', waitTimes = 1, timeout = 5000, checker: (...args: Parameters<ListenerType>) => boolean) {
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
const ListenerNameList = ListenerName.split('/')
const ListenerMainName = ListenerNameList[0]
const ListenerSubName = ListenerNameList[1]
const id = randomUUID()
let complete = 0
let retData: Parameters<ListenerType> | undefined = undefined
const databack = () => {
if (complete == 0) {
reject(new Error(' ListenerName:' + ListenerName + ' timeout'))
} else {
resolve(retData!)
}
}
const Timeouter = setTimeout(databack, timeout)
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: Parameters<ListenerType>) => {
complete++
retData = args
if (complete >= waitTimes) {
clearTimeout(Timeouter)
databack()
}
}
}
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map())
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.createListenerFunction(ListenerMainName)
})
}
async CallNormalEvent<EventType extends (...args: any[]) => Promise<any>, ListenerType extends (...args: any[]) => void>
(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, checker: (...args: Parameters<ListenerType>) => boolean, ...args: Parameters<EventType>) {
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(async (resolve, reject) => {
const id = randomUUID()
let complete = 0
let retData: Parameters<ListenerType> | undefined = undefined
let retEvent = {}
const databack = () => {
if (complete == 0) {
reject(new Error('Timeout: NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' EventRet:\n' + JSON.stringify(retEvent, null, 4) + '\n'))
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!])
}
}
const ListenerNameList = ListenerName.split('/')
const ListenerMainName = ListenerNameList[0]
const ListenerSubName = ListenerNameList[1]
const Timeouter = setTimeout(databack, timeout)
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: unknown[]) => {
complete++
//console.log('func', ...args)
retData = args as Parameters<ListenerType>
if (complete >= waitTimes) {
clearTimeout(Timeouter)
databack()
}
}
}
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map())
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.createListenerFunction(ListenerMainName)
const EventFunc = this.createEventFunction<EventType>(EventName)
retEvent = await EventFunc!(...args)
})
}
}
export const NTEventDispatch = new NTEventWrapper()
// 示例代码 快速创建事件
// let NTEvent = new NTEventWrapper()
// let TestEvent = NTEvent.CreatEventFunction<(force: boolean) => Promise<Number>>('NodeIKernelProfileLikeService/GetTest')
// if (TestEvent) {
// TestEvent(true)
// }
// 示例代码 快速创建监听Listener类
// let NTEvent = new NTEventWrapper()
// NTEvent.CreatListenerFunction<NodeIKernelMsgListener>('NodeIKernelMsgListener', 'core')
// 调用接口
//let NTEvent = new NTEventWrapper()
//let ret = await NTEvent.CallNormalEvent<(force: boolean) => Promise<Number>, (data1: string, data2: number) => void>('NodeIKernelProfileLikeService/GetTest', 'NodeIKernelMsgListener/onAddSendMsg', 1, 3000, true)
// 注册监听 解除监听
// NTEventDispatch.RigisterListener('NodeIKernelMsgListener/onAddSendMsg','core',cb)
// NTEventDispatch.UnRigisterListener('NodeIKernelMsgListener/onAddSendMsg','core')
// let GetTest = NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode)
// GetTest('test')
// always模式
// NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode,(...args:any[])=>{ console.log(args) })

View File

@ -10,154 +10,154 @@ import { DATA_DIR } from '../globalVars'
import { FileCacheV2 } from '../types' import { FileCacheV2 } from '../types'
interface SQLiteTables extends Tables { interface SQLiteTables extends Tables {
message: { message: {
shortId: number shortId: number
msgId: string msgId: string
chatType: number chatType: number
peerUid: string peerUid: string
} }
file_v2: FileCacheV2 file_v2: FileCacheV2
} }
interface MsgIdAndPeerByShortId { interface MsgIdAndPeerByShortId {
MsgId: string MsgId: string
Peer: Peer Peer: Peer
} }
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L84 // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L84
class MessageUniqueWrapper { class MessageUniqueWrapper {
private msgDataMap: LimitedHashTable<string, number> private msgDataMap: LimitedHashTable<string, number>
private msgIdMap: LimitedHashTable<string, number> private msgIdMap: LimitedHashTable<string, number>
private db: Database<SQLiteTables> | undefined private db: Database<SQLiteTables> | undefined
constructor(maxMap: number = 1000) { constructor(maxMap: number = 1000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap) this.msgIdMap = new LimitedHashTable<string, number>(maxMap)
this.msgDataMap = new LimitedHashTable<string, number>(maxMap) this.msgDataMap = new LimitedHashTable<string, number>(maxMap)
}
async init(uin: string) {
const dbDir = path.join(DATA_DIR, 'database')
if (!fs.existsSync(dbDir)) {
await fsPromise.mkdir(dbDir)
} }
const database = new Database<SQLiteTables>()
await database.connect(SQLite, {
path: path.join(dbDir, `${uin}.db`)
})
database.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
database.extend('file_v2', {
fileName: 'string',
fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileUuid',
indexes: ['fileName']
})
this.db = database
}
async init(uin: string) { async getRecentMsgIds(Peer: Peer, size: number): Promise<string[]> {
const dbDir = path.join(DATA_DIR, 'database') const heads = this.msgIdMap.getHeads(size)
if (!fs.existsSync(dbDir)) { if (!heads) {
await fsPromise.mkdir(dbDir) return []
}
const data: (MsgIdAndPeerByShortId | undefined)[] = []
for (const t of heads) {
data.push(await MessageUnique.getMsgIdAndPeerByShortId(t.value))
}
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid)
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
}
createMsg(peer: Peer, msgId: string): number {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(key).digest()
//设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f
const shortId = hash.readInt32BE(0)
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId)
// if (isExist && isExist === msgId) {
// return shortId
// }
this.msgIdMap.set(msgId, shortId)
this.msgDataMap.set(key, shortId)
this.db?.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgIdAndPeerByShortId(shortId: number): Promise<MsgIdAndPeerByShortId | undefined> {
const data = this.msgDataMap.getKey(shortId)
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|')
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
}
return { MsgId: msgId, Peer: peer }
}
const items = await this.db?.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
MsgId: msgId,
Peer: {
chatType,
peerUid,
guildId: '',
} }
const database = new Database<SQLiteTables>() }
await database.connect(SQLite, {
path: path.join(dbDir, `${uin}.db`)
})
database.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
database.extend('file_v2', {
fileName: 'string',
fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileUuid',
indexes: ['fileName']
})
this.db = database
} }
return undefined
}
async getRecentMsgIds(Peer: Peer, size: number): Promise<string[]> { getShortIdByMsgId(msgId: string): number | undefined {
const heads = this.msgIdMap.getHeads(size) return this.msgIdMap.getValue(msgId)
if (!heads) { }
return []
}
const data: (MsgIdAndPeerByShortId | undefined)[] = []
for (const t of heads) {
data.push(await MessageUnique.getMsgIdAndPeerByShortId(t.value))
}
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid)
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
}
createMsg(peer: Peer, msgId: string): number { async getPeerByMsgId(msgId: string) {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}` const shortId = this.msgIdMap.getValue(msgId)
const hash = createHash('md5').update(key).digest() if (!shortId) return undefined
//设置第一个bit为0 保证shortId为正数 return await this.getMsgIdAndPeerByShortId(shortId)
hash[0] &= 0x7f }
const shortId = hash.readInt32BE(0)
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId)
// if (isExist && isExist === msgId) {
// return shortId
// }
this.msgIdMap.set(msgId, shortId)
this.msgDataMap.set(key, shortId)
this.db?.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgIdAndPeerByShortId(shortId: number): Promise<MsgIdAndPeerByShortId | undefined> { resize(maxSize: number): void {
const data = this.msgDataMap.getKey(shortId) this.msgIdMap.resize(maxSize)
if (data) { this.msgDataMap.resize(maxSize)
const [msgId, chatTypeStr, peerUid] = data.split('|') }
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
}
return { MsgId: msgId, Peer: peer }
}
const items = await this.db?.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
MsgId: msgId,
Peer: {
chatType,
peerUid,
guildId: '',
}
}
}
return undefined
}
getShortIdByMsgId(msgId: string): number | undefined { addFileCache(data: FileCacheV2) {
return this.msgIdMap.getValue(msgId) return this.db?.upsert('file_v2', [data], 'fileUuid')
} }
async getPeerByMsgId(msgId: string) { getFileCacheByName(fileName: string) {
const shortId = this.msgIdMap.getValue(msgId) return this.db?.get('file_v2', { fileName }, {
if (!shortId) return undefined sort: { msgTime: 'desc' }
return await this.getMsgIdAndPeerByShortId(shortId) })
} }
resize(maxSize: number): void { getFileCacheById(fileUuid: string) {
this.msgIdMap.resize(maxSize) return this.db?.get('file_v2', { fileUuid })
this.msgDataMap.resize(maxSize) }
}
addFileCache(data: FileCacheV2) {
return this.db?.upsert('file_v2', [data], 'fileUuid')
}
getFileCacheByName(fileName: string) {
return this.db?.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.db?.get('file_v2', { fileUuid })
}
} }
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper() export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper()

View File

@ -1,72 +1,72 @@
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L5 // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L5
export class LimitedHashTable<K, V> { export class LimitedHashTable<K, V> {
private keyToValue: Map<K, V> = new Map() private keyToValue: Map<K, V> = new Map()
private valueToKey: Map<V, K> = new Map() private valueToKey: Map<V, K> = new Map()
private maxSize: number private maxSize: number
constructor(maxSize: number) { constructor(maxSize: number) {
this.maxSize = maxSize this.maxSize = maxSize
} }
resize(count: number) { resize(count: number) {
this.maxSize = count this.maxSize = count
} }
set(key: K, value: V): void { set(key: K, value: V): void {
this.keyToValue.set(key, value) this.keyToValue.set(key, value)
this.valueToKey.set(value, key) this.valueToKey.set(value, key)
while (this.keyToValue.size !== this.valueToKey.size) { while (this.keyToValue.size !== this.valueToKey.size) {
console.log('keyToValue.size !== valueToKey.size Error Atom') console.log('keyToValue.size !== valueToKey.size Error Atom')
this.keyToValue.clear() this.keyToValue.clear()
this.valueToKey.clear() this.valueToKey.clear()
}
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value
this.valueToKey.delete(this.keyToValue.get(oldestKey)!)
this.keyToValue.delete(oldestKey)
}
} }
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value
this.valueToKey.delete(this.keyToValue.get(oldestKey)!)
this.keyToValue.delete(oldestKey)
}
}
getValue(key: K): V | undefined { getValue(key: K): V | undefined {
return this.keyToValue.get(key) return this.keyToValue.get(key)
} }
getKey(value: V): K | undefined { getKey(value: V): K | undefined {
return this.valueToKey.get(value) return this.valueToKey.get(value)
} }
deleteByValue(value: V): void { deleteByValue(value: V): void {
const key = this.valueToKey.get(value) const key = this.valueToKey.get(value)
if (key !== undefined) { if (key !== undefined) {
this.keyToValue.delete(key) this.keyToValue.delete(key)
this.valueToKey.delete(value) this.valueToKey.delete(value)
}
} }
}
deleteByKey(key: K): void { deleteByKey(key: K): void {
const value = this.keyToValue.get(key) const value = this.keyToValue.get(key)
if (value !== undefined) { if (value !== undefined) {
this.keyToValue.delete(key) this.keyToValue.delete(key)
this.valueToKey.delete(value) this.valueToKey.delete(value)
}
} }
}
getKeyList(): K[] { getKeyList(): K[] {
return Array.from(this.keyToValue.keys()) return Array.from(this.keyToValue.keys())
} }
//获取最近刚写入的几个值 //获取最近刚写入的几个值
getHeads(size: number): { key: K; value: V }[] | undefined { getHeads(size: number): { key: K; value: V }[] | undefined {
const keyList = this.getKeyList() const keyList = this.getKeyList()
if (keyList.length === 0) { if (keyList.length === 0) {
return undefined return undefined
}
const result: { key: K; value: V }[] = []
const listSize = Math.min(size, keyList.length)
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i]
result.push({ key, value: this.keyToValue.get(key)! })
}
return result
} }
const result: { key: K; value: V }[] = []
const listSize = Math.min(size, keyList.length)
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i]
result.push({ key, value: this.keyToValue.get(key)! })
}
return result
}
} }

View File

@ -21,7 +21,6 @@ import { Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file' import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type' import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/eventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners' import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { Time } from 'cosmokit' import { Time } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
@ -143,76 +142,32 @@ export class NTQQFileApi extends Service {
return sourcePath return sourcePath
} }
} }
let filePath: string const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
if (NTEventDispatch.initialised) { 'nodeIKernelMsgService/downloadRichMedia',
const data = await NTEventDispatch.CallNormalEvent< [
(
params: {
fileModelId: string,
downloadSourceType: number,
triggerType: number,
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbSize: number,
downloadType: number,
filePath: string
}) => Promise<unknown>,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
>(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
1,
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
return true
}
return false
},
{ {
fileModelId: '0', getReq: {
downloadSourceType: 0, fileModelId: '0',
triggerType: 1, downloadSourceType: 0,
msgId: msgId, triggerType: 1,
chatType: chatType, msgId: msgId,
peerUid: peerUid, chatType: chatType,
elementId: elementId, peerUid: peerUid,
thumbSize: 0, elementId: elementId,
downloadType: 1, thumbSize: 0,
filePath: thumbPath downloadType: 1,
} filePath: thumbPath,
)
filePath = data[1].filePath
} else {
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
'nodeIKernelMsgService/downloadRichMedia',
[
{
getReq: {
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
}, },
null, },
], null,
{ ],
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, {
cmdCB: payload => payload.notifyInfo.msgId === msgId, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
timeout cmdCB: payload => payload.notifyInfo.msgId === msgId,
} timeout
) }
filePath = data.notifyInfo.filePath )
} let filePath = data.notifyInfo.filePath
if (filePath.startsWith('\\')) { if (filePath.startsWith('\\')) {
const downloadPath = TEMP_DIR const downloadPath = TEMP_DIR
filePath = path.join(downloadPath, filePath) filePath = path.join(downloadPath, filePath)
@ -238,7 +193,7 @@ export class NTQQFileApi extends Service {
const url: string = element.originImageUrl! // 没有域名 const url: string = element.originImageUrl! // 没有域名
const md5HexStr = element.md5HexStr const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr const fileMd5 = element.md5HexStr
if (url) { if (url) {
const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接 const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
const imageAppid = parsedUrl.searchParams.get('appid') const imageAppid = parsedUrl.searchParams.get('appid')

View File

@ -2,8 +2,7 @@ import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { invoke, NTMethod, NTClass } from '../ntcall' import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services' import { BuddyListReqType } from '../services'
import { NTEventDispatch } from '@/common/utils/eventTask'
import { Dict, pick } from 'cosmokit' import { Dict, pick } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
@ -19,7 +18,7 @@ export class NTQQFriendApi extends Service {
} }
/** 大于或等于 26702 应使用 getBuddyV2 */ /** 大于或等于 26702 应使用 getBuddyV2 */
async getFriends(_forced = false) { async getFriends() {
const data = await invoke<{ const data = await invoke<{
data: { data: {
categoryId: number categoryId: number
@ -75,9 +74,7 @@ export class NTQQFriendApi extends Service {
const buddyService = session.getBuddyService() const buddyService = session.getBuddyService()
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2.data.flatMap(item => item.buddyUids)) uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data.values()) return Array.from(data.values())
} else { } else {
const data = await invoke<{ const data = await invoke<{
@ -92,11 +89,8 @@ export class NTQQFriendApi extends Service {
afterFirstCmd: false, afterFirstCmd: false,
} }
) )
const categoryUids: Map<number, string[]> = new Map() const uids = data.buddyCategory.flatMap(item => item.buddyUids)
for (const item of data.buddyCategory) { return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!))
categoryUids.set(item.categoryId, item.buddyUids)
}
return Object.values(data.userSimpleInfos).filter(v => v.baseInfo && categoryUids.get(v.baseInfo.categoryId)?.includes(v.uid!))
} }
} }
@ -106,12 +100,10 @@ export class NTQQFriendApi extends Service {
const session = getSession() const session = getSession()
if (session) { if (session) {
const uids: string[] = [] const uids: string[] = []
const buddyService = session?.getBuddyService() const buddyService = session.getBuddyService()
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2.data.flatMap(item => item.buddyUids)) uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
for (const [, item] of data) { for (const [, item] of data) {
if (retMap.size > 5000) { if (retMap.size > 5000) {
break break
@ -155,9 +147,7 @@ export class NTQQFriendApi extends Service {
}) })
return item.buddyUids return item.buddyUids
})) }))
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data).map(([key, value]) => { return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key) const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value

View File

@ -1,5 +1,5 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify, GetFileListParam } from '../types' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GetFileListParam } from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { NTQQWindows } from './window' import { NTQQWindows } from './window'
@ -24,7 +24,7 @@ export class NTQQGroupApi extends Service {
super(ctx, 'ntGroupApi', true) super(ctx, 'ntGroupApi', true)
} }
async getGroups(forced = false): Promise<Group[]> { async getGroups(): Promise<Group[]> {
const result = await invoke<{ const result = await invoke<{
updateType: number updateType: number
groupList: Group[] groupList: Group[]

View File

@ -65,7 +65,7 @@ export class NTQQUserApi extends Service {
return ret return ret
} }
async getUserDetailInfo(uid: string, _getLevel = false) { async getUserDetailInfo(uid: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return this.fetchUserDetailInfo(uid) return this.fetchUserDetailInfo(uid)
} }

View File

@ -2,8 +2,6 @@ import fs from 'node:fs'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook' import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook'
import { MessageUnique } from '../common/utils/messageUnique' import { MessageUnique } from '../common/utils/messageUnique'
import { NTEventDispatch } from '../common/utils/eventTask'
import { wrapperConstructor, getSession } from './wrapper'
import { Config as LLOBConfig } from '../common/types' import { Config as LLOBConfig } from '../common/types'
import { llonebotError } from '../common/globalVars' import { llonebotError } from '../common/globalVars'
import { isNumeric } from '../common/utils/misc' import { isNumeric } from '../common/utils/misc'
@ -45,10 +43,6 @@ class Core extends Service {
public start() { public start() {
llonebotError.otherError = '' llonebotError.otherError = ''
const WrapperSession = getSession()
if (WrapperSession) {
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession })
}
MessageUnique.init(selfInfo.uin) MessageUnique.init(selfInfo.uin)
this.registerListener() this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`) this.ctx.logger.info(`LLOneBot/${version}`)

View File

@ -201,7 +201,7 @@ export namespace SendElementEntities {
// log("生成缩略图", _thumbPath) // log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath) thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath) const thumbMd5 = await calculateFileMD5(_thumbPath)
let element: SendVideoElement = { const element: SendVideoElement = {
elementType: ElementType.VIDEO, elementType: ElementType.VIDEO,
elementId: '', elementId: '',
videoElement: { videoElement: {

View File

@ -34,7 +34,7 @@ export interface WrapperApi {
} }
export interface WrapperConstructor { export interface WrapperConstructor {
[key: string]: any [key: string]: unknown
} }
const wrapperApi: WrapperApi = {} const wrapperApi: WrapperApi = {}

View File

@ -28,7 +28,7 @@ export class UploadGroupFile extends BaseAction<UploadGroupFilePayload, null> {
} }
const sendFileEle = await SendElementEntities.file(this.ctx, downloadResult.path, payload.name, payload.folder_id) const sendFileEle = await SendElementEntities.file(this.ctx, downloadResult.path, payload.name, payload.folder_id)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group) const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group)
await sendMsg(this.ctx, peer, [sendFileEle], [], true) await sendMsg(this.ctx, peer, [sendFileEle], [])
return null return null
} }
} }
@ -53,7 +53,7 @@ export class UploadPrivateFile extends BaseAction<UploadPrivateFilePayload, null
throw new Error(downloadResult.errMsg) throw new Error(downloadResult.errMsg)
} }
const sendFileEle: SendFileElement = await SendElementEntities.file(this.ctx, downloadResult.path, payload.name) const sendFileEle: SendFileElement = await SendElementEntities.file(this.ctx, downloadResult.path, payload.name)
await sendMsg(this.ctx, peer, [sendFileEle], [], true) await sendMsg(this.ctx, peer, [sendFileEle], [])
return null return null
} }
} }

View File

@ -10,8 +10,8 @@ interface Payload {
class GetGroupList extends BaseAction<Payload, OB11Group[]> { class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList actionName = ActionName.GetGroupList
protected async _handle(payload: Payload) { protected async _handle() {
const groupList = await this.ctx.ntGroupApi.getGroups(payload?.no_cache === true || payload?.no_cache === 'true') const groupList = await this.ctx.ntGroupApi.getGroups()
return OB11Entities.groups(groupList) return OB11Entities.groups(groupList)
} }
} }

View File

@ -18,7 +18,7 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
if (member) { if (member) {
if (isNullable(member.sex)) { if (isNullable(member.sex)) {
//log('获取群成员详细信息') //log('获取群成员详细信息')
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid, true) const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
//log('群成员详细信息结果', info) //log('群成员详细信息结果', info)
Object.assign(member, info) Object.assign(member, info)
} }

View File

@ -214,7 +214,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
} }
// log("分割后的转发节点", sendElementsSplit) // log("分割后的转发节点", sendElementsSplit)
for (const eles of sendElementsSplit) { for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [], true) const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [])
if (!nodeMsg) { if (!nodeMsg) {
this.ctx.logger.warn('转发节点生成失败', eles) this.ctx.logger.warn('转发节点生成失败', eles)
continue continue

View File

@ -5,7 +5,7 @@ interface ReturnType {
yes: boolean yes: boolean
} }
export default class CanSendRecord extends BaseAction<any, ReturnType> { export default class CanSendRecord extends BaseAction<null, ReturnType> {
actionName = ActionName.CanSendRecord actionName = ActionName.CanSendRecord
protected async _handle() { protected async _handle() {

View File

@ -16,7 +16,7 @@ export class GetFriendList extends BaseAction<Payload, OB11User[]> {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return OB11Entities.friendsV2(await this.ctx.ntFriendApi.getBuddyV2(refresh)) return OB11Entities.friendsV2(await this.ctx.ntFriendApi.getBuddyV2(refresh))
} }
return OB11Entities.friends(await this.ctx.ntFriendApi.getFriends(refresh)) return OB11Entities.friends(await this.ctx.ntFriendApi.getFriends())
} }
} }

View File

@ -399,7 +399,7 @@ class OneBot11Adapter extends Service {
this.handleRecallMsg(input) this.handleRecallMsg(input)
}) })
this.ctx.on('nt/message-sent', input => { this.ctx.on('nt/message-sent', input => {
this.handleRecallMsg(input) this.handleMsg(input)
}) })
this.ctx.on('nt/group-notify', input => { this.ctx.on('nt/group-notify', input => {
this.handleGroupNotify(input) this.handleGroupNotify(input)

View File

@ -173,8 +173,8 @@ export namespace OB11Entities {
id: MessageUnique.createMsg(peer, replyMsg ? replyMsg.msgId : records.msgId).toString() id: MessageUnique.createMsg(peer, replyMsg ? replyMsg.msgId : records.msgId).toString()
} }
} }
} catch (e: any) { } catch (e) {
ctx.logger.error('获取不到引用的消息', replyElement, e.stack) ctx.logger.error('获取不到引用的消息', replyElement, (e as Error).stack)
continue continue
} }
} }
@ -378,7 +378,7 @@ export namespace OB11Entities {
if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) { if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型 //判断业务类型
//Poke事件 //Poke事件
const pokedetail: any[] = json.items const pokedetail: Dict[] = json.items
//筛选item带有uid的元素 //筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid) const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) { if (poke_uid.length == 2) {

View File

@ -236,8 +236,7 @@ export async function sendMsg(
ctx: Context, ctx: Context,
peer: Peer, peer: Peer,
sendElements: SendMessageElement[], sendElements: SendMessageElement[],
deleteAfterSentFiles: string[], deleteAfterSentFiles: string[]
waitComplete = true,
) { ) {
if (!sendElements.length) { if (!sendElements.length) {
throw '消息体无法解析,请检查是否发送了不支持的消息类型' throw '消息体无法解析,请检查是否发送了不支持的消息类型'

View File

@ -88,7 +88,7 @@ async function handleMsg(ctx: Context, msg: OB11Message, quickAction: QuickOpera
} }
replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape)) replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape))
const { sendElements, deleteAfterSentFiles } = await createSendElements(ctx, replyMessage, peer) const { sendElements, deleteAfterSentFiles } = await createSendElements(ctx, replyMessage, peer)
sendMsg(ctx, peer, sendElements, deleteAfterSentFiles, false).catch(e => ctx.logger.error(e)) sendMsg(ctx, peer, sendElements, deleteAfterSentFiles).catch(e => ctx.logger.error(e))
} }
if (msg.message_type === 'group') { if (msg.message_type === 'group') {
const groupMsgQuickAction = quickAction as QuickOperationGroupMessage const groupMsgQuickAction = quickAction as QuickOperationGroupMessage

View File

@ -1,7 +1,7 @@
import { CheckVersion, Config } from '../common/types' import { CheckVersion, Config } from '../common/types'
import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components' import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components'
import { version } from '../version' import { version } from '../version'
// @ts-expect-error // @ts-expect-error: Unreachable code error
import StyleRaw from './style.css?raw' import StyleRaw from './style.css?raw'
type HostsType = 'httpHosts' | 'wsHosts' type HostsType = 'httpHosts' | 'wsHosts'

View File

@ -1 +1 @@
export const version = '3.31.9' export const version = '3.31.10'