Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/更新日志.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
V7.12.14
更新时间 2026-05-15

* Milky 修复 get_history_messages API 获取包含 ark 的消息时可能报错

=================
V7.12.13
更新时间 2026-05-05

Expand Down
2 changes: 1 addition & 1 deletion package-dist.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"name":"llonebot-dist","version":"7.12.13","type":"module","description":"","main":"llbot.js","author":"linyuchen","repository":{"type":"git","url":"https://github.com/LLOneBot/LuckyLilliaBot"}}
{"name":"llonebot-dist","version":"7.12.14","type":"module","description":"","main":"llbot.js","author":"linyuchen","repository":{"type":"git","url":"https://github.com/LLOneBot/LuckyLilliaBot"}}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dependencies": {
"@cordisjs/plugin-logger": "^1.0.2",
"@cordisjs/plugin-timer": "^1.1.1",
"@hono/node-server": "^2.0.0",
"@hono/node-server": "^2.0.2",
"@minatojs/driver-sqlite": "^5.0.3",
"@saltify/milky-types": "^1.2.2",
"@saltify/typeproto": "^0.4.1",
Expand All @@ -30,11 +30,11 @@
"compare-versions": "^6.1.1",
"cordis": "^4.0.0-rc.4",
"cosmokit": "^1.8.1",
"fast-xml-parser": "^5.7.2",
"fast-xml-parser": "^5.7.3",
"file-type": "^22.0.1",
"fluent-ffmpeg": "^2.1.3",
"get-port": "^7.2.0",
"hono": "^4.12.15",
"hono": "^4.12.18",
"image-size": "^2.0.2",
"json5": "^2.2.3",
"minato": "^4.0.1",
Expand All @@ -55,7 +55,7 @@
"ts-case-convert": "^2.1.0",
"tsx": "^4.21.0",
"typescript": "^6.0.3",
"vite": "^8.0.10",
"vite": "^8.0.11",
"vite-plugin-cp": "^6.0.3",
"vitest": "^4.1.5"
},
Expand Down
15 changes: 1 addition & 14 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,25 +147,12 @@ export interface CheckVersion {
version: string
}


export interface FileCache {
fileName: string
fileSize: string
msgId: string
peerUid: string
chatType: number
elementId: string
elementType: number
}

export interface FileCacheV2 {
fileName: string
fileSize: string
fileUuid: string
msgId: string
msgTime: number
peerUid: string
chatType: number
elementId: string
elementType: number
md5HexStr: string
}
31 changes: 19 additions & 12 deletions src/common/utils/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TEMP_DIR } from '../globalVars'
import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { Context } from 'cordis'
import { ChatType, ElementType } from '@/ntqqapi/types'

// 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
Expand Down Expand Up @@ -122,7 +123,7 @@ export async function uri2local(ctx: Context, uri: string, needExt?: boolean): P
let filePath = path.join(TEMP_DIR, fileName)
await fsPromise.writeFile(filePath, res.data)
if (needExt) {
const ext = (await ctx.ntFileApi.getFileType(filePath)).ext
const ext = (await getFileType(filePath)).ext
fileName += `.${ext}`
const newPath = `${filePath}.${ext}`
await fsPromise.rename(filePath, newPath)
Expand All @@ -141,7 +142,7 @@ export async function uri2local(ctx: Context, uri: string, needExt?: boolean): P
const base64 = uri.replace(/^base64:\/\//, '')
await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) {
const ext = (await ctx.ntFileApi.getFileType(filePath)).ext
const ext = (await getFileType(filePath)).ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
Expand All @@ -158,7 +159,7 @@ export async function uri2local(ctx: Context, uri: string, needExt?: boolean): P
let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) {
const ext = (await ctx.ntFileApi.getFileType(filePath)).ext
const ext = (await getFileType(filePath)).ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
Expand All @@ -174,15 +175,21 @@ export async function uri2local(ctx: Context, uri: string, needExt?: boolean): P
fileCache = await ctx.store.getFileCacheByName(uri)
}
if (fileCache?.length) {
const downloadPath = await ctx.ntFileApi.downloadMedia(
fileCache[0].msgId,
fileCache[0].chatType,
fileCache[0].peerUid,
fileCache[0].elementId,
'',
'',
)
return { success: true, errMsg: '', fileName: fileCache[0].fileName, path: downloadPath, isLocal: true }
const isGroup = fileCache[0].chatType === ChatType.Group
let url
if (fileCache[0].elementType === ElementType.Pic) {
const originImageUrl = `/download?appid=${isGroup ? 1407 : 1406}&fileid=${fileCache[0].fileUuid}&spec=0`
url = await ctx.ntFileApi.getImageUrl(originImageUrl, fileCache[0].md5HexStr)
} else if (fileCache[0].elementType === ElementType.Video) {
url = await ctx.ntFileApi.getVideoUrl(fileCache[0].fileUuid, isGroup)
} else if (fileCache[0].elementType === ElementType.Ptt) {
url = await ctx.ntFileApi.getPttUrl(fileCache[0].fileUuid, isGroup)
}
if (url) {
return await uri2local(ctx, url, needExt)
} else {
return { success: false, errMsg: `不支持的文件类型: ${fileCache[0].elementType}`, fileName: '', path: '', isLocal: false }
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/common/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ export function uint32ToIPV4Addr(value: number) {
export function sleep(ms = 0) {
return new Promise<void>((resolve) => setTimeout(resolve, ms))
}

export function isHttpUrl(str: string) {
return /^https?:\/\/.+/.test(str)
}
2 changes: 0 additions & 2 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { Context } from 'cordis'
import { selfInfo, LOG_DIR, TEMP_DIR, dbDir } from '../common/globalVars'
import {
NTQQFileApi,
NTQQFileCacheApi,
NTQQFriendApi,
NTQQGroupApi,
NTLoginApi,
Expand Down Expand Up @@ -64,7 +63,6 @@ async function onLoad() {
ctx.plugin(ConfigService)
ctx.plugin(PMHQ)
ctx.plugin(NTQQFileApi)
ctx.plugin(NTQQFileCacheApi)
ctx.plugin(NTQQFriendApi)
ctx.plugin(NTQQGroupApi)
ctx.plugin(NTLoginApi)
Expand Down
4 changes: 2 additions & 2 deletions src/main/pmhq/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PMHQBase } from './base'
import { FriendMixin, GroupMixin, MediaMixin, MessageMixin, UserMixin } from './mixins'
import { FriendMixin, GroupMixin, MediaMixin, MessageMixin, SystemMixin, UserMixin } from './mixins'

export type {
PBData,
Expand Down Expand Up @@ -49,7 +49,7 @@ function applyMixins<TBase extends Constructor, TMixins extends readonly Mixin<a
/**
* PMHQ 类 - 通过 Mixin 模式组合所有功能
*/
const mixins = [GroupMixin, FriendMixin, MediaMixin, MessageMixin, UserMixin] as const
const mixins = [GroupMixin, FriendMixin, MediaMixin, MessageMixin, UserMixin, SystemMixin] as const
export const PMHQ = applyMixins(PMHQBase, mixins)

declare module 'cordis' {
Expand Down
63 changes: 63 additions & 0 deletions src/main/pmhq/mixins/friend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,68 @@ export function FriendMixin<T extends new (...args: any[]) => PMHQBase>(Base: T)
url: `https://${download.downloadDns}/ftn_handler/${download.downloadUrl.toString('hex')}/?fname=${encodeURIComponent(fileName)}`
}
}

async setFriendRequest(targetUid: string, accept: number) {
const body = Oidb.SetFriendRequestReq.encode({
targetUid,
accept,
})
const data = Oidb.Base.encode({
command: 0xb5d,
subCommand: 44,
body,
})
await this.httpSendPB('OidbSvcTrpcTcp.0xb5d_44', data)
}

async setFilteredFriendRequestReq(selfUid: string, requestUid: string) {
const body = Oidb.SetFilteredFriendRequestReq.encode({
selfUid,
requestUid,
})
const data = Oidb.Base.encode({
command: 0xd72,
subCommand: 0,
body,
})
await this.httpSendPB('OidbSvcTrpcTcp.0xd72_0', data)
}

async fetchFriends() {
const body = Oidb.IncPullReq.encode({
reqCount: 500,
flag: 1,
requestBiz: [{
bizType: 1,
bizData: {
extBusi: [102, 103, 20002, 20009, 20031, 20037, 27394]
}
}]
})
const data = Oidb.Base.encode({
command: 0xfd4,
subCommand: 1,
body,
})
const res = await this.httpSendPB('OidbSvcTrpcTcp.0xfd4_1', data)
const oidbRespBody = Oidb.Base.decode(Buffer.from(res.pb, 'hex')).body
return Oidb.IncPullResp.decode(oidbRespBody)
}

async getFriendRecommendContactArk(uin: number) {
const body = Oidb.GetFriendRecommendContactArkReq.encode({
uin,
phoneNumber: '-',
jumpUrl: `mqqapi://card/show_pslcard?src_type=internal&source=sharecard&version=1&uin=${uin}`,
})
const data = Oidb.Base.encode({
command: 0x12b6,
subCommand: 0,
body,
})
const res = await this.httpSendPB('OidbSvcTrpcTcp.0x12b6_0', data)
const oidbRespBody = Oidb.Base.decode(Buffer.from(res.pb, 'hex')).body
return Oidb.GetFriendRecommendContactArkResp.decode(oidbRespBody)
}
}
}
1 change: 1 addition & 0 deletions src/main/pmhq/mixins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { GroupMixin } from './group'
export { MediaMixin } from './media'
export { MessageMixin } from './message'
export { UserMixin } from './user'
export { SystemMixin } from './system'
87 changes: 79 additions & 8 deletions src/main/pmhq/mixins/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ export function MediaMixin<T extends new (...args: any[]) => PMHQBase>(Base: T)
const data = Oidb.Base.encode({ command: 0x126d, subCommand: 200, body })
const res = await this.httpSendPB('OidbSvcTrpcTcp.0x126d_200', data)
const oidbRespBody = Oidb.Base.decode(Buffer.from(res.pb, 'hex')).body
const { download } = Media.NTV2RichMediaResp.decode(oidbRespBody)
return `https://${download?.info?.domain}${download?.info?.urlPath}${download?.rKeyParam}`
return Media.NTV2RichMediaResp.decode(oidbRespBody)
}

async getGroupPttUrl(fileUuid: string) {
Expand All @@ -82,8 +81,7 @@ export function MediaMixin<T extends new (...args: any[]) => PMHQBase>(Base: T)
const data = Oidb.Base.encode({ command: 0x126e, subCommand: 200, body })
const res = await this.httpSendPB('OidbSvcTrpcTcp.0x126e_200', data)
const oidbRespBody = Oidb.Base.decode(Buffer.from(res.pb, 'hex')).body
const { download } = Media.NTV2RichMediaResp.decode(oidbRespBody)
return `https://${download!.info.domain}${download!.info.urlPath}${download!.rKeyParam}`
return Media.NTV2RichMediaResp.decode(oidbRespBody)
}

async getGroupVideoUrl(fileUuid: string) {
Expand All @@ -98,8 +96,7 @@ export function MediaMixin<T extends new (...args: any[]) => PMHQBase>(Base: T)
const data = Oidb.Base.encode({ command: 0x11ea, subCommand: 200, body })
const res = await this.httpSendPB('OidbSvcTrpcTcp.0x11ea_200', data)
const oidbRespBody = Oidb.Base.decode(Buffer.from(res.pb, 'hex')).body
const { download } = Media.NTV2RichMediaResp.decode(oidbRespBody)
return `https://${download!.info.domain}${download!.info.urlPath}${download!.rKeyParam}`
return Media.NTV2RichMediaResp.decode(oidbRespBody)
}

async getPrivateVideoUrl(fileUuid: string) {
Expand All @@ -114,8 +111,7 @@ export function MediaMixin<T extends new (...args: any[]) => PMHQBase>(Base: T)
const data = Oidb.Base.encode({ command: 0x11e9, subCommand: 200, body })
const res = await this.httpSendPB('OidbSvcTrpcTcp.0x11e9_200', data)
const oidbRespBody = Oidb.Base.decode(Buffer.from(res.pb, 'hex')).body
const { download } = Media.NTV2RichMediaResp.decode(oidbRespBody)
return `https://${download!.info.domain}${download!.info.urlPath}${download!.rKeyParam}`
return Media.NTV2RichMediaResp.decode(oidbRespBody)
}

async getHighwaySession() {
Expand Down Expand Up @@ -284,5 +280,80 @@ export function MediaMixin<T extends new (...args: any[]) => PMHQBase>(Base: T)
sha3CheckSum,
}
}

async getGroupImageUploadInfo(groupCode: string, filePath: string) {
const peer = {
chatType: ChatType.Group,
peerUid: groupCode,
guildId: ''
}
const body = await NTV2RichMedia.buildUploadReq(
peer,
{ type: 'image', filePath },
{
pic: {
summary: '[图片]',
bytesPbReserveC2c: Buffer.from([0x08, 0x00, 0x18, 0x00, 0x20, 0x00, 0x4A, 0x00, 0x50, 0x00, 0x62, 0x00, 0x92, 0x01, 0x00, 0x9A, 0x01, 0x00, 0xAA, 0x01, 0x0C, 0x08, 0x00, 0x12, 0x00, 0x18, 0x00, 0x20, 0x00, 0x28, 0x00, 0x3A, 0x00])
}
},
)
const data = Oidb.Base.encode({ command: 0x11c4, subCommand: 100, body })
const res = await this.httpSendPB('OidbSvcTrpcTcp.0x11c4_100', data)
const oidbRespBody = Oidb.Base.decode(Buffer.from(res.pb, 'hex')).body
const { upload } = Media.NTV2RichMediaResp.decode(oidbRespBody)
return {
info: upload.msgInfo,
compat: upload.compatQMsg,
ext: NTV2RichMedia.generateExt(upload)
}
}

async getC2CImageUploadInfo(peerUid: string, filePath: string) {
const peer = {
chatType: ChatType.C2C,
peerUid,
guildId: ''
}
const body = await NTV2RichMedia.buildUploadReq(
peer,
{ type: 'image', filePath },
{
pic: {
summary: '[图片]',
bytesPbReserveC2c: Buffer.from([0x08, 0x00, 0x18, 0x00, 0x20, 0x00, 0x4A, 0x00, 0x50, 0x00, 0x62, 0x00, 0x92, 0x01, 0x00, 0x9A, 0x01, 0x00, 0xAA, 0x01, 0x0C, 0x08, 0x00, 0x12, 0x00, 0x18, 0x00, 0x20, 0x00, 0x28, 0x00, 0x3A, 0x00])
}
},
)
const data = Oidb.Base.encode({ command: 0x11c5, subCommand: 100, body })
const res = await this.httpSendPB('OidbSvcTrpcTcp.0x11c5_100', data)
const oidbRespBody = Oidb.Base.decode(Buffer.from(res.pb, 'hex')).body
const { upload } = Media.NTV2RichMediaResp.decode(oidbRespBody)
return {
info: upload.msgInfo,
compat: upload.compatQMsg,
ext: NTV2RichMedia.generateExt(upload)
}
}

async imageOcr(imageUrl: string) {
const body = Oidb.ImageOcrReq.encode({
version: 1,
client: 0,
entrance: 1,
ocrReqBody: {
imageUrl,
originMd5: '',
afterCompressMd5: '',
afterCompressFileSize: '',
afterCompressWeight: '',
afterCompressHeight: '',
isCut: false
}
})
const data = Oidb.Base.encode({ command: 0xe07, subCommand: 0, body })
const res = await this.httpSendPB('OidbSvcTrpcTcp.0xe07_0', data)
const oidbRespBody = Oidb.Base.decode(Buffer.from(res.pb, 'hex')).body
return Oidb.ImageOcrResp.decode(oidbRespBody)
}
}
}
17 changes: 17 additions & 0 deletions src/main/pmhq/mixins/system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Oidb } from '@/ntqqapi/proto'
import type { PMHQBase } from '../base'

export function SystemMixin<T extends new (...args: any[]) => PMHQBase>(Base: T) {
return class extends Base {
async fetchPins() {
const data = Oidb.Base.encode({
command: 0x12b3,
subCommand: 0,
body: Buffer.alloc(0),
})
const res = await this.httpSendPB('OidbSvcTrpcTcp.0x12b3_0', data)
const oidbRespBody = Oidb.Base.decode(Buffer.from(res.pb, 'hex')).body
return Oidb.FetchPinsResp.decode(oidbRespBody)
}
}
}
Loading
Loading