Merge pull request #463 from LLOneBot/dev

release: 4.0.4
This commit is contained in:
idranme 2024-10-11 18:22:37 +08:00 committed by GitHub
commit e988908784
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 201 additions and 91 deletions

View File

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

View File

@ -24,7 +24,7 @@
"cordis": "^3.18.1", "cordis": "^3.18.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cosmokit": "^1.6.3", "cosmokit": "^1.6.3",
"express": "^5.0.0", "express": "^5.0.1",
"fast-xml-parser": "^4.5.0", "fast-xml-parser": "^4.5.0",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"minato": "^3.6.0", "minato": "^3.6.0",
@ -42,7 +42,7 @@
"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.2", "typescript": "^5.6.3",
"vite": "^5.4.8", "vite": "^5.4.8",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },

View File

@ -34,7 +34,8 @@ export class ConfigUtil {
enableWsReverse: false, enableWsReverse: false,
messagePostFormat: 'array', messagePostFormat: 'array',
enableHttpHeart: false, enableHttpHeart: false,
listenLocalhost: false listenLocalhost: false,
reportSelfMessage: false
} }
const satoriDefault: SatoriConfig = { const satoriDefault: SatoriConfig = {
enable: true, enable: true,
@ -50,7 +51,6 @@ export class ConfigUtil {
enableLocalFile2Url: false, enableLocalFile2Url: false,
debug: false, debug: false,
log: false, log: false,
reportSelfMessage: false,
autoDeleteFile: false, autoDeleteFile: false,
autoDeleteFileSecond: 60, autoDeleteFileSecond: 60,
musicSignUrl: '', musicSignUrl: '',
@ -73,6 +73,7 @@ export class ConfigUtil {
this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http') this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http')
this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts') this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts')
this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort') this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort')
this.checkOldConfig(jsonData.ob11, jsonData, 'reportSelfMessage', 'reportSelfMessage')
this.config = jsonData this.config = jsonData
return this.config return this.config
} }
@ -86,8 +87,8 @@ export class ConfigUtil {
private checkOldConfig( private checkOldConfig(
currentConfig: OB11Config, currentConfig: OB11Config,
oldConfig: Config, oldConfig: Config,
currentKey: 'httpPort' | 'httpHosts' | 'wsPort', currentKey: 'httpPort' | 'httpHosts' | 'wsPort' | 'reportSelfMessage',
oldKey: 'http' | 'hosts' | 'wsPort', oldKey: 'http' | 'hosts' | 'wsPort' | 'reportSelfMessage',
) { ) {
// 迁移旧的配置到新配置,避免用户重新填写配置 // 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey] const oldValue = oldConfig[oldKey]

View File

@ -17,6 +17,7 @@ export interface OB11Config {
*/ */
enableQOAutoQuote?: boolean enableQOAutoQuote?: boolean
listenLocalhost: boolean listenLocalhost: boolean
reportSelfMessage: boolean
} }
export interface SatoriConfig { export interface SatoriConfig {
@ -33,7 +34,6 @@ export interface Config {
heartInterval: number // ms heartInterval: number // ms
enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64 enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64
debug?: boolean debug?: boolean
reportSelfMessage?: boolean
log?: boolean log?: boolean
autoDeleteFile?: boolean autoDeleteFile?: boolean
autoDeleteFileSecond?: number autoDeleteFileSecond?: number
@ -50,6 +50,8 @@ export interface Config {
wsPort?: string wsPort?: string
/** @deprecated */ /** @deprecated */
enableLLOB?: boolean enableLLOB?: boolean
/** @deprecated */
reportSelfMessage?: boolean
} }
export interface CheckVersion { export interface CheckVersion {

View File

@ -53,7 +53,6 @@ function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath?
} }
export async function encodeSilk(ctx: Context, filePath: string) { export async function encodeSilk(ctx: Context, filePath: string) {
try {
const file = await fsPromise.readFile(filePath) const file = await fsPromise.readFile(filePath)
if (!isSilk(file)) { if (!isSilk(file)) {
ctx.logger.info(`语音文件${filePath}需要转换成silk`) ctx.logger.info(`语音文件${filePath}需要转换成silk`)
@ -93,10 +92,6 @@ export async function encodeSilk(ctx: Context, filePath: string) {
duration, duration,
} }
} }
} catch (err) {
ctx.logger.error('convert silk failed', (err as Error).stack)
return {}
}
} }
type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'

View File

@ -183,7 +183,6 @@ function onLoad() {
heartInterval: config.heartInterval, heartInterval: config.heartInterval,
token: config.token!, token: config.token!,
debug: config.debug!, debug: config.debug!,
reportSelfMessage: config.reportSelfMessage!,
musicSignUrl: config.musicSignUrl, musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!, enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg, ffmpeg: config.ffmpeg,

View File

@ -201,6 +201,16 @@ export class NTQQFileApi extends Service {
) )
return data.notifyInfo.filePath return data.notifyInfo.filePath
} }
async ocrImage(path: string) {
return await invoke(
'nodeIKernelNodeMiscService/wantWinScreenOCR',
[
{ url: path },
{ timeout: 5000 }
]
)
}
} }
export class NTQQFileCacheApi extends Service { export class NTQQFileCacheApi extends Service {

View File

@ -16,7 +16,7 @@ export class NTQQFriendApi extends Service {
/** 大于或等于 26702 应使用 getBuddyV2 */ /** 大于或等于 26702 应使用 getBuddyV2 */
async getFriends() { async getFriends() {
const data = await invoke<{ const res = await invoke<{
data: { data: {
categoryId: number categoryId: number
categroyName: string categroyName: string
@ -28,11 +28,7 @@ export class NTQQFriendApi extends Service {
cbCmd: ReceiveCmdS.FRIENDS, cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false afterFirstCmd: false
}) })
const _friends: Friend[] = [] return res.data.flatMap(e => e.buddyList)
for (const item of data.data) {
_friends.push(...item.buddyList)
}
return _friends
} }
async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) { async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) {

View File

@ -296,4 +296,30 @@ export class NTQQGroupApi extends Service {
async setGroupAvatar(groupCode: string, path: string) { async setGroupAvatar(groupCode: string, path: string) {
return await invoke('nodeIKernelGroupService/setHeader', [{ path, groupCode }]) return await invoke('nodeIKernelGroupService/setHeader', [{ path, groupCode }])
} }
async searchMember(groupCode: string, keyword: string) {
await invoke('nodeIKernelGroupListener/onSearchMemberChange', [], {
registerEvent: true
})
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{
groupCode,
scene: 'groupMemberList_MainWindow'
}])
const data = await invoke<{
sceneId: string
keyword: string
infos: Map<string, GroupMember>
}>(
'nodeIKernelGroupService/searchMember',
[{ sceneId, keyword }],
{
cbCmd: 'nodeIKernelGroupListener/onSearchMemberChange',
cmdCB: payload => {
return payload.sceneId === sceneId && payload.keyword === keyword
},
afterFirstCmd: false
}
)
return data.infos
}
} }

View File

@ -108,18 +108,23 @@ export class NTQQUserApi extends Service {
async getUidByUinV1(uin: string, groupCode?: string) { async getUidByUinV1(uin: string, groupCode?: string) {
let uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin) let uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
if (!uid) { if (!uid) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换 const friends = await this.ctx.ntFriendApi.getFriends()
if (unveifyUid.indexOf('*') === -1) {
uid = unveifyUid
}
}
if (!uid) {
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
uid = friends.find(item => item.uin === uin)?.uid uid = friends.find(item => item.uin === uin)?.uid
} }
if (!uid && groupCode) { if (!uid && groupCode) {
const members = await this.ctx.ntGroupApi.getGroupMembers(groupCode) let member = await this.ctx.ntGroupApi.searchMember(groupCode, uin)
uid = Array.from(members.values()).find(e => e.uin === uin)?.uid if (member.size === 0) {
await this.ctx.ntGroupApi.getGroupMembers(groupCode, 1)
await this.ctx.sleep(30)
member = await this.ctx.ntGroupApi.searchMember(groupCode, uin)
}
uid = member.values().find(e => e.uin === uin)?.uid
}
if (!uid) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid
if (!unveifyUid.includes('*')) {
uid = unveifyUid
}
} }
return uid return uid
} }

View File

@ -205,9 +205,6 @@ class Core extends Service {
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => {
if (!this.config.reportSelfMessage) {
return
}
sentMsgIds.set(payload.msgRecord.msgId, true) sentMsgIds.set(payload.msgRecord.msgId, true)
}) })

View File

@ -210,13 +210,9 @@ export namespace SendElement {
export async function ptt(ctx: Context, pttPath: string): Promise<SendPttElement> { export async function ptt(ctx: Context, pttPath: string): Promise<SendPttElement> {
const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath) const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath)
if (!silkPath) {
throw '语音转换失败, 请检查语音文件是否正常'
}
// log("生成语音", silkPath, duration);
const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.Ptt) const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.Ptt)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常,大小为0' throw new Error('文件异常,大小为 0')
} }
if (converted) { if (converted) {
unlink(silkPath) unlink(silkPath)

View File

@ -14,7 +14,8 @@ import {
NodeIKernelRichMediaService, NodeIKernelRichMediaService,
NodeIKernelTicketService, NodeIKernelTicketService,
NodeIKernelTipOffService, NodeIKernelTipOffService,
NodeIKernelRobotService NodeIKernelRobotService,
NodeIKernelNodeMiscService
} from './services' } from './services'
export enum NTClass { export enum NTClass {
@ -94,6 +95,7 @@ interface NTService {
nodeIKernelTicketService: NodeIKernelTicketService nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService nodeIKernelTipOffService: NodeIKernelTipOffService
nodeIKernelRobotService: NodeIKernelRobotService nodeIKernelRobotService: NodeIKernelRobotService
nodeIKernelNodeMiscService: NodeIKernelNodeMiscService
} }
interface InvokeOptions<ReturnType> { interface InvokeOptions<ReturnType> {

View File

@ -125,4 +125,6 @@ export interface NodeIKernelGroupService {
removeGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown> removeGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
setHeader(args: unknown[]): Promise<GeneralCallResult> setHeader(args: unknown[]): Promise<GeneralCallResult>
searchMember(sceneId: string, keyword: string): Promise<void>
} }

View File

@ -0,0 +1,15 @@
export interface NodeIKernelNodeMiscService {
wantWinScreenOCR(...args: unknown[]): Promise<{
code: number
errMsg: string
result: {
text: string
[key: `pt${number}`]: {
x: string
y: string
}
charBox: unknown[]
score: ''
}[]
}>
}

View File

@ -10,3 +10,4 @@ export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService' export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService' export * from './NodeIKernelTipOffService'
export * from './NodeIKernelRobotService' export * from './NodeIKernelRobotService'
export * from './NodeIKernelNodeMiscService'

View File

@ -0,0 +1,63 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { uri2local } from '@/common/utils/file'
import { access, unlink } from 'node:fs/promises'
interface Payload {
image: string
}
interface TextDetection {
text: string
confidence: number
coordinates: {
x: number //int32
y: number
}[]
}
interface Response {
texts: TextDetection[]
language: string
}
export class OCRImage extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_OCRImage
payloadSchema = Schema.object({
image: Schema.string().required()
})
protected async _handle(payload: Payload) {
const { errMsg, isLocal, path, success } = await uri2local(this.ctx, payload.image, true)
if (!success) {
throw new Error(errMsg)
}
await access(path)
const data = await this.ctx.ntFileApi.ocrImage(path)
if (!isLocal) {
unlink(path)
}
const texts = data.result.map(item => {
const ret: TextDetection = {
text: item.text,
confidence: 1,
coordinates: []
}
for (let i = 0; i < 4; i++) {
const pt = item[`pt${i + 1}`]
ret.coordinates.push({
x: parseInt(pt.x),
y: parseInt(pt.y)
})
}
return ret
})
return {
texts,
language: ''
}
}
}

View File

@ -73,6 +73,7 @@ import { GetGroupFileUrl } from './go-cqhttp/GetGroupFileUrl'
import { GetGroupNotice } from './go-cqhttp/GetGroupNotice' import { GetGroupNotice } from './go-cqhttp/GetGroupNotice'
import { GetRobotUinRange } from './llonebot/GetRobotUinRange' import { GetRobotUinRange } from './llonebot/GetRobotUinRange'
import { DeleteFriend } from './go-cqhttp/DeleteFriend' import { DeleteFriend } from './go-cqhttp/DeleteFriend'
import { OCRImage } from './go-cqhttp/OCRImage'
export function initActionMap(adapter: Adapter) { export function initActionMap(adapter: Adapter) {
const actionHandlers = [ const actionHandlers = [
@ -151,6 +152,7 @@ export function initActionMap(adapter: Adapter) {
new GetGroupFileUrl(adapter), new GetGroupFileUrl(adapter),
new GetGroupNotice(adapter), new GetGroupNotice(adapter),
new DeleteFriend(adapter), new DeleteFriend(adapter),
new OCRImage(adapter),
] ]
const actionMap = new Map<string, BaseAction<any, unknown>>() const actionMap = new Map<string, BaseAction<any, unknown>>()
for (const action of actionHandlers) { for (const action of actionHandlers) {

View File

@ -86,4 +86,5 @@ export enum ActionName {
GoCQHTTP_GetGroupFileUrl = 'get_group_file_url', GoCQHTTP_GetGroupFileUrl = 'get_group_file_url',
GoCQHTTP_GetGroupNotice = '_get_group_notice', GoCQHTTP_GetGroupNotice = '_get_group_notice',
GoCQHTTP_DeleteFriend = 'delete_friend', GoCQHTTP_DeleteFriend = 'delete_friend',
GoCQHTTP_OCRImage = 'ocr_image',
} }

View File

@ -173,26 +173,23 @@ class OneBot11Adapter extends Service {
return return
} }
const isSelfMsg = msg.user_id.toString() === selfInfo.uin const isSelfMsg = msg.user_id.toString() === selfInfo.uin
if (isSelfMsg && !this.config.reportSelfMessage) {
return
}
if (isSelfMsg) { if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin) msg.target_id = parseInt(message.peerUin)
} }
this.dispatch(msg) this.dispatch(msg)
}).catch(e => this.ctx.logger.error('constructMessage error: ', e.stack.toString())) }).catch(e => this.ctx.logger.error('handling incoming messages', e))
OB11Entities.groupEvent(this.ctx, message).then(groupEvent => { OB11Entities.groupEvent(this.ctx, message).then(groupEvent => {
if (groupEvent) { if (groupEvent) {
this.dispatch(groupEvent) this.dispatch(groupEvent)
} }
}) }).catch(e => this.ctx.logger.error('handling incoming group events', e))
OB11Entities.privateEvent(this.ctx, message).then(privateEvent => { OB11Entities.privateEvent(this.ctx, message).then(privateEvent => {
if (privateEvent) { if (privateEvent) {
this.dispatch(privateEvent) this.dispatch(privateEvent)
} }
}) }).catch(e => this.ctx.logger.error('handling incoming buddy events', e))
} }
private handleRecallMsg(message: RawMessage) { private handleRecallMsg(message: RawMessage) {
@ -310,7 +307,6 @@ class OneBot11Adapter extends Service {
heartInterval: config.heartInterval, heartInterval: config.heartInterval,
token: config.token, token: config.token,
debug: config.debug, debug: config.debug,
reportSelfMessage: config.reportSelfMessage,
msgCacheExpire: config.msgCacheExpire, msgCacheExpire: config.msgCacheExpire,
musicSignUrl: config.musicSignUrl, musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url, enableLocalFile2Url: config.enableLocalFile2Url,
@ -341,6 +337,9 @@ class OneBot11Adapter extends Service {
this.handleRecallMsg(input) this.handleRecallMsg(input)
}) })
this.ctx.on('nt/message-sent', input => { this.ctx.on('nt/message-sent', input => {
if (!this.config.reportSelfMessage) {
return
}
this.handleMsg(input) this.handleMsg(input)
}) })
this.ctx.on('nt/group-notify', input => { this.ctx.on('nt/group-notify', input => {
@ -370,7 +369,6 @@ namespace OneBot11Adapter {
heartInterval: number heartInterval: number
token: string token: string
debug: boolean debug: boolean
reportSelfMessage: boolean
musicSignUrl?: string musicSignUrl?: string
enableLocalFile2Url: boolean enableLocalFile2Url: boolean
ffmpeg?: string ffmpeg?: string

View File

@ -66,15 +66,10 @@ export async function createSendElements(
} }
} }
else if (peer.chatType === ChatType.Group) { else if (peer.chatType === ChatType.Group) {
const uid = await ctx.ntUserApi.getUidByUin(atQQ) ?? '' const uid = await ctx.ntUserApi.getUidByUin(atQQ, peer.peerUid) ?? ''
let display = '' let display = ''
if (sendMsg.data.name) { if (sendMsg.data.name) {
display = `@${sendMsg.data.name}` display = `@${sendMsg.data.name}`
} else {
try {
const member = await ctx.ntGroupApi.getGroupMember(peer.peerUid, uid)
display = `@${member.cardName || member.nick}`
} catch { }
} }
sendElements.push(SendElement.at(atQQ, uid, AtType.One, display)) sendElements.push(SendElement.at(atQQ, uid, AtType.One, display))
} }

View File

@ -171,7 +171,7 @@ async function onSettingWindowCreated(view: Element) {
SettingItem( SettingItem(
'上报 Bot 自身发送的消息', '上报 Bot 自身发送的消息',
'上报 event 为 message_sent', '上报 event 为 message_sent',
SettingSwitch('reportSelfMessage', config.reportSelfMessage), SettingSwitch('ob11.reportSelfMessage', config.ob11.reportSelfMessage),
), ),
SettingItem( SettingItem(
'使用 Base64 编码获取文件', '使用 Base64 编码获取文件',

View File

@ -175,10 +175,14 @@ export class MessageEncoder {
if (type === 'text') { if (type === 'text') {
this.elements.push(SendElement.text(attrs.content)) this.elements.push(SendElement.text(attrs.content))
} else if (type === 'at') { } else if (type === 'at') {
this.peer ??= await getPeer(this.ctx, this.channelId)
if (this.peer.chatType !== NT.ChatType.Group) {
return
}
if (attrs.type === 'all') { if (attrs.type === 'all') {
this.elements.push(SendElement.at('', '', NT.AtType.All, '@全体成员')) this.elements.push(SendElement.at('', '', NT.AtType.All, '@全体成员'))
} else { } else {
const uid = await this.ctx.ntUserApi.getUidByUin(attrs.id) ?? '' const uid = await this.ctx.ntUserApi.getUidByUin(attrs.id, this.peer.peerUid) ?? ''
const display = attrs.name ? '@' + attrs.name : '' const display = attrs.name ? '@' + attrs.name : ''
this.elements.push(SendElement.at(attrs.id, uid, NT.AtType.One, display)) this.elements.push(SendElement.at(attrs.id, uid, NT.AtType.One, display))
} }

View File

@ -1 +1 @@
export const version = '4.0.3' export const version = '4.0.4'