Compare commits

...

35 Commits

Author SHA1 Message Date
idranme
8239e9a243 Merge pull request #477 from LLOneBot/dev
release: 4.0.11
2024-10-16 11:44:43 +08:00
idranme
50e5f89f4f chore: bump versions 2024-10-16 11:43:14 +08:00
idranme
be2119a1e6 feat: add save button 2024-10-16 11:32:50 +08:00
idranme
951afea794 Merge pull request #475 from LLOneBot/dev
release: 4.0.10
2024-10-15 21:09:42 +08:00
idranme
0946d9652e chore: bump versions 2024-10-15 20:54:57 +08:00
idranme
a66e48dfb0 optimize 2024-10-15 20:53:25 +08:00
idranme
029842ca08 fix(onebot): group_increase event 2024-10-15 20:22:39 +08:00
idranme
39fda24799 fix: config hot update 2024-10-15 11:51:00 +08:00
idranme
a4beeba528 Merge pull request #474 from LLOneBot/dev
release: 4.0.9
2024-10-13 23:51:30 +08:00
idranme
c837e970df chore: bump versions 2024-10-13 23:48:50 +08:00
idranme
4ced7fa3cf fix 2024-10-13 23:48:22 +08:00
idranme
dbd71d4376 chore 2024-10-13 23:42:01 +08:00
idranme
d43612b2a3 feat(satori): support for receiving llonebot:ark element 2024-10-13 20:31:20 +08:00
idranme
31ad0195d8 fix(onebot): get_group_msg_history API 2024-10-13 19:52:29 +08:00
idranme
9b32140f87 Merge pull request #470 from LLOneBot/dev
release: 4.0.8
2024-10-13 16:36:12 +08:00
idranme
dc5982c6b2 chore: bump versions 2024-10-13 16:34:35 +08:00
idranme
ce46c99330 fix 2024-10-13 16:32:39 +08:00
idranme
d3abaf806f optimize 2024-10-13 16:32:18 +08:00
idranme
e10a67ce05 Merge pull request #468 from LLOneBot/dev
release: 4.0.7
2024-10-13 01:30:01 +08:00
idranme
c8e897abdb chore: bump versions 2024-10-13 01:29:19 +08:00
idranme
e07c06f3e9 Merge pull request #467 from LLOneBot/dev
...
2024-10-13 01:17:06 +08:00
idranme
9c694a11b5 fix: poke 2024-10-13 01:12:12 +08:00
idranme
6e3bb7c9cf fix 2024-10-13 01:10:39 +08:00
idranme
0d8d3ac24f Merge pull request #466 from LLOneBot/dev
release: 4.0.6
2024-10-13 00:48:36 +08:00
idranme
c96d032820 chore: bump versions 2024-10-13 00:47:31 +08:00
idranme
837f48b63a fix: poke 2024-10-13 00:45:30 +08:00
idranme
9d0f9e7096 Merge pull request #465 from LLOneBot/dev
release: 4.0.5
2024-10-12 23:58:07 +08:00
idranme
801d79d79d chore: bump versions 2024-10-12 23:55:32 +08:00
idranme
0d5640046c feat: poke 2024-10-12 23:50:58 +08:00
idranme
e988908784 Merge pull request #463 from LLOneBot/dev
release: 4.0.4
2024-10-11 18:22:37 +08:00
idranme
1cfa736dd5 chore: bump versions 2024-10-11 18:18:51 +08:00
idranme
0081b0b124 refactor 2024-10-11 18:17:52 +08:00
idranme
ba565e7c38 feat(onebot): ocr_image API 2024-10-11 17:42:19 +08:00
idranme
abb468c3f8 optimize 2024-10-11 13:38:59 +08:00
idranme
433a175809 fix: at element 2024-10-11 12:47:12 +08:00
45 changed files with 770 additions and 254 deletions

7
.gitattributes vendored
View File

@@ -1,7 +0,0 @@
* text eol=lf
*.png -text
*.jpg -text
*.ico -text
*.gif -text
*.webp -text

View File

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

View File

@@ -12,7 +12,7 @@
"deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %LITELOADERQQNT_PROFILE%\\plugins\\LLOneBot\\\"",
"format": "prettier -cw .",
"check": "tsc",
"compile:proto": "pbjs --no-create --no-convert --no-encode --no-verify -t static-module -w es6 -p src/ntqqapi/proto -o src/ntqqapi/proto/compiled.js systemMessage.proto profileLikeTip.proto && pbts -o src/ntqqapi/proto/compiled.d.ts src/ntqqapi/proto/compiled.js"
"compile:proto": "pbjs --no-create --no-convert --no-encode --no-verify -t static-module -w es6 -p src/ntqqapi/proto -o src/ntqqapi/proto/compiled.js systemMessage.proto profileLikeTip.proto groupMemberIncrease.proto && pbts -o src/ntqqapi/proto/compiled.d.ts src/ntqqapi/proto/compiled.js"
},
"author": "",
"license": "MIT",
@@ -24,7 +24,7 @@
"cordis": "^3.18.1",
"cors": "^2.8.5",
"cosmokit": "^1.6.3",
"express": "^5.0.0",
"express": "^5.0.1",
"fast-xml-parser": "^4.5.0",
"fluent-ffmpeg": "^2.1.3",
"minato": "^3.6.0",
@@ -42,8 +42,8 @@
"electron": "^31.4.0",
"electron-vite": "^2.3.0",
"protobufjs-cli": "^1.1.3",
"typescript": "^5.6.2",
"vite": "^5.4.8",
"typescript": "^5.6.3",
"vite": "^5.4.9",
"vite-plugin-cp": "^4.0.8"
},
"packageManager": "yarn@4.5.0"

View File

@@ -1,6 +1,5 @@
export const CHANNEL_GET_CONFIG = 'llonebot_get_config'
export const CHANNEL_SET_CONFIG = 'llonebot_set_config'
export const CHANNEL_SET_CONFIG_CONFIRMED = 'llonebot_set_config_confirmed'
export const CHANNEL_LOG = 'llonebot_log'
export const CHANNEL_ERROR = 'llonebot_error'
export const CHANNEL_UPDATE = 'llonebot_update'

View File

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

View File

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

View File

@@ -53,49 +53,44 @@ function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath?
}
export async function encodeSilk(ctx: Context, filePath: string) {
try {
const file = await fsPromise.readFile(filePath)
if (!isSilk(file)) {
ctx.logger.info(`语音文件${filePath}需要转换成silk`)
let result: EncodeResult
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
result = await encode(file, 0)
} else {
const input = await convert(ctx, filePath, {
output: [
'-ar 24000',
'-ac 1',
'-f s16le'
]
})
result = await encode(input, 24000)
}
const pttPath = path.join(TEMP_DIR, randomUUID())
await fsPromise.writeFile(pttPath, result.data)
ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return {
converted: true,
path: pttPath,
duration: result.duration / 1000,
}
const file = await fsPromise.readFile(filePath)
if (!isSilk(file)) {
ctx.logger.info(`语音文件${filePath}需要转换成silk`)
let result: EncodeResult
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
result = await encode(file, 0)
} else {
const silk = file
let duration = 1
try {
duration = getDuration(silk) / 1000
} catch (e) {
ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, (e as Error).stack)
}
return {
converted: false,
path: filePath,
duration,
}
const input = await convert(ctx, filePath, {
output: [
'-ar 24000',
'-ac 1',
'-f s16le'
]
})
result = await encode(input, 24000)
}
const pttPath = path.join(TEMP_DIR, randomUUID())
await fsPromise.writeFile(pttPath, result.data)
ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return {
converted: true,
path: pttPath,
duration: result.duration / 1000,
}
} else {
const silk = file
let duration = 1
try {
duration = getDuration(silk) / 1000
} catch (e) {
ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, (e as Error).stack)
}
return {
converted: false,
path: filePath,
duration,
}
} catch (err) {
ctx.logger.error('convert silk failed', (err as Error).stack)
return {}
}
}

View File

@@ -15,8 +15,7 @@ import {
CHANNEL_LOG,
CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG,
CHANNEL_UPDATE,
CHANNEL_SET_CONFIG_CONFIRMED
CHANNEL_UPDATE
} from '../common/channels'
import { startHook } from '../ntqqapi/hook'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
@@ -35,7 +34,6 @@ import {
NTQQWebApi,
NTQQWindowApi
} from '../ntqqapi/api'
import { mkdir } from 'node:fs/promises'
import { existsSync, mkdirSync } from 'node:fs'
declare module 'cordis' {
@@ -56,6 +54,29 @@ function onLoad() {
mkdirSync(LOG_DIR)
}
if (!existsSync(TEMP_DIR)) {
mkdirSync(TEMP_DIR)
}
const dbDir = path.join(DATA_DIR, 'database')
if (!existsSync(dbDir)) {
mkdirSync(dbDir)
}
const ctx = new Context()
ctx.plugin(NTQQFileApi)
ctx.plugin(NTQQFileCacheApi)
ctx.plugin(NTQQFriendApi)
ctx.plugin(NTQQGroupApi)
ctx.plugin(NTQQMsgApi)
ctx.plugin(NTQQUserApi)
ctx.plugin(NTQQWebApi)
ctx.plugin(NTQQWindowApi)
ctx.plugin(Database)
let started = false
ipcMain.handle(CHANNEL_CHECK_VERSION, async () => {
return checkNewVersion()
})
@@ -114,7 +135,9 @@ function onLoad() {
if (!ask) {
getConfigUtil().setConfig(config)
log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then()
if (started) {
ctx.parallel('llob/config-updated', config)
}
resolve(true)
return
}
@@ -131,7 +154,9 @@ function onLoad() {
if (result.response === 0) {
getConfigUtil().setConfig(config)
log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then()
if (started) {
ctx.parallel('llob/config-updated', config)
}
resolve(true)
}
})
@@ -146,62 +171,6 @@ function onLoad() {
log(arg)
})
async function start() {
log('process pid', process.pid)
const config = getConfigUtil().getConfig()
if (!existsSync(TEMP_DIR)) {
await mkdir(TEMP_DIR)
}
const dbDir = path.join(DATA_DIR, 'database')
if (!existsSync(dbDir)) {
await mkdir(dbDir)
}
const ctx = new Context()
ctx.plugin(Log, {
enable: config.log!,
filename: logFileName
})
ctx.plugin(NTQQFileApi)
ctx.plugin(NTQQFileCacheApi)
ctx.plugin(NTQQFriendApi)
ctx.plugin(NTQQGroupApi)
ctx.plugin(NTQQMsgApi)
ctx.plugin(NTQQUserApi)
ctx.plugin(NTQQWebApi)
ctx.plugin(NTQQWindowApi)
ctx.plugin(Core, config)
ctx.plugin(Database)
ctx.plugin(SQLiteDriver, {
path: path.join(dbDir, `${selfInfo.uin}.db`)
})
ctx.plugin(Store, {
msgCacheExpire: config.msgCacheExpire! * 1000
})
if (config.ob11.enable) {
ctx.plugin(OneBot11Adapter, {
...config.ob11,
heartInterval: config.heartInterval,
token: config.token!,
debug: config.debug!,
reportSelfMessage: config.reportSelfMessage!,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg,
})
}
if (config.satori.enable) {
ctx.plugin(SatoriAdapter, {
...config.satori,
ffmpeg: config.ffmpeg,
})
}
ctx.start()
llonebotError.otherError = ''
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
ctx.parallel('llob/config-updated', config)
})
}
const intervalId = setInterval(() => {
const self = Object.assign(selfInfo, {
uin: globalThis.authData?.uin,
@@ -210,7 +179,41 @@ function onLoad() {
})
if (self.uin) {
clearInterval(intervalId)
start()
log('process pid', process.pid)
const config = getConfigUtil().getConfig()
ctx.plugin(Log, {
enable: config.log!,
filename: logFileName
})
ctx.plugin(SQLiteDriver, {
path: path.join(dbDir, `${selfInfo.uin}.db`)
})
ctx.plugin(Store, {
msgCacheExpire: config.msgCacheExpire! * 1000
})
ctx.plugin(Core, config)
if (config.ob11.enable) {
ctx.plugin(OneBot11Adapter, {
...config.ob11,
heartInterval: config.heartInterval,
token: config.token!,
debug: config.debug!,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg,
})
}
if (config.satori.enable) {
ctx.plugin(SatoriAdapter, {
...config.satori,
ffmpeg: config.ffmpeg,
})
}
ctx.start()
started = true
llonebotError.otherError = ''
}
}, 600)
}

View File

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

View File

@@ -16,7 +16,7 @@ export class NTQQFriendApi extends Service {
/** 大于或等于 26702 应使用 getBuddyV2 */
async getFriends() {
const data = await invoke<{
const res = await invoke<{
data: {
categoryId: number
categroyName: string
@@ -28,11 +28,7 @@ export class NTQQFriendApi extends Service {
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false
})
const _friends: Friend[] = []
for (const item of data.data) {
_friends.push(...item.buddyList)
}
return _friends
return res.data.flatMap(e => e.buddyList)
}
async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) {
@@ -62,7 +58,7 @@ export class NTQQFriendApi extends Service {
return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!))
}
/** uid => uin */
/** uid -> uin */
async getBuddyIdMap(refresh = false): Promise<Map<string, string>> {
const retMap: Map<string, string> = new Map()
const data = await invoke<{

View File

@@ -45,8 +45,11 @@ export class NTQQGroupApi extends Service {
return result.groupList
}
async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> {
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
async getGroupMembers(groupCode: string, num = 3000) {
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{
groupCode,
scene: 'groupMemberList_MainWindow'
}])
const data = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }])
if (data.errCode !== 0) {
throw new Error('获取群成员列表出错,' + data.errMsg)
@@ -296,4 +299,30 @@ export class NTQQGroupApi extends Service {
async setGroupAvatar(groupCode: string, path: string) {
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

@@ -52,7 +52,7 @@ export class NTQQMsgApi extends Service {
}
async getMsgHistory(peer: Peer, msgId: string, cnt: number, queryOrder = false) {
// 默认情况下消息时间从旧到
// 默认情况下消息时间从新到旧
return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder }])
}
@@ -93,7 +93,7 @@ export class NTQQMsgApi extends Service {
const uniqueId = await this.generateMsgUniqueId(destPeer.chatType)
destPeer.guildId = uniqueId
const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/forwardMsg',
'nodeIKernelMsgService/forwardMsgWithComment',
[{
msgIds,
srcContact: srcPeer,
@@ -111,7 +111,8 @@ export class NTQQMsgApi extends Service {
}
}
return false
}
},
timeout: 3000
}
)
delete destPeer.guildId
@@ -263,4 +264,8 @@ export class NTQQMsgApi extends Service {
async getServerTime() {
return await invoke('nodeIKernelMSFService/getServerTime', [])
}
async fetchUnitedCommendConfig(groups: string[]) {
return await invoke('nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', [{ groups }])
}
}

View File

@@ -108,18 +108,23 @@ export class NTQQUserApi extends Service {
async getUidByUinV1(uin: string, groupCode?: string) {
let uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
if (!uid) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换
if (unveifyUid.indexOf('*') === -1) {
uid = unveifyUid
}
}
if (!uid) {
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
const friends = await this.ctx.ntFriendApi.getFriends()
uid = friends.find(item => item.uin === uin)?.uid
}
if (!uid && groupCode) {
const members = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
uid = Array.from(members.values()).find(e => e.uin === uin)?.uid
let member = await this.ctx.ntGroupApi.searchMember(groupCode, uin)
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 = Array.from(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
}
@@ -173,7 +178,7 @@ export class NTQQUserApi extends Service {
if (uin) return uin
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
uin = (await this.ctx.ntFriendApi.getBuddyIdMap()).get(uid)
if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin
return uin

View File

@@ -19,6 +19,7 @@ import {
import { selfInfo, llonebotError } from '../common/globalVars'
import { version } from '../version'
import { invoke } from './ntcall'
import { Native } from './native/index'
declare module 'cordis' {
interface Context {
@@ -38,9 +39,11 @@ declare module 'cordis' {
class Core extends Service {
static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi', 'store']
public startTime = 0
public native
constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true)
this.native = new Native(ctx)
}
public start() {
@@ -205,9 +208,6 @@ class Core extends Service {
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => {
if (!this.config.reportSelfMessage) {
return
}
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> {
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)
if (fileSize === 0) {
throw '文件异常大小为0'
throw new Error('文件异常,大小为 0')
}
if (converted) {
unlink(silkPath)

Binary file not shown.

View File

@@ -0,0 +1,60 @@
import { Context } from 'cordis'
import { Dict } from 'cosmokit'
import { getBuildVersion } from '@/common/utils/misc'
import { TEMP_DIR } from '@/common/globalVars'
import { copyFile } from 'fs/promises'
import path from 'node:path'
import addon from './external/crychic-win32-x64.node?asset'
export class Native {
private crychic?: Dict
constructor(private ctx: Context) {
ctx.on('ready', () => {
this.start()
})
}
checkPlatform() {
return process.platform === 'win32' && process.arch === 'x64'
}
checkVersion() {
const version = getBuildVersion()
// 27333—27597
return version >= 27333 && version < 28060
}
async start() {
if (this.crychic) {
return
}
if (!this.checkPlatform()) {
return
}
if (!this.checkVersion()) {
return
}
try {
const fileName = path.basename(addon)
const dest = path.join(TEMP_DIR, fileName)
await copyFile(addon, dest)
this.crychic = require(dest)
this.crychic!.init()
} catch (e) {
this.ctx.logger.warn('crychic 加载失败', e)
}
}
async sendFriendPoke(uin: number) {
if (!this.crychic) return
this.crychic.sendFriendPoke(uin)
await this.ctx.ntMsgApi.fetchUnitedCommendConfig(['100243'])
}
async sendGroupPoke(groupCode: number, memberUin: number) {
if (!this.crychic) return
this.crychic.sendGroupPoke(memberUin, groupCode)
await this.ctx.ntMsgApi.fetchUnitedCommendConfig(['100243'])
}
}

View File

@@ -14,7 +14,8 @@ import {
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService,
NodeIKernelRobotService
NodeIKernelRobotService,
NodeIKernelNodeMiscService
} from './services'
export enum NTClass {
@@ -94,6 +95,7 @@ interface NTService {
nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService
nodeIKernelRobotService: NodeIKernelRobotService
nodeIKernelNodeMiscService: NodeIKernelNodeMiscService
}
interface InvokeOptions<ReturnType> {
@@ -122,7 +124,12 @@ export function invoke<
return new Promise<R>((resolve, reject) => {
const apiArgs = [method, ...args]
const callbackId = randomUUID()
let eventId: string
const timeoutId = setTimeout(() => {
if (eventId) {
removeReceiveHook(eventId)
}
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, args)
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${JSON.stringify(args)}`)
}, timeout)
@@ -138,16 +145,16 @@ export function invoke<
let result: unknown
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => {
const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
eventId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
if (options.cmdCB) {
if (options.cmdCB(payload, result)) {
removeReceiveHook(hookId)
removeReceiveHook(eventId)
clearTimeout(timeoutId)
resolve(payload)
}
}
else {
removeReceiveHook(hookId)
removeReceiveHook(eventId)
clearTimeout(timeoutId)
resolve(payload)
}

View File

@@ -64,11 +64,8 @@ export namespace SysMsg {
/** Properties of a SystemMessageHeader. */
interface ISystemMessageHeader {
/** SystemMessageHeader peerNumber */
peerNumber?: (number|null);
/** SystemMessageHeader peerString */
peerString?: (string|null);
/** SystemMessageHeader peerUin */
peerUin?: (number|null);
/** SystemMessageHeader uin */
uin?: (number|null);
@@ -86,11 +83,8 @@ export namespace SysMsg {
*/
constructor(properties?: SysMsg.ISystemMessageHeader);
/** SystemMessageHeader peerNumber. */
public peerNumber: number;
/** SystemMessageHeader peerString. */
public peerString: string;
/** SystemMessageHeader peerUin. */
public peerUin: number;
/** SystemMessageHeader uin. */
public uin: number;
@@ -160,10 +154,10 @@ export namespace SysMsg {
public msgType: number;
/** SystemMessageMsgSpec subType. */
public subType: number;
public subType?: (number|null);
/** SystemMessageMsgSpec subSubType. */
public subSubType: number;
public subSubType?: (number|null);
/** SystemMessageMsgSpec msgSeq. */
public msgSeq: number;
@@ -172,7 +166,7 @@ export namespace SysMsg {
public time: number;
/** SystemMessageMsgSpec other. */
public other: number;
public other?: (number|null);
/**
* Decodes a SystemMessageMsgSpec message from the specified reader or buffer.
@@ -466,4 +460,68 @@ export namespace SysMsg {
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a GroupMemberIncrease. */
interface IGroupMemberIncrease {
/** GroupMemberIncrease groupCode */
groupCode?: (number|null);
/** GroupMemberIncrease memberUid */
memberUid?: (string|null);
/** GroupMemberIncrease type */
type?: (number|null);
/** GroupMemberIncrease adminUid */
adminUid?: (string|null);
}
/** Represents a GroupMemberIncrease. */
class GroupMemberIncrease implements IGroupMemberIncrease {
/**
* Constructs a new GroupMemberIncrease.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.IGroupMemberIncrease);
/** GroupMemberIncrease groupCode. */
public groupCode: number;
/** GroupMemberIncrease memberUid. */
public memberUid: string;
/** GroupMemberIncrease type. */
public type: number;
/** GroupMemberIncrease adminUid. */
public adminUid: string;
/**
* Decodes a GroupMemberIncrease message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns GroupMemberIncrease
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): SysMsg.GroupMemberIncrease;
/**
* Decodes a GroupMemberIncrease message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns GroupMemberIncrease
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): SysMsg.GroupMemberIncrease;
/**
* Gets the default type url for GroupMemberIncrease
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
}

View File

@@ -150,8 +150,7 @@ export const SysMsg = $root.SysMsg = (() => {
* Properties of a SystemMessageHeader.
* @memberof SysMsg
* @interface ISystemMessageHeader
* @property {number|null} [peerNumber] SystemMessageHeader peerNumber
* @property {string|null} [peerString] SystemMessageHeader peerString
* @property {number|null} [peerUin] SystemMessageHeader peerUin
* @property {number|null} [uin] SystemMessageHeader uin
* @property {string|null} [uid] SystemMessageHeader uid
*/
@@ -172,20 +171,12 @@ export const SysMsg = $root.SysMsg = (() => {
}
/**
* SystemMessageHeader peerNumber.
* @member {number} peerNumber
* SystemMessageHeader peerUin.
* @member {number} peerUin
* @memberof SysMsg.SystemMessageHeader
* @instance
*/
SystemMessageHeader.prototype.peerNumber = 0;
/**
* SystemMessageHeader peerString.
* @member {string} peerString
* @memberof SysMsg.SystemMessageHeader
* @instance
*/
SystemMessageHeader.prototype.peerString = "";
SystemMessageHeader.prototype.peerUin = 0;
/**
* SystemMessageHeader uin.
@@ -231,11 +222,7 @@ export const SysMsg = $root.SysMsg = (() => {
let tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
message.peerNumber = reader.uint32();
break;
}
case 2: {
message.peerString = reader.string();
message.peerUin = reader.uint32();
break;
}
case 5: {
@@ -327,19 +314,19 @@ export const SysMsg = $root.SysMsg = (() => {
/**
* SystemMessageMsgSpec subType.
* @member {number} subType
* @member {number|null|undefined} subType
* @memberof SysMsg.SystemMessageMsgSpec
* @instance
*/
SystemMessageMsgSpec.prototype.subType = 0;
SystemMessageMsgSpec.prototype.subType = null;
/**
* SystemMessageMsgSpec subSubType.
* @member {number} subSubType
* @member {number|null|undefined} subSubType
* @memberof SysMsg.SystemMessageMsgSpec
* @instance
*/
SystemMessageMsgSpec.prototype.subSubType = 0;
SystemMessageMsgSpec.prototype.subSubType = null;
/**
* SystemMessageMsgSpec msgSeq.
@@ -359,11 +346,32 @@ export const SysMsg = $root.SysMsg = (() => {
/**
* SystemMessageMsgSpec other.
* @member {number} other
* @member {number|null|undefined} other
* @memberof SysMsg.SystemMessageMsgSpec
* @instance
*/
SystemMessageMsgSpec.prototype.other = 0;
SystemMessageMsgSpec.prototype.other = null;
// OneOf field names bound to virtual getters and setters
let $oneOfFields;
// Virtual OneOf for proto3 optional field
Object.defineProperty(SystemMessageMsgSpec.prototype, "_subType", {
get: $util.oneOfGetter($oneOfFields = ["subType"]),
set: $util.oneOfSetter($oneOfFields)
});
// Virtual OneOf for proto3 optional field
Object.defineProperty(SystemMessageMsgSpec.prototype, "_subSubType", {
get: $util.oneOfGetter($oneOfFields = ["subSubType"]),
set: $util.oneOfSetter($oneOfFields)
});
// Virtual OneOf for proto3 optional field
Object.defineProperty(SystemMessageMsgSpec.prototype, "_other", {
get: $util.oneOfGetter($oneOfFields = ["other"]),
set: $util.oneOfSetter($oneOfFields)
});
/**
* Decodes a SystemMessageMsgSpec message from the specified reader or buffer.
@@ -1007,6 +1015,141 @@ export const SysMsg = $root.SysMsg = (() => {
return ProfileLikeTip;
})();
SysMsg.GroupMemberIncrease = (function() {
/**
* Properties of a GroupMemberIncrease.
* @memberof SysMsg
* @interface IGroupMemberIncrease
* @property {number|null} [groupCode] GroupMemberIncrease groupCode
* @property {string|null} [memberUid] GroupMemberIncrease memberUid
* @property {number|null} [type] GroupMemberIncrease type
* @property {string|null} [adminUid] GroupMemberIncrease adminUid
*/
/**
* Constructs a new GroupMemberIncrease.
* @memberof SysMsg
* @classdesc Represents a GroupMemberIncrease.
* @implements IGroupMemberIncrease
* @constructor
* @param {SysMsg.IGroupMemberIncrease=} [properties] Properties to set
*/
function GroupMemberIncrease(properties) {
if (properties)
for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i)
if (properties[keys[i]] != null)
this[keys[i]] = properties[keys[i]];
}
/**
* GroupMemberIncrease groupCode.
* @member {number} groupCode
* @memberof SysMsg.GroupMemberIncrease
* @instance
*/
GroupMemberIncrease.prototype.groupCode = 0;
/**
* GroupMemberIncrease memberUid.
* @member {string} memberUid
* @memberof SysMsg.GroupMemberIncrease
* @instance
*/
GroupMemberIncrease.prototype.memberUid = "";
/**
* GroupMemberIncrease type.
* @member {number} type
* @memberof SysMsg.GroupMemberIncrease
* @instance
*/
GroupMemberIncrease.prototype.type = 0;
/**
* GroupMemberIncrease adminUid.
* @member {string} adminUid
* @memberof SysMsg.GroupMemberIncrease
* @instance
*/
GroupMemberIncrease.prototype.adminUid = "";
/**
* Decodes a GroupMemberIncrease message from the specified reader or buffer.
* @function decode
* @memberof SysMsg.GroupMemberIncrease
* @static
* @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
* @param {number} [length] Message length if known beforehand
* @returns {SysMsg.GroupMemberIncrease} GroupMemberIncrease
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
GroupMemberIncrease.decode = function decode(reader, length) {
if (!(reader instanceof $Reader))
reader = $Reader.create(reader);
let end = length === undefined ? reader.len : reader.pos + length, message = new $root.SysMsg.GroupMemberIncrease();
while (reader.pos < end) {
let tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
message.groupCode = reader.uint32();
break;
}
case 3: {
message.memberUid = reader.string();
break;
}
case 4: {
message.type = reader.uint32();
break;
}
case 5: {
message.adminUid = reader.string();
break;
}
default:
reader.skipType(tag & 7);
break;
}
}
return message;
};
/**
* Decodes a GroupMemberIncrease message from the specified reader or buffer, length delimited.
* @function decodeDelimited
* @memberof SysMsg.GroupMemberIncrease
* @static
* @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
* @returns {SysMsg.GroupMemberIncrease} GroupMemberIncrease
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
GroupMemberIncrease.decodeDelimited = function decodeDelimited(reader) {
if (!(reader instanceof $Reader))
reader = new $Reader(reader);
return this.decode(reader, reader.uint32());
};
/**
* Gets the default type url for GroupMemberIncrease
* @function getTypeUrl
* @memberof SysMsg.GroupMemberIncrease
* @static
* @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns {string} The default type url
*/
GroupMemberIncrease.getTypeUrl = function getTypeUrl(typeUrlPrefix) {
if (typeUrlPrefix === undefined) {
typeUrlPrefix = "type.googleapis.com";
}
return typeUrlPrefix + "/SysMsg.GroupMemberIncrease";
};
return GroupMemberIncrease;
})();
return SysMsg;
})();

View File

@@ -0,0 +1,10 @@
syntax = "proto3";
package SysMsg;
// GroupChange?
message GroupMemberIncrease {
uint32 groupCode = 1;
string memberUid = 3;
uint32 type = 4; // 130:主动 131:被邀请
string adminUid = 5;
}

View File

@@ -8,23 +8,22 @@ message SystemMessage {
}
message SystemMessageHeader {
uint32 peerNumber = 1;
string peerString = 2;
uint32 peerUin = 1;
//string peerUid = 2;
uint32 uin = 5;
optional string uid = 6;
}
message SystemMessageMsgSpec {
uint32 msgType = 1;
uint32 subType = 2;
uint32 subSubType = 3;
optional uint32 subType = 2;
optional uint32 subSubType = 3;
uint32 msgSeq = 5;
uint32 time = 6;
//uint64 msgId = 12;
uint32 other = 13;
optional uint32 other = 13;
}
message SystemMessageBodyWrapper {
bytes body = 2;
// Find the first [08], or ignore the first 7 bytes?
}
}

View File

@@ -125,4 +125,6 @@ export interface NodeIKernelGroupService {
removeGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
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 './NodeIKernelTipOffService'
export * from './NodeIKernelRobotService'
export * from './NodeIKernelNodeMiscService'

View File

@@ -294,6 +294,9 @@ export interface GrayTipElement {
jsonGrayTipElement?: {
busiId: string
jsonStr: string
xmlToJsonParam?: {
templParam: Map<string, string>
}
}
}

View File

@@ -30,12 +30,12 @@ export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
const { count, reverseOrder } = payload
const peer = { chatType: ChatType.Group, peerUid: payload.group_id.toString() }
let msgList: RawMessage[]
if (!payload.message_seq || payload.message_seq === '0') {
if (!payload.message_seq || +payload.message_seq === 0) {
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
} else {
const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId
if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`)
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count, true)).msgList
}
if (!msgList?.length) throw new Error('未找到消息')
if (reverseOrder) msgList.reverse()

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

@@ -2,25 +2,27 @@ import { BaseAction, Schema } from '../BaseAction'
import { OB11GroupMember } from '../../types'
import { OB11Entities } from '../../entities'
import { ActionName } from '../types'
import { calcQQLevel } from '@/common/utils/misc'
import { calcQQLevel, parseBool } from '@/common/utils/misc'
interface Payload {
group_id: number | string
user_id: number | string
no_cache: boolean
}
class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required()
user_id: Schema.union([Number, String]).required(),
no_cache: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(false)
})
protected async _handle(payload: Payload) {
const groupCode = payload.group_id.toString()
const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!uid) throw new Error('无法获取用户信息')
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uid)
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uid, payload.no_cache)
if (member) {
const ret = OB11Entities.groupMember(groupCode, member)
const date = Math.round(Date.now() / 1000)

View File

@@ -17,7 +17,10 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
protected async _handle(payload: Payload) {
const groupCode = payload.group_id.toString()
let groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
if (groupMembers.size === 0) {
for (let i = 0; i < 5; i++) {
if (groupMembers.size > 0) {
break
}
await this.ctx.sleep(100)
groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
}

View File

@@ -73,6 +73,9 @@ import { GetGroupFileUrl } from './go-cqhttp/GetGroupFileUrl'
import { GetGroupNotice } from './go-cqhttp/GetGroupNotice'
import { GetRobotUinRange } from './llonebot/GetRobotUinRange'
import { DeleteFriend } from './go-cqhttp/DeleteFriend'
import { OCRImage } from './go-cqhttp/OCRImage'
import { GroupPoke } from './llonebot/GroupPoke'
import { FriendPoke } from './llonebot/FriendPoke'
export function initActionMap(adapter: Adapter) {
const actionHandlers = [
@@ -91,6 +94,8 @@ export function initActionMap(adapter: Adapter) {
new FetchCustomFace(adapter),
new SetMsgEmojiLike(adapter),
new GetRobotUinRange(adapter),
new GroupPoke(adapter),
new FriendPoke(adapter),
// onebot11
new SendLike(adapter),
new GetMsg(adapter),
@@ -151,6 +156,7 @@ export function initActionMap(adapter: Adapter) {
new GetGroupFileUrl(adapter),
new GetGroupNotice(adapter),
new DeleteFriend(adapter),
new OCRImage(adapter),
]
const actionMap = new Map<string, BaseAction<any, unknown>>()
for (const action of actionHandlers) {

View File

@@ -0,0 +1,25 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils/misc'
interface Payload {
user_id: number | string
}
export class FriendPoke extends BaseAction<Payload, null> {
actionName = ActionName.FriendPoke
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
if (!this.ctx.app.native.checkPlatform()) {
throw new Error('当前系统平台或架构不支持')
}
if (!this.ctx.app.native.checkVersion()) {
throw new Error(`当前 QQ 版本 ${getBuildVersion()} 不支持,可尝试其他版本 27333—27597`)
}
await this.ctx.app.native.sendFriendPoke(+payload.user_id)
return null
}
}

View File

@@ -0,0 +1,27 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils/misc'
interface Payload {
group_id: number | string
user_id: number | string
}
export class GroupPoke extends BaseAction<Payload, null> {
actionName = ActionName.GroupPoke
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
if (!this.ctx.app.native.checkPlatform()) {
throw new Error('当前系统平台或架构不支持')
}
if (!this.ctx.app.native.checkVersion()) {
throw new Error(`当前 QQ 版本 ${getBuildVersion()} 不支持,可尝试其他版本 27333—27597`)
}
await this.ctx.app.native.sendGroupPoke(+payload.group_id, +payload.user_id)
return null
}
}

View File

@@ -27,6 +27,8 @@ export enum ActionName {
SendForwardMsg = 'send_forward_msg',
SetMsgEmojiLike = 'set_msg_emoji_like',
GetRobotUinRange = 'get_robot_uin_range',
GroupPoke = 'group_poke',
FriendPoke = 'friend_poke',
// onebot 11
SendLike = 'send_like',
GetLoginInfo = 'get_login_info',
@@ -86,4 +88,5 @@ export enum ActionName {
GoCQHTTP_GetGroupFileUrl = 'get_group_file_url',
GoCQHTTP_GetGroupNotice = '_get_group_notice',
GoCQHTTP_DeleteFriend = 'delete_friend',
GoCQHTTP_OCRImage = 'ocr_image',
}

View File

@@ -23,6 +23,7 @@ import { llonebotError } from '../common/globalVars'
import { OB11GroupAdminNoticeEvent } from './event/notice/OB11GroupAdminNoticeEvent'
import { OB11ProfileLikeEvent } from './event/notice/OB11ProfileLikeEvent'
import { SysMsg } from '@/ntqqapi/proto/compiled'
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
declare module 'cordis' {
interface Context {
@@ -33,12 +34,13 @@ declare module 'cordis' {
class OneBot11Adapter extends Service {
static inject = [
'ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi',
'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi', 'store'
'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi',
'store', 'app'
]
private ob11WebSocket: OB11WebSocket
private ob11WebSocketReverseManager: OB11WebSocketReverseManager
private ob11Http: OB11Http
private ob11HttpPost: OB11HttpPost
private ob11WebSocket
private ob11WebSocketReverseManager
private ob11Http
private ob11HttpPost
constructor(public ctx: Context, public config: OneBot11Adapter.Config) {
super(ctx, 'onebot', true)
@@ -173,26 +175,23 @@ class OneBot11Adapter extends Service {
return
}
const isSelfMsg = msg.user_id.toString() === selfInfo.uin
if (isSelfMsg && !this.config.reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
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 => {
if (groupEvent) {
this.dispatch(groupEvent)
}
})
}).catch(e => this.ctx.logger.error('handling incoming group events', e))
OB11Entities.privateEvent(this.ctx, message).then(privateEvent => {
if (privateEvent) {
this.dispatch(privateEvent)
}
})
}).catch(e => this.ctx.logger.error('handling incoming buddy events', e))
}
private handleRecallMsg(message: RawMessage) {
@@ -310,7 +309,6 @@ class OneBot11Adapter extends Service {
heartInterval: config.heartInterval,
token: config.token,
debug: config.debug,
reportSelfMessage: config.reportSelfMessage,
msgCacheExpire: config.msgCacheExpire,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url,
@@ -341,6 +339,9 @@ class OneBot11Adapter extends Service {
this.handleRecallMsg(input)
})
this.ctx.on('nt/message-sent', input => {
if (!this.config.reportSelfMessage) {
return
}
this.handleMsg(input)
})
this.ctx.on('nt/group-notify', input => {
@@ -349,7 +350,7 @@ class OneBot11Adapter extends Service {
this.ctx.on('nt/friend-request', input => {
this.handleFriendRequest(input)
})
this.ctx.on('nt/system-message-created', input => {
this.ctx.on('nt/system-message-created', async input => {
const sysMsg = SysMsg.SystemMessage.decode(input)
const { msgType, subType, subSubType } = sysMsg.msgSpec[0] ?? {}
if (msgType === 528 && subType === 39 && subSubType === 39) {
@@ -358,8 +359,16 @@ class OneBot11Adapter extends Service {
const detail = tip.content?.msg?.detail
if (!detail) return
const [times] = detail.txt?.match(/\d+/) ?? ['0']
const profileLikeEvent = new OB11ProfileLikeEvent(detail.uin!, detail.nickname!, +times)
this.dispatch(profileLikeEvent)
const event = new OB11ProfileLikeEvent(detail.uin!, detail.nickname!, +times)
this.dispatch(event)
} else if (msgType === 33) {
const tip = SysMsg.GroupMemberIncrease.decode(sysMsg.bodyWrapper!.body!)
if (tip.type !== 130) return
this.ctx.logger.info('群成员增加', tip)
const memberUin = await this.ctx.ntUserApi.getUinByUid(tip.memberUid)
const operatorUin = await this.ctx.ntUserApi.getUinByUid(tip.adminUid)
const event = new OB11GroupIncreaseEvent(tip.groupCode, +memberUin, +operatorUin)
this.dispatch(event)
}
})
}
@@ -370,7 +379,6 @@ namespace OneBot11Adapter {
heartInterval: number
token: string
debug: boolean
reportSelfMessage: boolean
musicSignUrl?: string
enableLocalFile2Url: boolean
ffmpeg?: string

View File

@@ -134,11 +134,13 @@ namespace OB11Http {
class OB11HttpPost {
private disposeInterval?: () => void
private activated = false
constructor(protected ctx: Context, public config: OB11HttpPost.Config) {
}
public start() {
this.activated = true
if (this.config.enableHttpHeart && !this.disposeInterval) {
this.disposeInterval = this.ctx.setInterval(() => {
// ws的心跳是ws自己维护的
@@ -148,10 +150,14 @@ class OB11HttpPost {
}
public stop() {
this.activated = false
this.disposeInterval?.()
}
public async emitEvent(event: OB11BaseEvent | OB11Message) {
if (!this.activated) {
return
}
const msgStr = JSON.stringify(event)
const headers: Dict = {
'Content-Type': 'application/json',

View File

@@ -79,11 +79,14 @@ export namespace OB11Entities {
if (msg.chatType === ChatType.Group) {
resMsg.sub_type = 'normal'
resMsg.group_id = parseInt(msg.peerUin)
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUid)
if (member) {
resMsg.sender.role = groupMemberRole(member.role)
resMsg.sender.nickname = member.nick
resMsg.sender.title = member.memberSpecialTitle ?? ''
// 284840486: 合并转发内部
if (msg.peerUin !== '284840486') {
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUid)
if (member) {
resMsg.sender.role = groupMemberRole(member.role)
resMsg.sender.nickname = member.nick
resMsg.sender.title = member.memberSpecialTitle ?? ''
}
}
}
else if (msg.chatType === ChatType.C2C) {
@@ -420,11 +423,11 @@ export namespace OB11Entities {
const groupElement = grayTipElement?.groupElement
if (groupElement) {
if (groupElement.type === TipGroupElementType.MemberIncrease) {
ctx.logger.info('收到群成员增加消息', groupElement)
/*ctx.logger.info('收到群成员增加消息', groupElement)
const { memberUid, adminUid } = groupElement
const memberUin = await ctx.ntUserApi.getUinByUid(memberUid)
const operatorUin = adminUid ? await ctx.ntUserApi.getUinByUid(adminUid) : memberUin
return new OB11GroupIncreaseEvent(+msg.peerUid, +memberUin, +operatorUin)
return new OB11GroupIncreaseEvent(+msg.peerUid, +memberUin, +operatorUin)*/
}
else if (groupElement.type === TipGroupElementType.Ban) {
ctx.logger.info('收到群成员禁言提示', groupElement)
@@ -548,14 +551,23 @@ export namespace OB11Entities {
else if (grayTipElement.subElementType == GrayTipElementSubType.JSON) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement!.jsonStr)
if (grayTipElement.jsonGrayTipElement?.busiId === '1061') {
const param = grayTipElement.jsonGrayTipElement.xmlToJsonParam
if (param) {
return new OB11GroupPokeEvent(
Number(msg.peerUid),
Number(param.templParam.get('uin_str1')),
Number(param.templParam.get('uin_str2')),
json.items
)
}
const pokedetail: Dict[] = json.items
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(
parseInt(msg.peerUid),
parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0),
parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid) ?? 0),
Number(msg.peerUid),
Number(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0),
Number(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid) ?? 0),
pokedetail
)
}
@@ -665,11 +677,13 @@ export namespace OB11Entities {
}
export function groupMember(group_id: string, member: GroupMember): OB11GroupMember {
const titleExpireTime = +member.specialTitleExpireTime
const int32Max = Math.pow(2, 31) - 1
return {
group_id: parseInt(group_id),
user_id: parseInt(member.uin),
nickname: member.nick,
card: member.cardName,
card: member.cardName || member.nick,
sex: OB11UserSex.Unknown,
age: 0,
area: '',
@@ -677,7 +691,7 @@ export namespace OB11Entities {
qq_level: 0,
join_time: member.joinTime,
last_sent_time: member.lastSpeakTime,
title_expire_time: +member.specialTitleExpireTime,
title_expire_time: titleExpireTime > int32Max ? 0 : titleExpireTime,
unfriendly: false,
card_changeable: true,
is_robot: member.isRobot,

View File

@@ -66,15 +66,10 @@ export async function createSendElements(
}
}
else if (peer.chatType === ChatType.Group) {
const uid = await ctx.ntUserApi.getUidByUin(atQQ) ?? ''
const uid = await ctx.ntUserApi.getUidByUin(atQQ, peer.peerUid) ?? ''
let display = ''
if (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))
}

View File

@@ -7,11 +7,8 @@ import {
CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG,
CHANNEL_UPDATE,
CHANNEL_SET_CONFIG_CONFIRMED,
} from './common/channels'
const { contextBridge } = require('electron')
const { ipcRenderer } = require('electron')
import { contextBridge, ipcRenderer } from 'electron'
const llonebot = {
log: (data: unknown) => {
@@ -24,8 +21,7 @@ const llonebot = {
return ipcRenderer.invoke(CHANNEL_UPDATE)
},
setConfig: async (ask: boolean, config: Config) => {
const isSuccess = await ipcRenderer.invoke(CHANNEL_SET_CONFIG, ask, config)
if (isSuccess) ipcRenderer.send(CHANNEL_SET_CONFIG_CONFIRMED, config)
return ipcRenderer.invoke(CHANNEL_SET_CONFIG, ask, config)
},
getConfig: async (): Promise<Config> => {
return ipcRenderer.invoke(CHANNEL_GET_CONFIG)

View File

@@ -25,9 +25,6 @@ async function onSettingWindowCreated(view: Element) {
} else {
Object.assign(config, { [key]: value })
}
if (!['heartInterval', 'token', 'ffmpeg'].includes(key)) {
window.llonebot.setConfig(false, config)
}
}
}
@@ -171,13 +168,14 @@ async function onSettingWindowCreated(view: Element) {
SettingItem(
'上报 Bot 自身发送的消息',
'上报 event 为 message_sent',
SettingSwitch('reportSelfMessage', config.reportSelfMessage),
SettingSwitch('ob11.reportSelfMessage', config.ob11.reportSelfMessage),
),
SettingItem(
'使用 Base64 编码获取文件',
'调用 /get_image、/get_record、/get_file 时,没有 url 时添加 Base64 字段',
SettingSwitch('enableLocalFile2Url', config.enableLocalFile2Url),
),
SettingItem('', null, SettingButton('保存', 'config-ob11-save-2', 'primary')),
]),
SettingList([
SettingItem(
@@ -217,6 +215,7 @@ async function onSettingWindowCreated(view: Element) {
'单位为秒,可用于获取撤回的消息',
`<div class="q-input"><input class="q-input__inner" data-config-key="msgCacheExpire" type="number" min="1" value="${config.msgCacheExpire}" placeholder="${config.msgCacheExpire}" /></div>`,
),
SettingItem('', null, SettingButton('保存', 'config-ob11-save-3', 'primary')),
]),
SettingList([
SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')),
@@ -395,6 +394,22 @@ async function onSettingWindowCreated(view: Element) {
alert('保存成功')
})
doc.querySelector('#config-ob11-save-2')?.addEventListener('click', () => {
config.ob11 = ob11Config
window.llonebot.setConfig(false, config)
showError().then()
alert('保存成功')
})
doc.querySelector('#config-ob11-save-3')?.addEventListener('click', () => {
config.ob11 = ob11Config
window.llonebot.setConfig(false, config)
showError().then()
alert('保存成功')
})
doc.body.childNodes.forEach((node) => {
view.appendChild(node)
})

View File

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

View File

@@ -99,6 +99,15 @@ export class SatoriServer {
}
public async stop() {
if (this.wsClients.length > 0) {
for (const socket of this.wsClients) {
try {
if (socket.readyState === WebSocket.OPEN) {
socket.close(1000)
}
} catch { }
}
}
if (this.wsServer) {
const close = promisify(this.wsServer.close)
await close.call(this.wsServer)

View File

@@ -129,6 +129,11 @@ async function decodeElement(ctx: Context, data: NT.RawMessage, quoted = false)
type: String(faceType),
platform: 'llonebot'
}))
} else if (v.arkElement) {
// llonebot:ark
buffer.push(h('llonebot:ark', {
data: v.arkElement.bytesData
}))
}
}
return buffer

View File

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

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ESNext",
"target": "ES2023",
"module": "CommonJS",
"outDir": "./dist",
"strict": true,
@@ -20,7 +20,10 @@
"./src/ntqqapi/*"
]
},
"noEmit": true
"noEmit": true,
"types": [
"electron-vite/node"
]
},
"include": [
"src",