Merge branch 'dev'

This commit is contained in:
linyuchen 2024-11-13 19:34:18 +08:00
commit a23a99310a
27 changed files with 7332 additions and 57 deletions

View File

@ -39,6 +39,7 @@ const config: ElectronViteConfig = {
...external.map(genCpModule), ...external.map(genCpModule),
{ src: './manifest.json', dest: 'dist' }, { src: './manifest.json', dest: 'dist' },
{ src: './icon.webp', dest: 'dist' }, { src: './icon.webp', dest: 'dist' },
{ src: './src/ntqqapi/native/napcat-protocol-packet/Moehoo/*', dest: 'dist/main/Moehoo' },
], ],
}), }),
], ],

View File

@ -4,12 +4,16 @@
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发", "description": "实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发",
"version": "4.1.4", "version": "4.2.0",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {
"name": "linyuchen", "name": "linyuchen",
"link": "https://github.com/linyuchen" "link": "https://github.com/linyuchen"
},
{
"name": "idranme",
"link": "https://github.com/idranme"
} }
], ],
"repository": { "repository": {

View File

@ -26,7 +26,7 @@
"cosmokit": "^1.6.3", "cosmokit": "^1.6.3",
"express": "^5.0.1", "express": "^5.0.1",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"minato": "^3.6.0", "minato": "^3.6.1",
"protobufjs": "^7.4.0", "protobufjs": "^7.4.0",
"silk-wasm": "^3.6.3", "silk-wasm": "^3.6.3",
"ts-case-convert": "^2.1.0", "ts-case-convert": "^2.1.0",
@ -35,14 +35,14 @@
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.26", "@types/fluent-ffmpeg": "^2.1.27",
"@types/node": "^20.14.15", "@types/node": "^20.14.15",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.13",
"electron": "^31.4.0", "electron": "^31.4.0",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"protobufjs-cli": "^1.1.3", "protobufjs-cli": "^1.1.3",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"vite": "^5.4.10 ", "vite": "^5.4.10",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.5.1" "packageManager": "yarn@4.5.1"

View File

@ -13,6 +13,10 @@ const manifest = {
{ {
name: 'linyuchen', name: 'linyuchen',
link: 'https://github.com/linyuchen' link: 'https://github.com/linyuchen'
},
{
"name": "idranme",
"link": "https://github.com/idranme"
} }
], ],
repository: { repository: {

View File

@ -27,9 +27,10 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
export function calculateFileMD5(filePath: string): Promise<string> { export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const hash = createHash('md5')
// 创建一个流式读取器 // 创建一个流式读取器
const stream = fs.createReadStream(filePath) const stream = fs.createReadStream(filePath)
const hash = createHash('md5')
stream.on('data', (data: Buffer) => { stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态 // 当读取到数据时,更新哈希对象的状态

View File

@ -35,7 +35,9 @@ import {
NTQQWindowApi NTQQWindowApi
} from '../ntqqapi/api' } from '../ntqqapi/api'
import { existsSync, mkdirSync } from 'node:fs' import { existsSync, mkdirSync } from 'node:fs'
import { checkChanelId } from '@/ntqqapi/ntcall'
import { initWrapperSession} from '@/ntqqapi/native/napcat-protocol-packet'
initWrapperSession().then()
declare module 'cordis' { declare module 'cordis' {
interface Events { interface Events {
@ -185,7 +187,6 @@ function onLoad() {
if (config.enableLLOB && (config.satori.enable || config.ob11.enable)) { if (config.enableLLOB && (config.satori.enable || config.ob11.enable)) {
startHook() startHook()
await checkChanelId()
await ctx.sleep(600) await ctx.sleep(600)
} else { } else {
llonebotError.otherError = 'LLOneBot 未启动' llonebotError.otherError = 'LLOneBot 未启动'

View File

@ -19,7 +19,8 @@ import {
import { selfInfo } from '../common/globalVars' import { selfInfo } from '../common/globalVars'
import { version } from '../version' import { version } from '../version'
import { invoke } from './ntcall' import { invoke } from './ntcall'
import { Native } from './native/index' import { Native } from './native/crychic'
import { initWrapperSession, NTQQPacketApi } from './native/napcat-protocol-packet'
declare module 'cordis' { declare module 'cordis' {
interface Context { interface Context {
@ -40,10 +41,12 @@ class Core extends Service {
static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi', 'store'] static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi', 'store']
public startTime = 0 public startTime = 0
public native public native
public ntqqPacketApi: NTQQPacketApi
constructor(protected ctx: Context, public config: Core.Config) { constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true) super(ctx, 'app', true)
this.native = new Native(ctx) this.native = new Native(ctx)
initWrapperSession().then(session=>{this.ntqqPacketApi = new NTQQPacketApi(session)})
} }
public start() { public start() {

View File

@ -1,4 +1,4 @@
import { invoke, NTChannel, NTMethod } from './ntcall' import { NTMethod } from './ntcall'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
@ -40,7 +40,7 @@ const callHooks: Array<{
}> = [] }> = []
export function startHook() { export function startHook() {
log("start hook") log('start hook')
const senderExclude = Symbol() const senderExclude = Symbol()

View File

@ -1,9 +1,9 @@
import { Context } from 'cordis' import { Context } from 'cordis'
import { Dict } from 'cosmokit' import { Dict } from 'cosmokit'
import { getBuildVersion } from '@/common/utils/misc' import { getBuildVersion } from '../../../common/utils/misc'
import { TEMP_DIR } from '@/common/globalVars' import { TEMP_DIR } from '../../../common/globalVars'
import { copyFile } from 'fs/promises' import { copyFile } from 'fs/promises'
import { ChatType, Peer } from '../types' import { ChatType, Peer } from '../../types'
import path from 'node:path' import path from 'node:path'
import addon from './external/crychic-win32-x64.node?asset' import addon from './external/crychic-win32-x64.node?asset'

View File

@ -0,0 +1,17 @@
import { WrapperSession } from './wrapper-session/types';
export { initWrapperSession } from './wrapper-session';
export declare class NTQQPacketApi {
private qqVersion;
private packetSession;
private logger;
private readonly wrapperSession;
constructor(wrapperSession: WrapperSession);
get available(): boolean;
private checkQQVersion;
private InitSendPacket;
private sendPacket;
private sendOidbPacket;
sendPokePacket(peer: number, group?: number): Promise<void>;
sendGroupSignPacket(selfUin: string, groupCode: string): Promise<void>;
sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): Promise<void>;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
export declare function initWrapperSession(): Promise<any>;

View File

@ -0,0 +1,6 @@
export interface MsgService {
sendSsoCmdReqByContend: (cmd: string, trace_id: string) => Promise<unknown>;
}
export type WrapperSession = {
getMsgService(): MsgService;
};

View File

@ -1,6 +1,6 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { hookApiCallbacks, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook' import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook'
import { getBuildVersion, log } from '../common/utils' import { log } from '../common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { import {
GeneralCallResult, GeneralCallResult,
@ -17,8 +17,6 @@ import {
NodeIKernelRobotService, NodeIKernelRobotService,
NodeIKernelNodeMiscService NodeIKernelNodeMiscService
} from './services' } from './services'
import { CategoryFriend, SimpleInfo, UserDetailInfoByUin } from '@/ntqqapi/types'
import { selfInfo } from '@/common/globalVars'
export enum NTClass { export enum NTClass {
NT_API = 'ns-ntApi', NT_API = 'ns-ntApi',
@ -110,38 +108,26 @@ interface InvokeOptions<ReturnType> {
timeout?: number timeout?: number
} }
let availableChannel: NTChannel | undefined = undefined; let channel: NTChannel
export async function checkChanelId(){ function getChannel() {
async function testChannel(channel: NTChannel) { if (channel) {
await invoke<UserDetailInfoByUin>( return channel
'nodeIKernelProfileService/getUserDetailInfoByUin',
[{ uin: selfInfo.uin }],
{ timeout: 1000, channel }
)
} }
if (ipcMain.eventNames().includes(NTChannel.IPC_UP_2)) {
for (const channel of [NTChannel.IPC_UP_2, NTChannel.IPC_UP_3]) { return channel = NTChannel.IPC_UP_2
// const channel = `IPC_UP_${windowId}` as NTChannel } else {
try { return channel = NTChannel.IPC_UP_3
await testChannel(channel)
log(`check channel ${channel} success`)
availableChannel = channel
return
} catch (e) {
log(`check channel ${channel} failed`, e)
}
} }
availableChannel = getBuildVersion() >= 28788 ? NTChannel.IPC_UP_3 : NTChannel.IPC_UP_2
} }
export function invoke< export function invoke<
R extends Awaited<ReturnType<Extract<NTService[S][M], (...args: any) => unknown>>>, R extends Awaited<ReturnType<Extract<NTService[S][M], (...args: any) => unknown>>>,
S extends keyof NTService = any, S extends keyof NTService = any,
M extends keyof NTService[S] & string = any M extends keyof NTService[S] & string = any
>(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) { >(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) {
const className = options.className ?? NTClass.NT_API const className = options.className ?? NTClass.NT_API
// const channel = options.channel ?? getBuildVersion() >= 28788 ? NTChannel.IPC_UP_3 : NTChannel.IPC_UP_2 const channel = options.channel ?? getChannel()
const channel = options.channel ?? availableChannel!
const timeout = options.timeout ?? 5000 const timeout = options.timeout ?? 5000
const afterFirstCmd = options.afterFirstCmd ?? true const afterFirstCmd = options.afterFirstCmd ?? true
let eventName = className + '-' + channel[channel.length - 1] let eventName = className + '-' + channel[channel.length - 1]

View File

@ -12,6 +12,7 @@ interface Payload {
interface Response extends OB11User { interface Response extends OB11User {
reg_time: number reg_time: number
long_nick: string long_nick: string
city: string
} }
export class GetStrangerInfo extends BaseAction<Payload, Response> { export class GetStrangerInfo extends BaseAction<Payload, Response> {
@ -33,7 +34,8 @@ export class GetStrangerInfo extends BaseAction<Payload, Response> {
level: data.detail.commonExt.qqLevel && calcQQLevel(data.detail.commonExt.qqLevel) || 0, level: data.detail.commonExt.qqLevel && calcQQLevel(data.detail.commonExt.qqLevel) || 0,
login_days: 0, login_days: 0,
reg_time: data.detail.commonExt.regTime, reg_time: data.detail.commonExt.regTime,
long_nick: data.detail.simpleInfo.baseInfo.longNick long_nick: data.detail.simpleInfo.baseInfo.longNick,
city: data.detail.commonExt.city
} }
} else { } else {
const data = await this.ctx.ntUserApi.getUserDetailInfoByUin(uin) const data = await this.ctx.ntUserApi.getUserDetailInfoByUin(uin)
@ -46,7 +48,8 @@ export class GetStrangerInfo extends BaseAction<Payload, Response> {
level: data.info.qqLevel && calcQQLevel(data.info.qqLevel) || 0, level: data.info.qqLevel && calcQQLevel(data.info.qqLevel) || 0,
login_days: 0, login_days: 0,
reg_time: data.info.regTime, reg_time: data.info.regTime,
long_nick: data.info.longNick long_nick: data.info.longNick,
city: data.info.city
} }
} }
} }

View File

@ -0,0 +1,20 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils/misc'
import { selfInfo } from '@/common/globalVars'
interface Payload {
group_id: number | string
}
export class SendGroupSign extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SendGroupSign
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
})
async _handle(payload: Payload) {
await this.ctx.app.ntqqPacketApi.sendGroupSignPacket(selfInfo.uin, payload.group_id.toString())
return null
}
}

View File

@ -0,0 +1,31 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils/misc'
import { selfInfo } from '@/common/globalVars'
import { GroupMemberRole } from '@/ntqqapi/types'
interface Payload {
group_id: number | string
user_id: number | string
special_title?: string
}
export class SetGroupSpecialTitle extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SetGroupSpecialTitle
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required(),
special_title: Schema.string()
})
async _handle(payload: Payload) {
const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString(), payload.group_id.toString())
if (!uid) throw new Error(`用户${payload.user_id}的uid获取失败`)
const self = await this.ctx.ntGroupApi.getGroupMember(payload.group_id.toString(), selfInfo.uid, false)
if (self.role !== GroupMemberRole.Owner){
throw new Error(`不是群${payload.group_id}的群主,无法设置群头衔`)
}
await this.ctx.app.ntqqPacketApi.sendSetSpecialTittlePacket(payload.group_id.toString(), uid, payload.special_title || "")
return null
}
}

View File

@ -78,6 +78,8 @@ import { GroupPoke } from './llonebot/GroupPoke'
import { FriendPoke } from './llonebot/FriendPoke' import { FriendPoke } from './llonebot/FriendPoke'
import { GetGroupFileSystemInfo } from './go-cqhttp/GetGroupFileSystemInfo' import { GetGroupFileSystemInfo } from './go-cqhttp/GetGroupFileSystemInfo'
import { GetCredentials } from './system/GetCredentials' import { GetCredentials } from './system/GetCredentials'
import { SetGroupSpecialTitle } from '@/onebot11/action/go-cqhttp/SetGroupSpecialTitle'
import { SendGroupSign } from '@/onebot11/action/go-cqhttp/SendGroupSign'
export function initActionMap(adapter: Adapter) { export function initActionMap(adapter: Adapter) {
const actionHandlers = [ const actionHandlers = [
@ -161,6 +163,8 @@ export function initActionMap(adapter: Adapter) {
new DeleteFriend(adapter), new DeleteFriend(adapter),
new OCRImage(adapter), new OCRImage(adapter),
new GetGroupFileSystemInfo(adapter), new GetGroupFileSystemInfo(adapter),
new SetGroupSpecialTitle(adapter),
new SendGroupSign(adapter),
] ]
const actionMap = new Map() const actionMap = new Map()
for (const action of actionHandlers) { for (const action of actionHandlers) {

View File

@ -13,13 +13,15 @@ export class FriendPoke extends BaseAction<Payload, null> {
}) })
async _handle(payload: Payload) { async _handle(payload: Payload) {
if (!this.ctx.app.native.checkPlatform()) { // if (!this.ctx.app.native.checkPlatform()) {
throw new Error('当前系统平台或架构不支持') // throw new Error('当前系统平台或架构不支持')
} // }
if (!this.ctx.app.native.checkVersion()) { // if (!this.ctx.app.native.checkVersion()) {
throw new Error(`当前 QQ 版本 ${getBuildVersion()} 不支持,可尝试其他版本 27333—27597`) // throw new Error(`当前 QQ 版本 ${getBuildVersion()} 不支持,可尝试其他版本 27333—27597`)
} // }
await this.ctx.app.native.sendFriendPoke(+payload.user_id) // await this.ctx.app.native.sendFriendPoke(+payload.user_id)
await this.ctx.app.ntqqPacketApi.sendPokePacket(+payload.user_id)
return null return null
} }
} }

View File

@ -1,6 +1,7 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils/misc' import { getBuildVersion } from '@/common/utils/misc'
import {NTQQPacketApi} from '@/ntqqapi/native/napcat-protocol-packet'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
@ -15,13 +16,14 @@ export class GroupPoke extends BaseAction<Payload, null> {
}) })
async _handle(payload: Payload) { async _handle(payload: Payload) {
if (!this.ctx.app.native.checkPlatform()) { // if (!this.ctx.app.native.checkPlatform()) {
throw new Error('当前系统平台或架构不支持') // throw new Error('当前系统平台或架构不支持')
} // }
if (!this.ctx.app.native.checkVersion()) { // if (!this.ctx.app.native.checkVersion()) {
throw new Error(`当前 QQ 版本 ${getBuildVersion()} 不支持,可尝试其他版本 27333—27597`) // throw new Error(`当前 QQ 版本 ${getBuildVersion()} 不支持,可尝试其他版本 27333—27597`)
} // }
await this.ctx.app.native.sendGroupPoke(+payload.group_id, +payload.user_id) // await this.ctx.app.native.sendGroupPoke(+payload.group_id, +payload.user_id)
await this.ctx.app.ntqqPacketApi.sendPokePacket(+payload.user_id, +payload.group_id)
return null return null
} }
} }

View File

@ -91,4 +91,6 @@ export enum ActionName {
GoCQHTTP_DeleteFriend = 'delete_friend', GoCQHTTP_DeleteFriend = 'delete_friend',
GoCQHTTP_OCRImage = 'ocr_image', GoCQHTTP_OCRImage = 'ocr_image',
GoCQHTTP_GetGroupFileSystemInfo = 'get_group_file_system_info', GoCQHTTP_GetGroupFileSystemInfo = 'get_group_file_system_info',
GoCQHTTP_SetGroupSpecialTitle = 'set_group_special_title',
GoCQHTTP_SendGroupSign = 'send_group_sign',
} }

View File

@ -1 +1 @@
export const version = '4.1.4' export const version = '4.2.0'