Compare commits

...

36 Commits

Author SHA1 Message Date
linyuchen
2a1aa8c649 feat: image subType 2024-07-13 14:26:23 +08:00
linyuchen
1633734e08 Merge branch 'dev' 2024-07-13 14:09:45 +08:00
linyuchen
dff92e6f27 chore: version 3.27.0
feat: support poke
feat: LLOneBot global switch
2024-07-13 14:09:03 +08:00
linyuchen
dba5e30d5d doc: plugin description 2024-07-10 13:48:05 +08:00
linyuchen
2d04ab2e72 fix: crychic crash 2024-07-10 13:47:44 +08:00
linyuchen
1a015ac8d3 Merge pull request #262 from LLOneBot/dev
get_record 支持 out_format 进行转码,和其他小修复
2024-06-21 17:39:53 +08:00
linyuchen
6390620ddd chore: version 3.26.7 2024-06-21 17:33:48 +08:00
linyuchen
0d19005dc3 refactor: remove duplicate import 2024-06-21 17:28:17 +08:00
linyuchen
c6479dd2c4 Merge remote-tracking branch 'origin/dev' into dev 2024-06-21 16:21:15 +08:00
linyuchen
8871331b7c 🐛 fix: ws echo #261 2024-06-21 16:20:59 +08:00
linyuchen
e01148b86a 🐛 fix: ws echo 2024-06-21 16:20:26 +08:00
linyuchen
2f87e3818e Merge pull request #260 from idranme/main
perf: audio
2024-06-21 10:36:29 +08:00
linyuchen
2c8a594c38 Merge branch 'dev' into main 2024-06-21 10:36:14 +08:00
idranme
1508dab7fe perf: audio 2024-06-18 19:15:56 +00:00
linyuchen
958b21e47e fix: wait get_file download complete 2024-06-17 17:41:23 +08:00
linyuchen
781c3311ae fix: get_file cache not found 2024-06-17 16:20:37 +08:00
linyuchen
52850d172e feat: decode silk 2024-06-17 16:05:38 +08:00
linyuchen
52a065542e chore: v3.26.6 2024-06-10 14:38:20 +08:00
linyuchen
fd10469685 feat: video url 2024-06-10 14:35:00 +08:00
linyuchen
a2ee75b113 refactor: sent msg status waiter 2024-06-09 15:27:33 +08:00
linyuchen
0f7f243b98 Merge pull request #250 from Bluefissure/reverse-ws-ua
feat: add ua to reverse websocket headers
2024-06-06 17:35:21 +08:00
Bluefissure
97d7996a50 fix: add version to ua 2024-06-06 08:53:37 +00:00
Bluefissure
b658d164f9 feat: add ua to reverse websocket headers 2024-06-06 08:48:18 +00:00
linyuchen
f150ae478b chore: v3.26.5 2024-06-01 20:19:05 +08:00
linyuchen
d1f68553f1 fix: 加载卡顿,群成员名片变动 2024-06-01 20:18:38 +08:00
linyuchen
f47f0800de Merge remote-tracking branch 'origin/main' 2024-05-29 16:56:08 +08:00
linyuchen
b7ddefc950 fix: QZone cookies 2024-05-29 16:38:22 +08:00
linyuchen
25b3325a44 fix: comment 2024-05-29 16:28:46 +08:00
linyuchen
c281b87bab merge main 2024-05-29 16:27:06 +08:00
linyuchen
c0946ddda2 chore: version 3.26.4 2024-05-29 16:26:04 +08:00
linyuchen
1128cf679c refactor: send file timeout 2024-05-29 16:25:42 +08:00
linyuchen
ff65a42350 Merge pull request #242 from LLOneBot/dev
feat: support qzone cookies
2024-05-29 16:24:32 +08:00
手瓜一十雪
c459587dcd refactor: get cookies 2024-05-29 12:03:35 +08:00
手瓜一十雪
6f8ea9677f feat: support qzone cookies 2024-05-28 17:14:24 +08:00
手瓜一十雪
38197527fa Merge branch 'main' into dev 2024-05-28 17:11:13 +08:00
手瓜一十雪
21b2bd2c8e feat: cookies 2024-05-28 17:11:07 +08:00
27 changed files with 475 additions and 303 deletions

View File

@@ -1,10 +1,10 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot v3.26.3", "name": "LLOneBot v3.27.0",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新", "description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发",
"version": "3.26.3", "version": "3.27.0",
"icon": "./icon.jpg", "icon": "./icon.jpg",
"authors": [ "authors": [
{ {

View File

@@ -23,7 +23,7 @@
"file-type": "^19.0.0", "file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"level": "^8.0.1", "level": "^8.0.1",
"silk-wasm": "^3.3.4", "silk-wasm": "^3.6.0",
"utf-8-validate": "^6.0.3", "utf-8-validate": "^6.0.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"ws": "^8.16.0" "ws": "^8.16.0"

View File

@@ -43,6 +43,7 @@ export class ConfigUtil {
enableQOAutoQuote: false enableQOAutoQuote: false
} }
let defaultConfig: Config = { let defaultConfig: Config = {
enableLLOB: true,
ob11: ob11Default, ob11: ob11Default,
heartInterval: 60000, heartInterval: 60000,
token: '', token: '',

View File

@@ -17,6 +17,7 @@ export interface CheckVersion {
version: string version: string
} }
export interface Config { export interface Config {
enableLLOB: boolean
ob11: OB11Config ob11: OB11Config
token?: string token?: string
heartInterval?: number // ms heartInterval?: number // ms

View File

@@ -1,9 +1,9 @@
import fs from 'fs' import fs from 'fs'
import { encode, getDuration, getWavFileInfo, isWav } from 'silk-wasm'
import fsPromise from 'fs/promises' import fsPromise from 'fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm'
import { log } from './log' import { log } from './log'
import path from 'node:path' import path from 'node:path'
import { DATA_DIR, TEMP_DIR } from './index' import { TEMP_DIR } from './index'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { getConfigUtil } from '../config' import { getConfigUtil } from '../config'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
@@ -60,10 +60,11 @@ export async function encodeSilk(filePath: string) {
// } // }
try { try {
const file = await fsPromise.readFile(filePath)
const pttPath = path.join(TEMP_DIR, uuidv4()) const pttPath = path.join(TEMP_DIR, uuidv4())
if (getFileHeader(filePath) !== '02232153494c4b') { if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`) log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath) const _isWav = isWav(file)
const pcmPath = pttPath + '.pcm' const pcmPath = pttPath + '.pcm'
let sampleRate = 0 let sampleRate = 0
const convert = () => { const convert = () => {
@@ -79,7 +80,8 @@ export async function encodeSilk(filePath: string) {
if (code == null || EXIT_CODES.includes(code)) { if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000 sampleRate = 24000
const data = fs.readFileSync(pcmPath) const data = fs.readFileSync(pcmPath)
fs.unlink(pcmPath, (err) => {}) fs.unlink(pcmPath, (err) => {
})
return resolve(data) return resolve(data)
} }
log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`) log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`)
@@ -91,7 +93,7 @@ export async function encodeSilk(filePath: string) {
if (!_isWav) { if (!_isWav) {
input = await convert() input = await convert()
} else { } else {
input = fs.readFileSync(filePath) input = file
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000] const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const { fmt } = getWavFileInfo(input) const { fmt } = getWavFileInfo(input)
// log(`wav文件信息`, fmt) // log(`wav文件信息`, fmt)
@@ -108,7 +110,7 @@ export async function encodeSilk(filePath: string) {
duration: silk.duration / 1000, duration: silk.duration / 1000,
} }
} else { } else {
const silk = fs.readFileSync(filePath) const silk = file
let duration = 0 let duration = 0
try { try {
duration = getDuration(silk) / 1000 duration = getDuration(silk) / 1000
@@ -128,3 +130,41 @@ export async function encodeSilk(filePath: string) {
return {} return {}
} }
} }
export async function decodeSilk(inputFilePath: string, outFormat: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' = 'mp3') {
const silkArrayBuffer = await fsPromise.readFile(inputFilePath)
const data = (await decode(silkArrayBuffer, 24000)).data
const fileName = path.join(TEMP_DIR, path.basename(inputFilePath))
const outPCMPath = fileName + '.pcm'
const outFilePath = fileName + '.' + outFormat
await fsPromise.writeFile(outPCMPath, data)
const convert = () => {
return new Promise<string>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, [
'-y',
'-f', 's16le', // PCM format
'-ar', '24000', // Sample rate
'-ac', '1', // Number of audio channels
'-i', outPCMPath,
outFilePath,
])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
fs.unlink(outPCMPath, (err) => {
})
return resolve(outFilePath)
}
const exitErr = `FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`
log(exitErr)
reject(Error(`FFmpeg处理转换失败,${exitErr}`))
})
})
}
return convert()
}

View File

@@ -1,30 +1,50 @@
import https from 'node:https'; import https from 'node:https';
import http from 'node:http'; import http from 'node:http';
import { log } from '@/common/utils/log'
export class RequestUtil { export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET // 适用于获取服务器下发cookies时获取仅GET
static async HttpsGetCookies(url: string): Promise<Map<string, string>> { static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> {
return new Promise<Map<string, string>>((resolve, reject) => { const client = url.startsWith('https') ? https : http;
const protocol = url.startsWith('https://') ? https : http; return new Promise((resolve, reject) => {
protocol.get(url, (res) => { client.get(url, (res) => {
const cookiesHeader = res.headers['set-cookie']; let cookies: { [key: string]: string } = {};
if (!cookiesHeader) { const handleRedirect = (res: http.IncomingMessage) => {
resolve(new Map<string, string>()); //console.log(res.headers.location);
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location) {
const redirectUrl = new URL(res.headers.location, url);
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
// 合并重定向过程中的cookies
log('redirectCookies', redirectCookies)
cookies = { ...cookies, ...redirectCookies };
resolve(cookies);
});
} else { } else {
const cookiesMap = new Map<string, string>(); resolve(cookies);
cookiesHeader.forEach((cookieStr) => { }
cookieStr.split(';').forEach((cookiePart) => { } else {
const trimmedPart = cookiePart.trim(); resolve(cookies);
if (trimmedPart.includes('=')) { }
const [key, value] = trimmedPart.split('=').map(part => part.trim()); };
cookiesMap.set(key, decodeURIComponent(value)); // 解码cookie值 res.on('data', () => { }); // Necessary to consume the stream
res.on('end', () => {
handleRedirect(res);
});
if (res.headers['set-cookie']) {
// console.log(res.headers['set-cookie']);
log('set-cookie', url, res.headers['set-cookie']);
res.headers['set-cookie'].forEach((cookie) => {
const parts = cookie.split(';')[0].split('=');
const key = parts[0];
const value = parts[1];
if (key && value && key.length > 0 && value.length > 0) {
cookies[key] = value;
} }
}); });
});
resolve(cookiesMap);
} }
}).on('error', (error) => { }).on('error', (err) => {
reject(error); reject(err);
}); });
}); });
} }

View File

@@ -13,7 +13,7 @@ import {
CHANNEL_UPDATE, CHANNEL_UPDATE,
} from '../common/channels' } from '../common/channels'
import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer' import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import { DATA_DIR } from '../common/utils' import { DATA_DIR, qqPkgInfo } from '../common/utils'
import { import {
friendRequests, friendRequests,
getFriend, getFriend,
@@ -25,7 +25,7 @@ import {
selfInfo, selfInfo,
uidMaps, uidMaps,
} from '../common/data' } from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook } from '../ntqqapi/hook' import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { import {
ChatType, ChatType,
@@ -56,6 +56,7 @@ import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video' import { checkFfmpeg } from '../common/utils/video'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import '../ntqqapi/native/wrapper' import '../ntqqapi/native/wrapper'
import { sentMessages } from '@/ntqqapi/api'
let running = false let running = false
@@ -200,7 +201,12 @@ function onLoad() {
} }
async function startReceiveHook() { async function startReceiveHook() {
startHook().then()
if (getConfigUtil().getConfig().enablePoke) { if (getConfigUtil().getConfig().enablePoke) {
if ( qqPkgInfo.buildVersion > '23873'){
log(`当前版本${qqPkgInfo.buildVersion}不支持发送戳一戳模块`)
return
}
crychic.loadNode() crychic.loadNode()
crychic.registerPokeHandler((id, isGroup) => { crychic.registerPokeHandler((id, isGroup) => {
log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`) log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`)
@@ -226,6 +232,10 @@ function onLoad() {
const recallMsgIds: string[] = [] // 避免重复上报 const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
const sentMessage = sentMessages[message.msgId]
if (sentMessage){
Object.assign(sentMessage, message)
}
log('message update', message.msgId, message) log('message update', message.msgId, message)
if (message.recallTime != '0') { if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) { if (recallMsgIds.includes(message.msgId)) {
@@ -429,6 +439,11 @@ function onLoad() {
async function start() { async function start() {
log('llonebot pid', process.pid) log('llonebot pid', process.pid)
const config = getConfigUtil().getConfig()
if (!config.enableLLOB){
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
llonebotError.otherError = '' llonebotError.otherError = ''
startTime = Date.now() startTime = Date.now()
dbUtil.getReceivedTempUinMap().then((m) => { dbUtil.getReceivedTempUinMap().then((m) => {
@@ -436,15 +451,33 @@ function onLoad() {
uidMaps[value] = key uidMaps[value] = key
} }
}) })
startReceiveHook().then() try{
NTQQGroupApi.getGroups(true).then(groups=> { log('start get groups')
for (let group of groups) { const _groups = await NTQQGroupApi.getGroups()
log('_groups', _groups)
await Promise.all(
_groups.map(async (group) => {
try {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = members
groups.push(group)
} catch (e) {
log('获取群成员失败', e)
} }
})
)
} }
).catch(log) catch (e) {
log('获取群列表失败', e)
}
finally {
log('start activate group member info')
NTQQGroupApi.activateMemberInfoChange().then().catch(log) NTQQGroupApi.activateMemberInfoChange().then().catch(log)
NTQQGroupApi.activateMemberListChange().then().catch(log) NTQQGroupApi.activateMemberListChange().then().catch(log)
const config = getConfigUtil().getConfig() startReceiveHook().then()
}
if (config.ob11.enableHttp) { if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort) ob11HTTPServer.start(config.ob11.httpPort)
} }

View File

@@ -10,15 +10,23 @@ import {
ElementType, ElementType,
IMAGE_HTTP_HOST, IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement, IMAGE_HTTP_HOST_NT, PicElement,
RawMessage,
} from '../types' } from '../types'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey' import { rkeyManager } from '@/ntqqapi/api/rkey'
import { wrapperApi } from '@/ntqqapi/native/wrapper'
import { Peer } from '@/ntqqapi/api/msg'
export class NTQQFileApi { export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
return (await wrapperApi.NodeIQQNTWrapperSession.getRichMediaService().getVideoPlayUrlV2(peer,
msgId,
elementId,
0,
{ downSourceType: 1, triggerType: 1 })).urlResult?.domainUrl[0]?.url;
}
static async getFileType(filePath: string) { static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({ return await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API, className: NTQQApiClass.FS_API,

View File

@@ -35,14 +35,20 @@ export class NTQQGroupApi {
}) })
} }
static async getGroups(forced = false) { static async getGroups(forced = false) {
let cbCmd = ReceiveCmdS.GROUPS // let cbCmd = ReceiveCmdS.GROUPS
if (process.platform != 'win32') { // if (process.platform != 'win32') {
cbCmd = ReceiveCmdS.GROUPS_STORE // cbCmd = ReceiveCmdS.GROUPS_STORE
} // }
const result = await callNTQQApi<{ const result = await callNTQQApi<{
updateType: number updateType: number
groupList: Group[] groupList: Group[]
}>({ methodName: NTQQApiMethod.GROUPS, args: [{ force_update: forced }, undefined], cbCmd }) }>({
methodName: NTQQApiMethod.GROUPS,
args: [{ force_update: forced }, undefined],
cbCmd: [ReceiveCmdS.GROUPS, ReceiveCmdS.GROUPS_STORE],
afterFirstCmd: false,
})
log('get groups result', result)
return result.groupList return result.groupList
} }
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> { static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> {

View File

@@ -7,7 +7,9 @@ import { log } from '../../common/utils/log'
import { sleep } from '../../common/utils/helper' import { sleep } from '../../common/utils/helper'
import { isQQ998 } from '../../common/utils' import { isQQ998 } from '../../common/utils'
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunnc export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunc
export let sentMessages: Record<string, RawMessage> = {} // msgId: RawMessage
export interface Peer { export interface Peer {
chatType: ChatType chatType: ChatType
@@ -40,17 +42,20 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid] delete sendMessagePool[peerUid]
sentMessage = rawMessage sentMessage = rawMessage
sentMessages[rawMessage.msgId] = rawMessage
} }
let checkSendCompleteUsingTime = 0 let checkSendCompleteUsingTime = 0
const checkSendComplete = async (): Promise<RawMessage> => { const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) { if (sentMessage) {
if (waitComplete) { if (waitComplete) {
if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) { if (sentMessage.sendStatus == 2) {
delete sentMessages[sentMessage.msgId]
return sentMessage return sentMessage
} }
} }
else { else {
delete sentMessages[sentMessage.msgId]
return sentMessage return sentMessage
} }
// log(`给${peerUid}发送消息成功`) // log(`给${peerUid}发送消息成功`)

View File

@@ -6,6 +6,7 @@ import { NTQQWindowApi, NTQQWindows } from './window'
import { cacheFunc, isQQ998, log, sleep } from '../../common/utils' import { cacheFunc, isQQ998, log, sleep } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/native/wrapper' import { wrapperApi } from '@/ntqqapi/native/wrapper'
import * as https from 'https' import * as https from 'https'
import { RequestUtil } from '@/common/utils/request'
let userInfoCache: Record<string, User> = {} // uid: User let userInfoCache: Record<string, User> = {} // uid: User
@@ -98,7 +99,17 @@ export class NTQQUserApi {
], ],
}) })
} }
static async getQzoneCookies() {
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27'
let cookies: { [key: string]: string; } = {};
try {
cookies = await RequestUtil.HttpsGetCookies(requestUrl);
} catch (e: any) {
log('获取QZone Cookies失败', e)
cookies = {}
}
return cookies;
}
static async getSkey(): Promise<string> { static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey() const clientKeyData = await this.getClientKey()
if (clientKeyData.result !== 0) { if (clientKeyData.result !== 0) {
@@ -106,31 +117,17 @@ export class NTQQUserApi {
} }
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin
+ '&clientkey=' + clientKeyData.clientKey + '&clientkey=' + clientKeyData.clientKey
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex + '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex;
return (await RequestUtil.HttpsGetCookies(url))?.skey;
return new Promise((resolve, reject) => {
const req = https.get(url, (res) => {
const rawCookies = res.headers['set-cookie']
const cookies = {}
rawCookies.forEach(cookie => {
// 使用正则表达式匹配 cookie 名称和值
const regex = /([^=;]+)=([^;]*)/
const match = regex.exec(cookie)
if (match) {
cookies[match[1].trim()] = match[2].trim()
}
})
resolve(cookies['skey'])
})
req.on('error', e => {
reject(e)
});
req.end();
});
} }
@cacheFunc(60 * 30 * 1000) @cacheFunc(60 * 30 * 1000)
static async getCookies(domain: string) { static async getCookies(domain: string) {
if (domain.endsWith("qzone.qq.com")) {
let data = (await NTQQUserApi.getQzoneCookies());
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin;
return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue };
}
const skey = await this.getSkey(); const skey = await this.getSkey();
const pskey = (await this.getPSkey([domain])).get(domain); const pskey = (await this.getPSkey([domain])).get(domain);
if (!pskey || !skey) { if (!pskey || !skey) {

View File

@@ -22,6 +22,7 @@ import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { isNumeric, sleep } from '@/common/utils' import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -324,13 +325,16 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
} }
} }
export async function startHook() {
// 群列表变动 // 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => { registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动 // updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList) // log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) { if (payload.updateType != 2) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then()
} else { }
else {
if (process.platform == 'win32') { if (process.platform == 'win32') {
processGroupEvent(payload).then() processGroupEvent(payload).then()
} }
@@ -341,7 +345,8 @@ registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROU
// log("群列表变动", payload.updateType, payload.groupList) // log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) { if (payload.updateType != 2) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then()
} else { }
else {
if (process.platform != 'win32') { if (process.platform != 'win32') {
processGroupEvent(payload).then() processGroupEvent(payload).then()
} }
@@ -359,6 +364,12 @@ registerReceiveHook<{
for (const member of members) { for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin) const existMember = await getGroupMember(groupCode, member.uin)
if (existMember) { if (existMember) {
if (member.cardName != existMember.cardName) {
log('群成员名片变动', `${groupCode}: ${existMember.uin}`, existMember.cardName, '->', member.cardName)
postOb11Event(
new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName),
)
}
Object.assign(existMember, member) Object.assign(existMember, member)
} }
} }
@@ -391,7 +402,8 @@ registerReceiveHook<{
let existFriend = friends.find((f) => f.uin == friend.uin) let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) { if (!existFriend) {
friends.push(friend) friends.push(friend)
} else { }
else {
Object.assign(existFriend, friend) Object.assign(existFriend, friend)
} }
} }
@@ -503,7 +515,8 @@ registerReceiveHook<{
} }
}) })
}) })
} else { }
else {
NTQQMsgApi.activateChat(peer).then() NTQQMsgApi.activateChat(peer).then()
} }
} }
@@ -516,7 +529,8 @@ registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => {
let chatType = ChatType.friend let chatType = ChatType.friend
if (isNumeric(peerUid)) { if (isNumeric(peerUid)) {
chatType = ChatType.group chatType = ChatType.group
} else { }
else {
// 检查是否好友 // 检查是否好友
if (!(await getFriend(peerUid))) { if (!(await getFriend(peerUid))) {
chatType = ChatType.temp chatType = ChatType.temp
@@ -528,3 +542,5 @@ registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg }) log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
}) })
}) })
}

View File

@@ -107,7 +107,7 @@ interface NTQQApiParams {
channel?: NTQQApiChannel channel?: NTQQApiChannel
classNameIsRegister?: boolean classNameIsRegister?: boolean
args?: unknown[] args?: unknown[]
cbCmd?: ReceiveCmd | null cbCmd?: ReceiveCmd | ReceiveCmd[] | null
cmdCB?: (payload: any) => boolean cmdCB?: (payload: any) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number timeoutSecond?: number
@@ -147,7 +147,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
success = true success = true
resolve(r) resolve(r)
} }
} else { }
else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => { const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
@@ -158,7 +159,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
success = true success = true
resolve(payload) resolve(payload)
} }
} else { }
else {
removeReceiveHook(hookId) removeReceiveHook(hookId)
success = true success = true
resolve(payload) resolve(payload)
@@ -170,7 +172,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
log(`${methodName} callback`, result) log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) { if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} else { }
else {
success = true success = true
reject(`ntqq api call failed, ${result.errMsg}`) reject(`ntqq api call failed, ${result.errMsg}`)
} }
@@ -188,7 +191,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
channel, channel,
{ {
sender: { sender: {
send: (..._args: unknown[]) => {}, send: (..._args: unknown[]) => {
},
}, },
}, },
{ type: 'request', callbackId: uuid, eventName }, { type: 'request', callbackId: uuid, eventName },

View File

@@ -188,6 +188,7 @@ export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn'
export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn' export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn'
export interface PicElement { export interface PicElement {
picSubType: PicSubType
picType: PicType // 有这玩意儿吗 picType: PicType // 有这玩意儿吗
originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
originImageMd5?: string originImageMd5?: string
@@ -226,6 +227,7 @@ export interface GrayTipElement {
content: string content: string
} }
jsonGrayTipElement: { jsonGrayTipElement: {
busiId: number
jsonStr: string jsonStr: string
} }
} }

View File

@@ -2,7 +2,7 @@ import BaseAction from '../BaseAction'
import fs from 'fs/promises' import fs from 'fs/promises'
import { dbUtil } from '@/common/db' import { dbUtil } from '@/common/db'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { log, sleep, uri2local } from '@/common/utils' import { checkFileReceived, log, sleep, uri2local } from '@/common/utils'
import { NTQQFileApi } from '@/ntqqapi/api' import { NTQQFileApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types' import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types'
@@ -38,20 +38,21 @@ export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
log('找到了文件 element', element) log('找到了文件 element', element)
// 构建下载函数 // 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '', true) await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '', true)
await sleep(1000) // 这里延时是为何? // 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId) msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg) log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg, cache.elementId).filePath cache.filePath = this.getElement(msg, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then() dbUtil.addFileCache(file, cache).then()
} }
} }
} }
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const cache = await dbUtil.getFileCache(payload.file) let cache = await dbUtil.getFileCache(payload.file)
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
if (!cache) { if (!cache) {
throw new Error('file not found') throw new Error('file not found')
} }
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
if (cache.downloadFunc) { if (cache.downloadFunc) {
await cache.downloadFunc() await cache.downloadFunc()
} }

View File

@@ -1,5 +1,9 @@
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile' import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import { ActionName } from '../types' import { ActionName } from '../types'
import {decodeSilk} from "@/common/utils/audio";
import { getConfigUtil } from '@/common/config'
import path from 'node:path'
import fs from 'node:fs'
interface Payload extends GetFilePayload { interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
@@ -9,7 +13,13 @@ export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord actionName = ActionName.GetRecord
protected async _handle(payload: Payload): Promise<GetFileResponse> { protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = super._handle(payload) let res = await super._handle(payload)
res.file = await decodeSilk(res.file, payload.out_format)
res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString()
if (getConfigUtil().getConfig().enableLocalFile2Url){
res.base64 = fs.readFileSync(res.file, 'base64')
}
return res return res
} }
} }

View File

@@ -46,7 +46,7 @@ import GetFile from './file/GetFile'
import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg' import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie' import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike' import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardSingleGroupMsg } from './msg/ForwardSingleMsg' import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg'
import { GetGroupEssence } from './group/GetGroupEssence' import { GetGroupEssence } from './group/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo' import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation' import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation'
@@ -91,7 +91,7 @@ export const actionHandlers = [
new GetCookies(), new GetCookies(),
new SetMsgEmojiLike(), new SetMsgEmojiLike(),
new ForwardFriendSingleMsg(), new ForwardFriendSingleMsg(),
new ForwardSingleGroupMsg(), new ForwardGroupSingleMsg(),
//以下为go-cqhttp api //以下为go-cqhttp api
new GetGroupEssence(), new GetGroupEssence(),
new GetGroupHonorInfo(), new GetGroupHonorInfo(),

View File

@@ -43,6 +43,6 @@ export class ForwardFriendSingleMsg extends ForwardSingleMsg {
actionName = ActionName.ForwardFriendSingleMsg actionName = ActionName.ForwardFriendSingleMsg
} }
export class ForwardSingleGroupMsg extends ForwardSingleMsg { export class ForwardGroupSingleMsg extends ForwardSingleMsg {
actionName = ActionName.ForwardGroupSingleMsg actionName = ActionName.ForwardGroupSingleMsg
} }

View File

@@ -327,7 +327,7 @@ export async function sendMsg(
} }
} }
log('发送消息总大小', totalSize, 'bytes') log('发送消息总大小', totalSize, 'bytes')
let timeout = ((totalSize / 1024 / 512) * 1000) + 5000 // 512kb/s let timeout = ((totalSize / 1024 / 100) * 1000) + 5000 // 100kb/s
log('设置消息超时时间', timeout) log('设置消息超时时间', timeout)
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout) const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout)
log('消息发送结果', returnMsg) log('消息发送结果', returnMsg)

View File

@@ -1,6 +1,5 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQUserApi } from '../../../ntqqapi/api' import { NTQQUserApi } from '@/ntqqapi/api'
import { groups } from '../../../common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {

View File

@@ -24,7 +24,7 @@ import {
User, User,
VideoElement, VideoElement,
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap } from '../common/data' import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap, uidMaps } from '../common/data'
import { EventType } from './event/OB11BaseEvent' import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
import { dbUtil } from '../common/db' import { dbUtil } from '../common/db'
@@ -47,6 +47,7 @@ import { mFaceCache } from '../ntqqapi/constructor'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent' import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent' import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent'
import { OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
let lastRKeyUpdateTime = 0 let lastRKeyUpdateTime = 0
@@ -161,10 +162,12 @@ export class OB11Constructor {
// message_data["data"]["file"] = element.picElement.sourcePath // message_data["data"]["file"] = element.picElement.sourcePath
let fileName = element.picElement.fileName let fileName = element.picElement.fileName
const sourcePath = element.picElement.sourcePath const sourcePath = element.picElement.sourcePath
if (element.picElement.picType === PicType.gif && !fileName.endsWith('.gif')) { const isGif = element.picElement.picType === PicType.gif
if (isGif && !fileName.endsWith('.gif')) {
fileName += '.gif' fileName += '.gif'
} }
message_data['data']['file'] = fileName message_data['data']['file'] = fileName
message_data['data']['subType'] = element.picElement.picSubType
// message_data["data"]["path"] = element.picElement.sourcePath // message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" // let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
@@ -188,9 +191,7 @@ export class OB11Constructor {
element.picElement.sourcePath, element.picElement.sourcePath,
) )
}, },
}) }).then()
.then()
// 不在自动下载图片
} }
else if (element.videoElement || element.fileElement) { else if (element.videoElement || element.fileElement) {
const videoOrFileElement = element.videoElement || element.fileElement const videoOrFileElement = element.videoElement || element.fileElement
@@ -200,6 +201,13 @@ export class OB11Constructor {
message_data['data']['path'] = videoOrFileElement.filePath message_data['data']['path'] = videoOrFileElement.filePath
message_data['data']['file_id'] = videoOrFileElement.fileUuid message_data['data']['file_id'] = videoOrFileElement.fileUuid
message_data['data']['file_size'] = videoOrFileElement.fileSize message_data['data']['file_size'] = videoOrFileElement.fileSize
if (element.videoElement) {
message_data['data']['url'] = await NTQQFileApi.getVideoUrl({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId, element.elementId,
)
}
dbUtil dbUtil
.addFileCache(videoOrFileElement.fileUuid, { .addFileCache(videoOrFileElement.fileUuid, {
msgId: msg.msgId, msgId: msg.msgId,
@@ -290,7 +298,7 @@ export class OB11Constructor {
if (message_data.type !== 'unknown' && message_data.data) { if (message_data.type !== 'unknown' && message_data.data) {
const cqCode = encodeCQCode(message_data) const cqCode = encodeCQCode(message_data)
if (messagePostFormat === 'string') { if (messagePostFormat === 'string') {
;(resMsg.message as string) += cqCode (resMsg.message as string) += cqCode
} }
else (resMsg.message as OB11MessageData[]).push(message_data) else (resMsg.message as OB11MessageData[]).push(message_data)
@@ -485,6 +493,16 @@ export class OB11Constructor {
} }
* */ * */
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
let pokedetail: any[] = json.items;
//筛选item带有uid的元素
pokedetail = pokedetail.filter(item => item.uid);
if (pokedetail.length == 2) {
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[pokedetail[0].uid])!), parseInt((uidMaps[pokedetail[1].uid])));
}
}
const memberUin = json.items[1].param[0] const memberUin = json.items[1].param[0]
const title = json.items[3].txt const title = json.items[3].txt
log('收到群成员新头衔消息', json) log('收到群成员新头衔消息', json)

View File

@@ -50,6 +50,7 @@ export function encodeCQCode(data: OB11MessageData) {
} }
const CQCodeEscape = (text: string) => { const CQCodeEscape = (text: string) => {
text = text.toString()
return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;').replace(/,/g, '&#44;') return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;').replace(/,/g, '&#44;')
} }

View File

@@ -21,10 +21,10 @@ export class OB11FriendPokeEvent extends OB11PokeEvent {
export class OB11GroupPokeEvent extends OB11PokeEvent { export class OB11GroupPokeEvent extends OB11PokeEvent {
group_id: number group_id: number
constructor(group_id: number, user_id: number = 0) { constructor(group_id: number, user_id: number = 0, target_id: number = 0) {
super() super()
this.group_id = group_id this.group_id = group_id
this.target_id = user_id this.target_id = target_id
this.user_id = user_id this.user_id = user_id
} }
} }

View File

@@ -10,6 +10,7 @@ import { WebSocket as WebSocketClass } from 'ws'
import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent' import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent'
import { log } from '../../../common/utils/log' import { log } from '../../../common/utils/log'
import { getConfigUtil } from '../../../common/config' import { getConfigUtil } from '../../../common/config'
import { version } from '../../../version'
export let rwsList: ReverseWebsocket[] = [] export let rwsList: ReverseWebsocket[] = []
@@ -85,6 +86,7 @@ export class ReverseWebsocket {
'X-Self-ID': selfInfo.uin, 'X-Self-ID': selfInfo.uin,
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段 'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段
'User-Agent': `LLOneBot/${version}`,
}, },
}) })
registerWsEventSender(this.websocket) registerWsEventSender(this.websocket)

View File

@@ -27,6 +27,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
} }
try { try {
let handleResult = await action.websocketHandle(params, echo) let handleResult = await action.websocketHandle(params, echo)
handleResult.echo = echo
wsReply(wsClient, handleResult) wsReply(wsClient, handleResult)
} catch (e) { } catch (e) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo)) wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo))

View File

@@ -62,6 +62,13 @@ async function onSettingWindowCreated(view: Element) {
SettingButton('请稍候', 'llonebot-update-button', 'secondary'), SettingButton('请稍候', 'llonebot-update-button', 'secondary'),
), ),
]), ]),
SettingList([
SettingItem(
'是否启用 LLOneBot, 重启QQ后生效',
null,
SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }),
)]
),
SettingList([ SettingList([
SettingItem( SettingItem(
'启用 HTTP 服务', '启用 HTTP 服务',

View File

@@ -1 +1 @@
export const version = '3.26.3' export const version = '3.27.0'