From 51c2fa285a2c03596f7b05de0cf205dc9d7efbb2 Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Sat, 9 May 2026 04:08:52 +0800 Subject: [PATCH] refactor: move web action into web-actions from bridge-actions --- packages/core/src/bridge/bridge-actions.ts | 212 ------------------ packages/core/src/bridge/bridge.ts | 4 +- packages/core/src/bridge/web-actions.ts | 221 +++++++++++++++++++ packages/core/src/onebot/actions/extended.ts | 9 +- 4 files changed, 231 insertions(+), 215 deletions(-) diff --git a/packages/core/src/bridge/bridge-actions.ts b/packages/core/src/bridge/bridge-actions.ts index 7cb0efdf..a3c85aae 100644 --- a/packages/core/src/bridge/bridge-actions.ts +++ b/packages/core/src/bridge/bridge-actions.ts @@ -10,10 +10,6 @@ import { buildSendElems } from './builders/element-builder'; import { parseMsgPush } from './handlers/msg-push-handler'; import type { ForwardNodePayload, MessageElement } from './events'; import { PushMsgSchema } from './proto/message'; -import { RequestUtil } from './web/request-util'; -import { getHonorListWebAPI, WebHonorType } from './web/group-honor'; -import { getGroupEssenceMsg, getGroupEssenceMsgAll } from './web/group-essence'; -import { setGroupNoticeWebAPI } from './web/group-notice' import { OidbMuteMemberSchema, OidbMuteAllSchema, @@ -48,10 +44,6 @@ import { GroupRecallRequestSchema, C2CRecallRequestSchema, SsoReadedReportReqSchema, - OidbClientKeyRespSchema, - OidbClientKeyReqSchema, - OidbGetPskeyReqSchema, - OidbGetPskeyRespSchema } from './proto/oidb-action'; import { FileUploadExtSchema } from './proto/highway'; import { @@ -1298,208 +1290,4 @@ export async function markGroupMessageRead( if (!result.success) { throw new Error(result.errorMessage || 'mark group message read failed'); } -} - -export async function forceFetchClientKey(bridge: Bridge) { - const resp = await sendOidbAndDecode( - bridge, - 'OidbSvcTrpcTcp.0x102a_1', - 0x102A, - 1, - {}, - OidbClientKeyReqSchema, - OidbClientKeyRespSchema - ); - - const clientKey = resp?.clientKey || ''; - const keyIndex = String(resp?.keyIndex || '19'); // 不知道是什么 - - return { - clientKey, - keyIndex, - expireTime: String(resp?.expireTime || '1800') - }; -} - -export async function getPSkey(bridge: Bridge, domainList: string[]) { - const resp = await sendOidbAndDecode( - bridge, - 'OidbSvcTrpcTcp.0x102a_0', - 0x102A, - 0, - { domainList }, - OidbGetPskeyReqSchema, - OidbGetPskeyRespSchema - ); - - const domainPskeyMap = new Map(); - - if (resp?.pskeyItems && Array.isArray(resp.pskeyItems)) { - for (const item of resp.pskeyItems) { - if (item.domain && item.pskey) { - domainPskeyMap.set(item.domain, item.pskey); - } - } - } - - return { - domainPskeyMap, - }; -} - -export async function getCookies(bridge: Bridge, domain: string) { - const ClientKeyData = await forceFetchClientKey(bridge); - - // 构造 ptlogin2 跳转 URL - const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + bridge.qqInfo.uin + - '&clientkey=' + ClientKeyData.clientKey + - '&u1=https%3A%2F%2F' + domain + '%2F' + bridge.qqInfo.uin + '%2Finfocenter&keyindex=' + ClientKeyData.keyIndex; - - const data = await RequestUtil.HttpsGetCookies(requestUrl); - - if (!data['p_skey'] || data['p_skey'].length === 0) { - try { - const pskeyData = await getPSkey(bridge, [domain]); - const pskey = pskeyData.domainPskeyMap.get(domain); - if (pskey) { - data['p_skey'] = pskey; - } - } catch { - return data; - } - } - - return data; -} - -export async function getGroupHonorInfo(bridge: Bridge, groupId: number, type: WebHonorType | string) { - const groupCode = groupId.toString(); - const cookieObject = await getCookies(bridge, 'qun.qq.com'); - - const honorInfo: any = { - group_id: groupId, - current_talkative: null, - talkative_list: [], - performer_list: [], - legend_list: [], - emotion_list: [], - strong_newbie_list: [], - }; - - if (type === WebHonorType.TALKATIVE || type === WebHonorType.ALL) { - const talkativeList = await getHonorListWebAPI(cookieObject, groupCode, 1); - if (talkativeList.length > 0) { - honorInfo.current_talkative = talkativeList[0]; - honorInfo.talkative_list = talkativeList; - } - } - - if (type === WebHonorType.PERFORMER || type === WebHonorType.ALL) { - honorInfo.performer_list = await getHonorListWebAPI(cookieObject, groupCode, 2); - } - - if (type === WebHonorType.LEGEND || type === WebHonorType.ALL) { - honorInfo.legend_list = await getHonorListWebAPI(cookieObject, groupCode, 3); - } - - if (type === WebHonorType.EMOTION || type === WebHonorType.ALL) { - honorInfo.emotion_list = await getHonorListWebAPI(cookieObject, groupCode, 6); - } - - return honorInfo; -} - -export async function getSKey(bridge: Bridge): Promise { - const ClientKeyData = await forceFetchClientKey(bridge); - - if (!ClientKeyData.clientKey) { - throw new Error('getClientKey Error: clientKey is empty'); - } - - const u1 = encodeURIComponent('https://h5.qzone.qq.com/qqnt/qzoneinpcqq/friend?refresh=0&clientuin=0&darkMode=0'); - const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033' + - '&clientuin=' + bridge.qqInfo.uin + - '&clientkey=' + ClientKeyData.clientKey + - '&u1=' + u1 + - '&keyindex=' + ClientKeyData.keyIndex; - - const cookies: { [key: string]: string } = await RequestUtil.HttpsGetCookies(requestUrl); - const skey = cookies['skey']; - - if (!skey) { - throw new Error('SKey is Empty'); - } - - return skey; -} - -/** - * 分页获取群精华消息 - */ -export async function getGroupEssence(bridge: Bridge, groupId: number, pageStart: number = 0, pageLimit: number = 50) { - const groupCode = groupId.toString(); - const cookieObject = await getCookies(bridge, 'qun.qq.com'); - - const essenceData = await getGroupEssenceMsg(cookieObject, groupCode, pageStart, pageLimit); - - return essenceData || { retcode: -1, data: { is_end: true, msg_list: [] } }; -} - -/** - * 获取所有群精华消息 - */ -export async function getGroupEssenceAll(bridge: Bridge, groupId: number) { - const groupCode = groupId.toString(); - const cookieObject = await getCookies(bridge, 'qun.qq.com'); - - const essenceDataAll = await getGroupEssenceMsgAll(cookieObject, groupCode); - - return essenceDataAll; -} - -export async function sendGroupNotice( - bridge: Bridge, - groupId: number, - content: string, - options?: { - image?: string; // 未来如果要发图片,传路径进来 - pinned?: number; - type?: number; - confirm_required?: number; - } -) { - const groupCode = groupId.toString(); - const cookieObject = await getCookies(bridge, 'qun.qq.com'); - - let picId = ''; - let imgWidth = 540; - let imgHeight = 300; - - // 📝 预留的图片上传逻辑: - // if (options?.image) { - // const picInfo = await bridge.uploadGroupBulletinPic(groupCode, options.image); - // picId = picInfo.id; - // imgWidth = picInfo.width; - // imgHeight = picInfo.height; - // } - - const ret = await setGroupNoticeWebAPI( - cookieObject, - groupCode, - content, - options?.pinned ?? 0, - options?.type ?? 1, - 1, - 1, - options?.confirm_required ?? 1, - picId, - imgWidth, - imgHeight - ); - - if (!ret || ret.ec !== 0) { - throw new Error(`设置群公告失败: ${ret?.em || '未知错误(Cookie过期或权限不足)'}`); - } - - return ret; } \ No newline at end of file diff --git a/packages/core/src/bridge/bridge.ts b/packages/core/src/bridge/bridge.ts index ba095316..19b9618d 100644 --- a/packages/core/src/bridge/bridge.ts +++ b/packages/core/src/bridge/bridge.ts @@ -65,12 +65,14 @@ import { markGroupMessageRead as markPrivateMsgAsRead_, setFriendRemark as setFriendRemark_, fetchGroupFileCount as fetchGroupFileCount_, +} from './bridge-actions'; +import { getGroupHonorInfo as getGroupHonorInfo_, forceFetchClientKey as forceFetchClientKey_, getGroupEssence as getGroupEssence_, getGroupEssenceAll as getGroupEssenceAll_, sendGroupNotice as sendGroupNotice_, -} from './bridge-actions'; +} from './web-actions'; import type { GroupFilesResult } from './bridge-actions'; import type { MediaIndexNode } from './bridge-actions'; diff --git a/packages/core/src/bridge/web-actions.ts b/packages/core/src/bridge/web-actions.ts index e69de29b..f427bf57 100644 --- a/packages/core/src/bridge/web-actions.ts +++ b/packages/core/src/bridge/web-actions.ts @@ -0,0 +1,221 @@ +// Web API operations extracted from Bridge. + +import type { Bridge } from './bridge'; +import { sendOidbAndDecode } from './bridge-oidb'; +import { resolveUserUid } from './bridge-oidb'; +import { + OidbClientKeyRespSchema, + OidbClientKeyReqSchema, + OidbGetPskeyReqSchema, + OidbGetPskeyRespSchema +} from './proto/oidb-action'; +import { RequestUtil } from './web/request-util'; +import { getHonorListWebAPI, WebHonorType } from './web/group-honor'; +import { getGroupEssenceMsg, getGroupEssenceMsgAll } from './web/group-essence'; +import { setGroupNoticeWebAPI } from './web/group-notice'; + +export async function forceFetchClientKey(bridge: Bridge) { + const resp = await sendOidbAndDecode( + bridge, + 'OidbSvcTrpcTcp.0x102a_1', + 0x102A, + 1, + {}, + OidbClientKeyReqSchema, + OidbClientKeyRespSchema + ); + + const clientKey = resp?.clientKey || ''; + const keyIndex = String(resp?.keyIndex || '19'); // 不知道是什么 + + return { + clientKey, + keyIndex, + expireTime: String(resp?.expireTime || '1800') + }; +} + +export async function getPSkey(bridge: Bridge, domainList: string[]) { + const resp = await sendOidbAndDecode( + bridge, + 'OidbSvcTrpcTcp.0x102a_0', + 0x102A, + 0, + { domainList }, + OidbGetPskeyReqSchema, + OidbGetPskeyRespSchema + ); + + const domainPskeyMap = new Map(); + + if (resp?.pskeyItems && Array.isArray(resp.pskeyItems)) { + for (const item of resp.pskeyItems) { + if (item.domain && item.pskey) { + domainPskeyMap.set(item.domain, item.pskey); + } + } + } + + return { + domainPskeyMap, + }; +} + +export async function getCookies(bridge: Bridge, domain: string) { + const ClientKeyData = await forceFetchClientKey(bridge); + + // 构造 ptlogin2 跳转 URL + const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + bridge.qqInfo.uin + + '&clientkey=' + ClientKeyData.clientKey + + '&u1=https%3A%2F%2F' + domain + '%2F' + bridge.qqInfo.uin + '%2Finfocenter&keyindex=' + ClientKeyData.keyIndex; + + const data = await RequestUtil.HttpsGetCookies(requestUrl); + + if (!data['p_skey'] || data['p_skey'].length === 0) { + try { + const pskeyData = await getPSkey(bridge, [domain]); + const pskey = pskeyData.domainPskeyMap.get(domain); + if (pskey) { + data['p_skey'] = pskey; + } + } catch { + return data; + } + } + + return data; +} + +export async function getGroupHonorInfo(bridge: Bridge, groupId: number, type: WebHonorType | string) { + const groupCode = groupId.toString(); + const cookieObject = await getCookies(bridge, 'qun.qq.com'); + + const honorInfo: any = { + group_id: groupId, + current_talkative: null, + talkative_list: [], + performer_list: [], + legend_list: [], + emotion_list: [], + strong_newbie_list: [], + }; + + if (type === WebHonorType.TALKATIVE || type === WebHonorType.ALL) { + const talkativeList = await getHonorListWebAPI(cookieObject, groupCode, 1); + if (talkativeList.length > 0) { + honorInfo.current_talkative = talkativeList[0]; + honorInfo.talkative_list = talkativeList; + } + } + + if (type === WebHonorType.PERFORMER || type === WebHonorType.ALL) { + honorInfo.performer_list = await getHonorListWebAPI(cookieObject, groupCode, 2); + } + + if (type === WebHonorType.LEGEND || type === WebHonorType.ALL) { + honorInfo.legend_list = await getHonorListWebAPI(cookieObject, groupCode, 3); + } + + if (type === WebHonorType.EMOTION || type === WebHonorType.ALL) { + honorInfo.emotion_list = await getHonorListWebAPI(cookieObject, groupCode, 6); + } + + return honorInfo; +} + +export async function getSKey(bridge: Bridge): Promise { + const ClientKeyData = await forceFetchClientKey(bridge); + + if (!ClientKeyData.clientKey) { + throw new Error('getClientKey Error: clientKey is empty'); + } + + const u1 = encodeURIComponent('https://h5.qzone.qq.com/qqnt/qzoneinpcqq/friend?refresh=0&clientuin=0&darkMode=0'); + const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033' + + '&clientuin=' + bridge.qqInfo.uin + + '&clientkey=' + ClientKeyData.clientKey + + '&u1=' + u1 + + '&keyindex=' + ClientKeyData.keyIndex; + + const cookies: { [key: string]: string } = await RequestUtil.HttpsGetCookies(requestUrl); + const skey = cookies['skey']; + + if (!skey) { + throw new Error('SKey is Empty'); + } + + return skey; +} + +/** + * 分页获取群精华消息 + */ +export async function getGroupEssence(bridge: Bridge, groupId: number, pageStart: number = 0, pageLimit: number = 50) { + const groupCode = groupId.toString(); + const cookieObject = await getCookies(bridge, 'qun.qq.com'); + + const essenceData = await getGroupEssenceMsg(cookieObject, groupCode, pageStart, pageLimit); + + return essenceData || { retcode: -1, data: { is_end: true, msg_list: [] } }; +} + +/** + * 获取所有群精华消息 + */ +export async function getGroupEssenceAll(bridge: Bridge, groupId: number) { + const groupCode = groupId.toString(); + const cookieObject = await getCookies(bridge, 'qun.qq.com'); + + const essenceDataAll = await getGroupEssenceMsgAll(cookieObject, groupCode); + + return essenceDataAll; +} + +export async function sendGroupNotice( + bridge: Bridge, + groupId: number, + content: string, + options?: { + image?: string; // 未来如果要发图片,传路径进来 + pinned?: number; + type?: number; + confirm_required?: number; + } +) { + const groupCode = groupId.toString(); + const cookieObject = await getCookies(bridge, 'qun.qq.com'); + + let picId = ''; + let imgWidth = 540; + let imgHeight = 300; + + + // 暂时不知道怎么实现,后面有空再看看 + + // if (options?.image) { + // const picInfo = await bridge.uploadGroupBulletinPic(groupCode, options.image); + // picId = picInfo.id; + // imgWidth = picInfo.width; + // imgHeight = picInfo.height; + // } + + const ret = await setGroupNoticeWebAPI( + cookieObject, + groupCode, + content, + options?.pinned ?? 0, + options?.type ?? 1, + 1, + 1, + options?.confirm_required ?? 1, + picId, + imgWidth, + imgHeight + ); + + if (!ret || ret.ec !== 0) { + throw new Error(`设置群公告失败: ${ret?.em || '未知错误(Cookie过期或权限不足)'}`); + } + + return ret; +} diff --git a/packages/core/src/onebot/actions/extended.ts b/packages/core/src/onebot/actions/extended.ts index 0d09a3c9..0d37f2a7 100644 --- a/packages/core/src/onebot/actions/extended.ts +++ b/packages/core/src/onebot/actions/extended.ts @@ -230,20 +230,25 @@ export function register(h: ApiHandler, ctx: ApiActionContext): void { // --- Group notice stubs --- - h.registerAction('send_group_notice', async (params) => { + h.registerAction('_send_group_notice', async (params) => { const groupId = asNumber(params.group_id); const content = asString(params.content); + const image = asString(params.image); if (!groupId || !content) { return failedResponse(RETCODE.BAD_REQUEST, 'group_id and content are required'); } + + if (image) { + return failedResponse(RETCODE.ACTION_FAILED, 'not implemented: image upload for group notice is not supported yet'); + } + if (!ctx.sendGroupNotice) { return failedResponse(RETCODE.ACTION_FAILED, 'not implemented'); } try { const options = { - image: asString(params.image), pinned: params.pinned !== undefined ? Number(params.pinned) : 0, type: params.type !== undefined ? Number(params.type) : 1, confirm_required: params.confirm_required !== undefined ? Number(params.confirm_required) : 1,