SnowCat 雪猫底层全源码:NapCat 4.18.28 底座 + SnowLuma 能力移植 + V8 通道适配 9.9.37

26 包 monorepo(TS 全源码):
- snowcat-core: NT 内核 API 封装 + listener(含 9.9.37 六回调适配)+ Packet 发包
- snowcat-onebot: OneBot v11 全动作 + MCP Streamable HTTP 适配器 + 稳定线
- snowcat-webui-frontend/backend: 雪猫控制台(React+HeroUI)
- snowcat-shell: 单文件 bundle 出口(vite inlineDynamicImports)
- snowcat-native: napi2native / MoeHoo / ffmpeg / pty 运行时
- 56 项 SnowLuma 独门动作移植(qzone 全家桶/群相册/装扮/闪照/收藏)
- 工具件: 解包器/启动器/便携制作器/doctor

GitHub 关联已全量指向 DandanLLab/SnowCatQQ
This commit is contained in:
天天的鸟蛋蛋 committed 2026-11-12 06:27:36 +08:00
commit b9655a5ab1
1048 files changed
+130851

No files matched your search

@@ -0,0 +1,43 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
// 兼容gocq 与name二选一
folder_name: Type.Optional(Type.String({ description: '文件夹名称' })),
// 兼容gocq 与folder_name二选一
name: Type.Optional(Type.String({ description: '文件夹名称' })),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
result: Type.Any({ description: '操作结果' }),
groupItem: Type.Any({ description: '群项信息' }),
}, { description: '创建文件夹结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class CreateGroupFileFolder extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GoCQHTTP_CreateGroupFileFolder;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '创建群文件目录';
override actionDescription = '在群文件系统中创建新的文件夹';
override actionTags = ['Go-CQHTTP'];
override payloadExample = {
group_id: '123456789',
folder_name: '新建文件夹',
};
override returnExample = {
result: {},
groupItem: {},
};
async _handle (payload: PayloadType) {
const folderName = payload.folder_name || payload.name;
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), folderName!)).resultWithGroupItem;
}
}
@@ -0,0 +1,33 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { FileNapCatOneBotUUID } from 'snowcat-common/src/file-uuid';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
file_id: Type.String({ description: '文件ID' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Any({ description: '删除结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class DeleteGroupFile extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GOCQHTTP_DeleteGroupFile;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '删除群文件';
override actionDescription = '在群文件系统中删除指定的文件';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.DeleteGroupFile.payload;
override returnExample = GoCQHTTPActionsExamples.DeleteGroupFile.response;
async _handle (payload: PayloadType) {
const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (!data || !data.fileId) throw new Error('Invalid file_id');
return await this.core.apis.GroupApi.delGroupFile(payload.group_id.toString(), [data.fileId]);
}
}
@@ -0,0 +1,32 @@
import { ActionName } from '@/snowcat-onebot/action/router';
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
folder_id: Type.Optional(Type.String({ description: '文件夹ID' })),
folder: Type.Optional(Type.String({ description: '文件夹ID' })),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Any({ description: '删除结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class DeleteGroupFileFolder extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GoCQHTTP_DeleteGroupFileFolder;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '删除群文件目录';
override actionDescription = '在群文件系统中删除指定的文件夹';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.DeleteGroupFileFolder.payload;
override returnExample = GoCQHTTPActionsExamples.DeleteGroupFileFolder.response;
async _handle (payload: PayloadType) {
return (await this.core.apis.GroupApi.delGroupFileFolder(
payload.group_id.toString(), payload.folder ?? payload.folder_id ?? '')).groupFileCommonResult;
}
}
@@ -0,0 +1,87 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import fs from 'fs';
import { join as joinPath } from 'node:path';
import { calculateFileMD5, uriToLocalFile } from 'snowcat-common/src/file';
import { randomUUID } from 'crypto';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
url: Type.Optional(Type.String({ description: '下载链接' })),
base64: Type.Optional(Type.String({ description: 'base64数据' })),
name: Type.Optional(Type.String({ description: '文件名' })),
headers: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], { description: '请求头' })),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
file: Type.String({ description: '文件路径' }),
}, { description: '下载结果' });
type ReturnType = Static<typeof ReturnSchema>;
export default class GoCQHTTPDownloadFile extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GoCQHTTP_DownloadFile;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '下载文件';
override actionDescription = '下载网络文件到本地临时目录';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.DownloadFile.payload;
override returnExample = GoCQHTTPActionsExamples.DownloadFile.response;
async _handle (payload: PayloadType): Promise<ReturnType> {
const isRandomName = !payload.name;
const name = payload.name || randomUUID();
let result: Awaited<globalThis.ReturnType<typeof uriToLocalFile>>;
if (payload.base64) {
result = await uriToLocalFile(this.core.SnowCatTempPath, `base64://${payload.base64}`, name);
} else if (payload.url) {
const headers = this.getHeaders(payload.headers);
result = await uriToLocalFile(this.core.SnowCatTempPath, payload.url, name, headers);
} else {
throw new Error('不存在任何文件, 无法下载');
}
if (!result.success) {
throw new Error(result.errMsg);
}
const filePath = result.path;
if (fs.existsSync(filePath)) {
if (isRandomName) {
// 默认实现要名称未填写时文件名为文件 md5
const md5 = await calculateFileMD5(filePath);
const newPath = joinPath(this.core.SnowCatTempPath, md5);
fs.renameSync(filePath, newPath);
return { file: newPath };
}
return { file: filePath };
} else {
throw new Error('文件写入失败, 检查权限');
}
}
getHeaders (headersIn?: string | string[]): Record<string, string> {
const headers: Record<string, string> = {};
if (typeof headersIn === 'string') {
headersIn = headersIn.split('[\\r\\n]');
}
if (Array.isArray(headersIn)) {
for (const headerItem of headersIn) {
const spilt = headerItem.indexOf('=');
if (spilt < 0) {
headers[headerItem] = '';
} else {
const key = headerItem.substring(0, spilt);
headers[key] = headerItem.substring(spilt + 1);
}
}
}
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/octet-stream';
}
return headers;
}
}
@@ -0,0 +1,170 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward, OB11MessageNodePlain as OB11MessageNode } from '@/snowcat-onebot/index';
import { ActionName } from '@/snowcat-onebot/action/router';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { Static, Type } from '@sinclair/typebox';
import { ChatType, ElementType, MsgSourceType, NTMsgType, RawMessage } from 'snowcat-core';
import { isNumeric } from 'snowcat-common/src/helper';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
message_id: Type.Optional(Type.String({ description: '消息ID' })),
id: Type.Optional(Type.String({ description: '消息ID' })),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
messages: Type.Optional(Type.Array(Type.Unknown(), { description: '消息列表' })),
}, { description: '合并转发消息' });
type ReturnType = Static<typeof ReturnSchema>;
function isForward (msg: OB11MessageData | string): msg is OB11MessageForward {
return typeof msg !== 'string' && msg.type === OB11MessageDataType.forward;
}
export class GoCQHTTPGetForwardMsgAction extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GoCQHTTP_GetForwardMsg;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取合并转发消息';
override actionDescription = '获取合并转发消息的具体内容';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GetForwardMsg.payload;
override returnExample = GoCQHTTPActionsExamples.GetForwardMsg.response;
private createTemplateNode (message: OB11Message): OB11MessageNode {
return {
type: OB11MessageDataType.node,
data: {
user_id: message.user_id,
nickname: message.sender.nickname,
message: [],
content: [],
},
};
}
async parseForward (messages: OB11Message[]): Promise<OB11MessageNode[]> {
const retMsg: OB11MessageNode[] = [];
for (const message of messages) {
const templateNode = this.createTemplateNode(message);
for (const msgdata of message.message) {
if (isForward(msgdata)) {
const newNode = this.createTemplateNode(message);
newNode.data.message = await this.parseForward(msgdata.data.content ?? []);
templateNode.data.message.push(newNode);
} else {
if (typeof msgdata === 'string') {
const textMsg: OB11MessageData = { type: OB11MessageDataType.text, data: { text: msgdata } };
templateNode.data.message.push(textMsg);
} else {
templateNode.data.message.push(msgdata);
}
}
}
retMsg.push(templateNode);
}
return retMsg;
}
async _handle (payload: PayloadType) {
// 1. 检查消息ID是否存在
const msgId = payload.message_id || payload.id;
if (!msgId) {
throw new Error('message_id is required');
}
// 2. 定义辅助函数 - 创建伪转发消息对象
const createFakeForwardMsg = (resId: string): RawMessage => {
const fakeMsg: RawMessage = {
chatType: ChatType.KCHATTYPEGROUP,
elements: [{
elementType: ElementType.MULTIFORWARD,
elementId: '',
multiForwardMsgElement: {
resId,
fileName: '',
xmlContent: '',
},
}],
guildId: '',
isOnlineMsg: false,
msgId: '', // TODO: no necessary
msgRandom: '0',
msgSeq: '',
msgTime: '',
msgType: NTMsgType.KMSGTYPEMIX,
parentMsgIdList: [],
parentMsgPeer: {
chatType: ChatType.KCHATTYPEGROUP,
peerUid: '',
},
peerName: '',
peerUid: '284840486',
peerUin: '284840486',
recallTime: '0',
records: [],
sendNickName: '',
sendRemarkName: '',
senderUid: '',
senderUin: '1094950020',
sourceType: MsgSourceType.K_DOWN_SOURCETYPE_UNKNOWN,
subMsgType: 1,
};
return fakeMsg;
};
// 3. 定义协议回退逻辑函数
const protocolFallbackLogic = async (resId: string) => {
const ob = (await this.obContext.apis.MsgApi.parseMessageV2(createFakeForwardMsg(resId), true))?.arrayMsg;
const firstMsg = ob?.message?.[0];
if (firstMsg && isForward(firstMsg)) {
return {
messages: firstMsg.data.content,
};
}
throw new Error('protocolFallbackLogic: 找不到相关的聊天记录');
};
// 4. 尝试通过正常渠道获取消息
// 如果是数字ID,优先使用getMsgsByMsgId获取
if (!isNumeric(msgId)) {
const ret = await protocolFallbackLogic(msgId);
if (ret.messages) {
return ret;
}
throw new Error('ResId无效: 找不到相关的聊天记录');
}
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId.toString());
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId);
if (rootMsg) {
// 5. 获取消息内容
const data = await this.core.apis.MsgApi.getMsgHistory(rootMsg.Peer, rootMsg.MsgId, 1);// getMsgsIncludeSelf
if (data && data.result === 0 && data.msgList.length > 0) {
const singleMsg = data.msgList[0];
if (!singleMsg) {
throw new Error('消息不存在或已过期');
}
// 6. 解析消息内容
const resMsg = (await this.obContext.apis.MsgApi.parseMessage(singleMsg, 'array', true));
const firstMsg = resMsg?.message?.[0];
if (firstMsg && isForward(firstMsg)) {
const forwardContent = firstMsg.data.content;
if (forwardContent) {
return { messages: forwardContent };
}
}
}
}
// 说明消息已过期或者为内层消息 NapCat 一次返回不处理内层消息
throw new Error('消息已过期或者为内层消息,无法获取转发消息');
}
}
@@ -0,0 +1,63 @@
import type { MessageFormat } from '@/snowcat-onebot/config/config';
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { OB11Message } from '@/snowcat-onebot/index';
import { ActionName } from '@/snowcat-onebot/action/router';
import { ChatType } from 'snowcat-core/types';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { Static, Type } from '@sinclair/typebox';
import { NetworkAdapterConfig } from '@/snowcat-onebot/config/config';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
user_id: Type.String({ description: '用户QQ' }),
message_seq: Type.Optional(Type.String({ description: '起始消息序号' })),
count: Type.Number({ default: 20, description: '获取消息数量' }),
reverse_order: Type.Boolean({ default: false, description: '是否反向排序' }),
disable_get_url: Type.Boolean({ default: false, description: '是否禁用获取URL' }),
parse_mult_msg: Type.Boolean({ default: true, description: '是否解析合并消息' }),
quick_reply: Type.Boolean({ default: false, description: '是否快速回复' }),
reverseOrder: Type.Boolean({ default: false, description: '是否反向排序(旧版本兼容)' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
messages: Type.Array(Type.Any(), { description: '消息列表' }),
}, { description: '好友历史消息' });
type ReturnType = Static<typeof ReturnSchema>;
export default class GetFriendMsgHistory extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GetFriendMsgHistory;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取好友历史消息';
override actionDescription = '获取指定好友的历史聊天记录';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GetFriendMsgHistory.payload;
override returnExample = GoCQHTTPActionsExamples.GetFriendMsgHistory.response;
async _handle (payload: PayloadType, _adapter: string, config: NetworkAdapterConfig): Promise<ReturnType> {
// 处理参数
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error(`记录${payload.user_id}不存在`);
const friend = await this.core.apis.FriendApi.isBuddy(uid);
const peer = { chatType: friend ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, peerUid: uid };
const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0');
const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0';
const msgList = hasMessageSeq
? (await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count, payload.reverse_order || payload.reverseOrder)).msgList
: (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`);
// 转换序号
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
}));
// 烘焙消息
const ob11MsgList = (await Promise.all(
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, config['messagePostFormat'] as MessageFormat | undefined ?? 'array', payload.parse_mult_msg, payload.disable_get_url)))
).filter((msg): msg is OB11Message => msg !== undefined);
return { messages: ob11MsgList };
}
}
@@ -0,0 +1,39 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
can_at_all: Type.Boolean({ description: '是否可以艾特全体' }),
remain_at_all_count_for_group: Type.Number({ description: '群艾特全体剩余次数' }),
remain_at_all_count_for_uin: Type.Number({ description: '个人艾特全体剩余次数' }),
}, { description: '群艾特全体剩余次数' });
type ReturnType = Static<typeof ReturnSchema>;
export class GoCQHTTPGetGroupAtAllRemain extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取群艾特全体剩余次数';
override actionDescription = '获取指定群聊中艾特全体成员的剩余次数';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GetGroupAtAllRemain.payload;
override returnExample = GoCQHTTPActionsExamples.GetGroupAtAllRemain.response;
async _handle (payload: PayloadType) {
const ret = await this.core.apis.GroupApi.getGroupRemainAtTimes(payload.group_id.toString());
const data = {
can_at_all: ret.atInfo.canAtAll,
remain_at_all_count_for_group: ret.atInfo.RemainAtAllCountForGroup,
remain_at_all_count_for_uin: ret.atInfo.RemainAtAllCountForUin,
};
return data;
}
}
@@ -0,0 +1,50 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
file_count: Type.Number({ description: '文件总数' }),
limit_count: Type.Number({ description: '文件上限' }),
used_space: Type.Number({ description: '已使用空间' }),
total_space: Type.Number({ description: '总空间' }),
}, { description: '群文件系统信息' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetGroupFileSystemInfo extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GoCQHTTP_GetGroupFileSystemInfo;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取群文件系统信息';
override actionDescription = '获取群聊文件系统的空间及状态信息';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GetGroupFileSystemInfo.payload;
override returnExample = GoCQHTTPActionsExamples.GetGroupFileSystemInfo.response;
async _handle (payload: PayloadType) {
const groupCode = payload.group_id.toString();
const [countResult, space] = await Promise.all([
this.core.apis.GroupApi.getGroupFileCount([groupCode]),
this.core.apis.GroupApi.getGroupFileSpace(groupCode),
]);
const groupFileCount = countResult.groupFileCounts[0];
if (groupFileCount === undefined) {
throw new Error('Group not found');
}
return {
file_count: groupFileCount,
limit_count: 10000,
// Real values from the kernel's groupSpaceResult; fall back to the old
// placeholders if the field is unexpectedly absent.
used_space: space?.usedSpace ?? 0,
total_space: space?.totalSpace ?? 10 * 1024 * 1024 * 1024,
};
}
}
@@ -0,0 +1,49 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { OB11Construct } from '@/snowcat-onebot/helper/data';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
folder_id: Type.Optional(Type.String({ description: '文件夹ID' })),
folder: Type.Optional(Type.String({ description: '文件夹ID' })),
file_count: Type.Union([Type.Number(), Type.String()], { default: 50, description: '文件数量' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
files: Type.Array(Type.Unknown(), { description: '文件列表' }),
folders: Type.Array(Type.Unknown(), { description: '文件夹列表' }),
}, { description: '群文件夹文件列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetGroupFilesByFolder extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GoCQHTTP_GetGroupFilesByFolder;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取群文件夹文件列表';
override actionDescription = '获取指定群文件夹下的文件及子文件夹列表';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GetGroupFilesByFolder.payload;
override returnExample = GoCQHTTPActionsExamples.GetGroupFilesByFolder.response;
async _handle (payload: PayloadType): Promise<ReturnType> {
const retRaw = await this.core.apis.MsgApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +payload.file_count,
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,
folderId: payload.folder ?? payload.folder_id ?? '',
});
const ret = Array.isArray(retRaw) ? retRaw : [];
return {
files: ret.filter(item => item.fileInfo)
.map(item => OB11Construct.file(item.peerId, item.fileInfo!)),
folders: [],
};
}
}
@@ -0,0 +1,42 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { WebHonorType } from 'snowcat-core/types';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
type: Type.Optional(Type.Enum(WebHonorType, { description: '荣誉类型' })),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
group_id: Type.Number({ description: '群号' }),
current_talkative: Type.Record(Type.String(), Type.Unknown(), { description: '当前龙王' }),
talkative_list: Type.Array(Type.Unknown(), { description: '龙王列表' }),
performer_list: Type.Array(Type.Unknown(), { description: '群聊之火列表' }),
legend_list: Type.Array(Type.Unknown(), { description: '群聊炽热列表' }),
emotion_list: Type.Array(Type.Unknown(), { description: '快乐源泉列表' }),
strong_newbie_list: Type.Array(Type.Unknown(), { description: '冒尖小春笋列表' }),
}, { description: '群荣誉信息' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetGroupHonorInfo extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GetGroupHonorInfo;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取群荣誉信息';
override actionDescription = '获取指定群聊的荣誉信息,如龙王等';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GetGroupHonorInfo.payload;
override returnExample = GoCQHTTPActionsExamples.GetGroupHonorInfo.response;
async _handle (payload: PayloadType): Promise<ReturnType> {
if (!payload.type) {
payload.type = WebHonorType.ALL;
}
return await this.core.apis.WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type);
}
}
@@ -0,0 +1,59 @@
import type { MessageFormat } from '@/snowcat-onebot/config/config';
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { OB11Message } from '@/snowcat-onebot/index';
import { ActionName } from '@/snowcat-onebot/action/router';
import { ChatType, Peer } from 'snowcat-core/types';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { Static, Type } from '@sinclair/typebox';
import { NetworkAdapterConfig } from '@/snowcat-onebot/config/config';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
message_seq: Type.Optional(Type.String({ description: '起始消息序号' })),
count: Type.Number({ default: 20, description: '获取消息数量' }),
reverse_order: Type.Boolean({ default: false, description: '是否反向排序' }),
disable_get_url: Type.Boolean({ default: false, description: '是否禁用获取URL' }),
parse_mult_msg: Type.Boolean({ default: true, description: '是否解析合并消息' }),
quick_reply: Type.Boolean({ default: false, description: '是否快速回复' }),
reverseOrder: Type.Boolean({ default: false, description: '是否反向排序(旧版本兼容)' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
messages: Type.Array(Type.Any(), { description: '消息列表' }),
}, { description: '群历史消息' });
type ReturnType = Static<typeof ReturnSchema>;
export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GoCQHTTP_GetGroupMsgHistory;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取群历史消息';
override actionDescription = '获取指定群聊的历史聊天记录';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GetGroupMsgHistory.payload;
override returnExample = GoCQHTTPActionsExamples.GetGroupMsgHistory.response;
async _handle (payload: PayloadType, _adapter: string, config: NetworkAdapterConfig): Promise<ReturnType> {
const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() };
const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0');
// 拉取消息
const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0';
const msgList = hasMessageSeq
? (await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count, payload.reverse_order || payload.reverseOrder)).msgList
: (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`);
// 转换序号
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
}));
// 烘焙消息
const ob11MsgList = (await Promise.all(
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, config['messagePostFormat'] as MessageFormat | undefined ?? 'array', payload.parse_mult_msg, payload.disable_get_url, payload.quick_reply)))
).filter((msg): msg is OB11Message => msg !== undefined);
return { messages: ob11MsgList };
}
}
@@ -0,0 +1,47 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { OB11Construct } from '@/snowcat-onebot/helper/data';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
file_count: Type.Union([Type.Number(), Type.String()], { default: 50, description: '文件数量' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
files: Type.Array(Type.Any(), { description: '文件列表' }),
folders: Type.Array(Type.Any(), { description: '文件夹列表' }),
}, { description: '群根目录文件列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetGroupRootFiles extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GoCQHTTP_GetGroupRootFiles;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取群根目录文件列表';
override actionDescription = '获取群文件根目录下的所有文件和文件夹';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GetGroupRootFiles.payload;
override returnExample = GoCQHTTPActionsExamples.GetGroupRootFiles.response;
async _handle (payload: PayloadType) {
const ret = await this.core.apis.MsgApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +payload.file_count,
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,
});
return {
files: ret.filter(item => item.fileInfo)
.map(item => OB11Construct.file(item.peerId, item.fileInfo!)),
folders: ret.filter(item => item.folderInfo)
.map(item => OB11Construct.folder(item.peerId, item.folderInfo!)),
};
}
}
@@ -0,0 +1,32 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { sleep } from 'snowcat-common/src/helper';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({}, { description: '在线客户端负载' });
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Array(Type.Any(), { description: '在线客户端列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetOnlineClient extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GetOnlineClient;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取在线客户端';
override actionDescription = '获取当前登录账号的在线客户端列表';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GetOnlineClient.payload;
override returnExample = GoCQHTTPActionsExamples.GetOnlineClient.response;
async _handle () {
// 注册监听
this.core.apis.SystemApi.getOnlineDev();
await sleep(500);
return [];
}
}
@@ -0,0 +1,76 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { OB11UserSex } from '@/snowcat-onebot/index';
import { OB11Construct } from '@/snowcat-onebot/helper/data';
import { ActionName } from '@/snowcat-onebot/action/router';
import { calcQQLevel } from 'snowcat-common/src/helper';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
const PayloadSchema = Type.Object({
user_id: Type.String({ description: '用户QQ' }),
no_cache: Type.Union([Type.Boolean(), Type.String()], { default: false, description: '是否不使用缓存' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
user_id: Type.Number({ description: '用户QQ' }),
uid: Type.String({ description: 'UID' }),
nickname: Type.String({ description: '昵称' }),
age: Type.Number({ description: '年龄' }),
qid: Type.String({ description: 'QID' }),
qqLevel: Type.Number({ description: 'QQ等级' }),
sex: Type.String({ description: '性别' }),
long_nick: Type.String({ description: '个性签名' }),
reg_time: Type.Number({ description: '注册时间' }),
is_vip: Type.Boolean({ description: '是否VIP' }),
is_years_vip: Type.Boolean({ description: '是否年费VIP' }),
vip_level: Type.Number({ description: 'VIP等级' }),
remark: Type.String({ description: '备注' }),
status: Type.Number({ description: '状态' }),
login_days: Type.Number({ description: '登录天数' }),
}, { description: '陌生人信息' });
type ReturnType = Static<typeof ReturnSchema>;
export default class GoCQHTTPGetStrangerInfo extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GoCQHTTP_GetStrangerInfo;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取陌生人信息';
override actionDescription = '获取指定非好友用户的信息';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GetStrangerInfo.payload;
override returnExample = GoCQHTTPActionsExamples.GetStrangerInfo.response;
async _handle (payload: PayloadType): Promise<ReturnType> {
const user_id = payload.user_id.toString();
const isNocache = typeof payload.no_cache === 'string' ? payload.no_cache === 'true' : !!payload.no_cache;
const extendData = await this.core.apis.UserApi.getUserDetailInfoByUin(user_id);
let uid = (await this.core.apis.UserApi.getUidByUinV2(user_id));
if (!uid) uid = extendData.detail.uid;
const info = (await this.core.apis.UserApi.getUserDetailInfo(uid, isNocache));
return {
...extendData.detail.simpleInfo.coreInfo,
...extendData.detail.commonExt ?? {},
...extendData.detail.simpleInfo.baseInfo,
...extendData.detail.simpleInfo.relationFlags ?? {},
...extendData.detail.simpleInfo.status ?? {},
user_id: parseInt(extendData.detail.uin) ?? 0,
uid: info.uid ?? uid,
nickname: extendData.detail.simpleInfo.coreInfo.nick ?? '',
age: extendData.detail.simpleInfo.baseInfo.age ?? info.age,
qid: extendData.detail.simpleInfo.baseInfo.qid,
qqLevel: calcQQLevel(extendData.detail.commonExt?.qqLevel ?? info.qqLevel),
sex: OB11Construct.sex(extendData.detail.simpleInfo.baseInfo.sex) ?? OB11UserSex.unknown,
long_nick: extendData.detail.simpleInfo.baseInfo.longNick ?? info.longNick,
reg_time: extendData.detail.commonExt?.regTime ?? info.regTime,
is_vip: extendData.detail.simpleInfo.vasInfo?.svipFlag ?? false,
is_years_vip: extendData.detail.simpleInfo.vasInfo?.yearVipFlag ?? false,
vip_level: extendData.detail.simpleInfo.vasInfo?.vipLevel ?? 0,
remark: extendData.detail.simpleInfo.coreInfo.remark ?? info.remark ?? '',
status: extendData.detail.simpleInfo.status?.status ?? info.status ?? 0,
login_days: 0, // 失效
};
}
}
@@ -0,0 +1,31 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
export const GoCQHTTPCheckUrlSafelyPayloadSchema = Type.Object({
url: Type.String({ description: '要检查的 URL' }),
});
export type GoCQHTTPCheckUrlSafelyPayload = Static<typeof GoCQHTTPCheckUrlSafelyPayloadSchema>;
export const GoCQHTTPCheckUrlSafelyReturnSchema = Type.Object({
level: Type.Number({ description: '安全等级 (1: 安全, 2: 未知, 3: 危险)' }),
});
export type GoCQHTTPCheckUrlSafelyReturn = Static<typeof GoCQHTTPCheckUrlSafelyReturnSchema>;
export class GoCQHTTPCheckUrlSafely extends OneBotAction<GoCQHTTPCheckUrlSafelyPayload, GoCQHTTPCheckUrlSafelyReturn> {
override actionName = ActionName.GoCQHTTP_CheckUrlSafely;
override payloadSchema = GoCQHTTPCheckUrlSafelyPayloadSchema;
override returnSchema = GoCQHTTPCheckUrlSafelyReturnSchema;
override actionSummary = '检查URL安全性';
override actionDescription = '检查指定URL的安全等级';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GoCQHTTPCheckUrlSafely.payload;
override returnExample = GoCQHTTPActionsExamples.GoCQHTTPCheckUrlSafely.response;
async _handle () {
return { level: 1 };
}
}
@@ -0,0 +1,44 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
export const GoCQHTTPDeleteFriendPayloadSchema = Type.Object({
friend_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '好友 QQ 号' })),
user_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '用户 QQ 号' })),
temp_block: Type.Optional(Type.Boolean({ description: '是否加入黑名单' })),
temp_both_del: Type.Optional(Type.Boolean({ description: '是否双向删除' })),
});
export type GoCQHTTPDeleteFriendPayload = Static<typeof GoCQHTTPDeleteFriendPayloadSchema>;
export class GoCQHTTPDeleteFriend extends OneBotAction<GoCQHTTPDeleteFriendPayload, any> {
override actionName = ActionName.GoCQHTTP_DeleteFriend;
override payloadSchema = GoCQHTTPDeleteFriendPayloadSchema;
override returnSchema = Type.Any();
override actionSummary = '删除好友';
override actionDescription = '从好友列表中删除指定用户';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GoCQHTTPDeleteFriend.payload;
override returnExample = GoCQHTTPActionsExamples.GoCQHTTPDeleteFriend.response;
async _handle (payload: GoCQHTTPDeleteFriendPayload) {
const uin = payload.friend_id ?? payload.user_id ?? '';
const uid = await this.core.apis.UserApi.getUidByUinV2(uin.toString());
if (!uid) {
return {
valid: false,
message: '好友不存在',
};
}
const isBuddy = await this.core.apis.FriendApi.isBuddy(uid);
if (!isBuddy) {
return {
valid: false,
message: '不是好友',
};
}
return await this.core.apis.FriendApi.delBuddy(uid, payload.temp_block, payload.temp_both_del);
}
}
@@ -0,0 +1,42 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
export const GoCQHTTPGetModelShowPayloadSchema = Type.Object({
model: Type.Optional(Type.String({ description: '模型名称' })),
});
export type GoCQHTTPGetModelShowPayload = Static<typeof GoCQHTTPGetModelShowPayloadSchema>;
export const GoCQHTTPGetModelShowReturnSchema = Type.Array(Type.Object({
variants: Type.Object({
model_show: Type.String({ description: '显示名称' }),
need_pay: Type.Boolean({ description: '是否需要付费' }),
}),
}), { description: '机型显示列表' });
export type GoCQHTTPGetModelShowReturn = Static<typeof GoCQHTTPGetModelShowReturnSchema>;
export class GoCQHTTPGetModelShow extends OneBotAction<GoCQHTTPGetModelShowPayload, GoCQHTTPGetModelShowReturn> {
override actionName = ActionName.GoCQHTTP_GetModelShow;
override payloadSchema = GoCQHTTPGetModelShowPayloadSchema;
override returnSchema = GoCQHTTPGetModelShowReturnSchema;
override actionSummary = '获取机型显示';
override actionDescription = '获取当前账号可用的设备机型显示名称列表';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GoCQHTTPGetModelShow.payload;
override returnExample = GoCQHTTPActionsExamples.GoCQHTTPGetModelShow.response;
async _handle (payload: GoCQHTTPGetModelShowPayload) {
if (!payload.model) {
payload.model = 'napcat';
}
return [{
variants: {
model_show: 'napcat',
need_pay: false,
},
}];
}
}
@@ -0,0 +1,20 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
// 兼容性代码
export class GoCQHTTPSetModelShow extends OneBotAction<void, void> {
override actionName = ActionName.GoCQHTTP_SetModelShow;
override payloadSchema = Type.Object({});
override returnSchema = Type.Null();
override actionSummary = '设置机型';
override actionDescription = '设置当前账号的设备机型名称';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GoCQHTTPSetModelShow.payload;
override returnExample = GoCQHTTPActionsExamples.GoCQHTTPSetModelShow.response;
async _handle () {
}
}
@@ -0,0 +1,78 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { QuickAction, QuickActionEvent } from '@/snowcat-onebot/types';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
import { OB11MessageMixTypeSchema } from '@/snowcat-onebot/types/message';
const SenderSchema = Type.Object({
user_id: Type.String({ description: '用户ID' }),
nickname: Type.String({ description: '昵称' }),
sex: Type.Optional(Type.String({ description: '性别' })),
age: Type.Optional(Type.Number({ description: '年龄' })),
card: Type.Optional(Type.String({ description: '群名片' })),
level: Type.Optional(Type.String({ description: '群等级' })),
role: Type.Optional(Type.String({ description: '群角色' })),
});
// 定义 QuickAction 的详细 Schema
const QuickActionSchema = Type.Object({
reply: Type.Optional(OB11MessageMixTypeSchema),
auto_escape: Type.Optional(Type.Boolean({ description: '是否作为纯文本发送' })),
at_sender: Type.Optional(Type.Boolean({ description: '是否 @ 发送者' })),
delete: Type.Optional(Type.Boolean({ description: '是否撤回该消息' })),
kick: Type.Optional(Type.Boolean({ description: '是否踢出发送者' })),
ban: Type.Optional(Type.Boolean({ description: '是否禁言发送者' })),
ban_duration: Type.Optional(Type.Number({ description: '禁言时长' })),
approve: Type.Optional(Type.Boolean({ description: '是否同意请求/加群' })),
remark: Type.Optional(Type.String({ description: '好友备注' })),
reason: Type.Optional(Type.String({ description: '拒绝理由' })),
}, { description: '快速操作内容' });
// 定义 QuickActionEvent 的详细 Schema
const QuickActionEventSchema = Type.Object({
time: Type.Number({ description: '事件发生时间' }),
self_id: Type.Number({ description: '收到事件的机器人 QQ 号' }),
post_type: Type.String({ description: '上报类型' }),
message_type: Type.Optional(Type.String({ description: '消息类型' })),
sub_type: Type.Optional(Type.String({ description: '消息子类型' })),
user_id: Type.String({ description: '发送者 QQ 号' }),
group_id: Type.Optional(Type.String({ description: '群号' })),
message_id: Type.Optional(Type.Number({ description: '消息 ID' })),
message_seq: Type.Optional(Type.Number({ description: '消息序列号' })),
real_id: Type.Optional(Type.Number({ description: '真实消息 ID' })),
sender: Type.Optional(SenderSchema),
message: Type.Optional(Type.Any({ description: '消息内容' })),
message_format: Type.Optional(Type.String({ description: '消息格式' })),
raw_message: Type.Optional(Type.String({ description: '原始消息内容' })),
font: Type.Optional(Type.Number({ description: '字体' })),
notice_type: Type.Optional(Type.String({ description: '通知类型' })),
meta_event_type: Type.Optional(Type.String({ description: '元事件类型' })),
}, { description: '事件上下文' });
export const GoCQHTTPHandleQuickActionPayloadSchema = Type.Object({
context: QuickActionEventSchema,
operation: QuickActionSchema,
});
export type GoCQHTTPHandleQuickActionPayload = {
context: QuickActionEvent;
operation: QuickAction;
} & Static<typeof GoCQHTTPHandleQuickActionPayloadSchema>;
export class GoCQHTTPHandleQuickAction extends OneBotAction<GoCQHTTPHandleQuickActionPayload, void> {
override actionName = ActionName.GoCQHTTP_HandleQuickAction;
override payloadSchema = GoCQHTTPHandleQuickActionPayloadSchema;
override returnSchema = Type.Null();
override actionSummary = '处理快速操作';
override actionDescription = '处理来自事件上报的快速操作请求';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.QuickAction.payload;
override returnExample = GoCQHTTPActionsExamples.QuickAction.response;
async _handle (payload: GoCQHTTPHandleQuickActionPayload): Promise<void> {
this.obContext.apis.QuickActionApi
.handleQuickOperation(payload.context, payload.operation)
.catch(e => this.core.context.logger.logError(e));
}
}
@@ -0,0 +1,67 @@
import { OB11MessageMixType } from '@/snowcat-onebot/types';
import { ContextMode, normalize, ReturnDataType, SendMsgBase, SendMsgPayload } from '@/snowcat-onebot/action/msg/SendMsg';
import { ActionName } from '@/snowcat-onebot/action/router';
// 未验证
type GoCQHTTPSendForwardMsgPayload = SendMsgPayload & { messages?: OB11MessageMixType; };
export class GoCQHTTPSendForwardMsgBase extends SendMsgBase {
protected override async check (payload: GoCQHTTPSendForwardMsgPayload) {
if (payload.messages) payload.message = normalize(payload.messages);
return super.check(payload);
}
}
export class GoCQHTTPSendForwardMsg extends GoCQHTTPSendForwardMsgBase {
override actionName = ActionName.GoCQHTTP_SendForwardMsg;
override actionSummary = '发送合并转发消息';
override actionDescription = '发送合并转发消息';
override actionTags = ['Go-CQHTTP'];
override payloadExample = {
group_id: '123456789',
messages: [],
};
override returnExample = {
message_id: 123456,
};
protected override async check (payload: GoCQHTTPSendForwardMsgPayload) {
if (payload.messages) payload.message = normalize(payload.messages);
return super.check(payload);
}
}
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsgBase {
override actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
override actionSummary = '发送私聊合并转发消息';
override actionTags = ['Go-CQHTTP'];
override payloadExample = {
user_id: '123456789',
messages: [],
};
override returnExample = {
message_id: 123456,
};
override async _handle (payload: GoCQHTTPSendForwardMsgPayload): Promise<ReturnDataType> {
return this.base_handle(payload, ContextMode.Private);
}
}
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsgBase {
override actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
override actionSummary = '发送群合并转发消息';
override actionTags = ['Go-CQHTTP'];
override payloadExample = {
group_id: '123456789',
messages: [],
};
override returnExample = {
message_id: 123456,
};
override async _handle (payload: GoCQHTTPSendForwardMsgPayload): Promise<ReturnDataType> {
return this.base_handle(payload, ContextMode.Group);
}
}
@@ -0,0 +1,72 @@
import { checkFileExist, uriToLocalFile } from 'snowcat-common/src/file';
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { unlink } from 'node:fs/promises';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
export const SendGroupNoticePayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
content: Type.String({ description: '公告内容' }),
image: Type.Optional(Type.String({ description: '公告图片路径或 URL' })),
pinned: Type.Union([Type.Number(), Type.String()], { default: 0, description: '是否置顶 (0/1)' }),
type: Type.Union([Type.Number(), Type.String()], { default: 1, description: '类型 (默认为 1)' }),
confirm_required: Type.Union([Type.Number(), Type.String()], { default: 1, description: '是否需要确认 (0/1)' }),
is_show_edit_card: Type.Union([Type.Number(), Type.String()], { default: 0, description: '是否显示修改群名片引导 (0/1)' }),
tip_window_type: Type.Union([Type.Number(), Type.String()], { default: 0, description: '弹窗类型 (默认为 0)' }),
});
export type SendGroupNoticePayload = Static<typeof SendGroupNoticePayloadSchema>;
export class SendGroupNotice extends OneBotAction<SendGroupNoticePayload, void> {
override actionName = ActionName.GoCQHTTP_SendGroupNotice;
override payloadSchema = SendGroupNoticePayloadSchema;
override returnSchema = Type.Null();
override actionSummary = '发送群公告';
override actionDescription = '在指定群聊中发布新的公告';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.SendGroupNotice.payload;
override returnExample = GoCQHTTPActionsExamples.SendGroupNotice.response;
async _handle (payload: SendGroupNoticePayload) {
let UploadImage: { id: string, width: number, height: number; } | undefined;
if (payload.image) {
// 公告图逻辑
const {
path,
success,
isLocal,
} = (await uriToLocalFile(this.core.SnowCatTempPath, payload.image));
if (!success) {
throw new Error(`群公告${payload.image}设置失败,image字段可能格式不正确`);
}
if (!path) {
throw new Error(`群公告${payload.image}设置失败,获取资源失败`);
}
await checkFileExist(path, 5000);
const ImageUploadResult = await this.core.apis.GroupApi.uploadGroupBulletinPic(payload.group_id.toString(), path);
if (ImageUploadResult.errCode !== 0) {
throw new Error(`群公告${payload.image}设置失败,图片上传失败 , 错误信息:${ImageUploadResult.errMsg}`);
}
if (!isLocal) unlink(path).catch(() => { });
UploadImage = ImageUploadResult.picInfo;
}
const publishGroupBulletinResult = await this.core.apis.WebApi.setGroupNotice(
payload.group_id.toString(),
payload.content,
+payload.pinned,
+payload.type,
+payload.is_show_edit_card,
+payload.tip_window_type,
+payload.confirm_required,
UploadImage?.id,
UploadImage?.width,
UploadImage?.height
);
if (!publishGroupBulletinResult || publishGroupBulletinResult.ec !== 0) {
throw new Error(`设置群公告失败,错误信息:${publishGroupBulletinResult?.em}`);
}
}
}
@@ -0,0 +1,57 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { checkFileExistV2, uriToLocalFile } from 'snowcat-common/src/file';
import { Static, Type } from '@sinclair/typebox';
import fs from 'node:fs/promises';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
export const SetGroupPortraitPayloadSchema = Type.Object({
file: Type.String({ description: '头像文件路径或 URL' }),
group_id: Type.String({ description: '群号' }),
});
export type SetGroupPortraitPayload = Static<typeof SetGroupPortraitPayloadSchema>;
const ReturnSchema = Type.Object({
result: Type.Number(),
errMsg: Type.String(),
}, { description: '设置结果' });
export type ReturnType = Static<typeof ReturnSchema>;
export default class SetGroupPortrait extends OneBotAction<SetGroupPortraitPayload, ReturnType> {
override actionName = ActionName.SetGroupPortrait;
override payloadSchema = SetGroupPortraitPayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '设置群头像';
override actionDescription = '修改指定群聊的头像';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.SetGroupPortrait.payload;
override returnExample = GoCQHTTPActionsExamples.SetGroupPortrait.response;
async _handle (payload: SetGroupPortraitPayload): Promise<ReturnType> {
const { path, success, isLocal } = (await uriToLocalFile(this.core.SnowCatTempPath, payload.file));
if (!success) {
throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`);
}
if (path) {
await checkFileExistV2(path, 5000); // 文件不存在QQ会崩溃,需要提前判断
const ret = await this.core.apis.GroupApi.setGroupAvatar(payload.group_id.toString(), path);
if (!isLocal) fs.unlink(path).catch(() => { });
if (!ret) {
throw new Error(`头像${payload.file}设置失败,api无返回`);
}
if (Number(ret.result) === 1004022) {
throw new Error(`头像${payload.file}设置失败,文件可能不是图片格式或权限不足`);
} else if (Number(ret.result) !== 0) {
throw new Error(`头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`);
}
return {
result: Number(ret.result),
errMsg: ret.errMsg,
};
} else {
throw new Error(`头像${payload.file}设置失败,无法获取头像,文件可能不存在`);
}
}
}
@@ -0,0 +1,38 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
export const SetQQProfilePayloadSchema = Type.Object({
nickname: Type.String({ description: '昵称' }),
personal_note: Type.Optional(Type.String({ description: '个性签名' })),
sex: Type.Optional(Type.Union([Type.Number(), Type.String()], { description: '性别 (0: 未知, 1: 男, 2: 女)' })), // 传Sex值?建议传0
});
export type SetQQProfilePayload = Static<typeof SetQQProfilePayloadSchema>;
export class SetQQProfile extends OneBotAction<SetQQProfilePayload, any> {
override actionName = ActionName.SetQQProfile;
override payloadSchema = SetQQProfilePayloadSchema;
override returnSchema = Type.Any({ description: '设置结果' });
override actionSummary = '设置QQ资料';
override actionDescription = '修改当前账号的昵称、个性签名等资料';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.SetQQProfile.payload;
override returnExample = GoCQHTTPActionsExamples.SetQQProfile.response;
async _handle (payload: SetQQProfilePayload) {
const self = this.core.selfInfo;
const OldProfile = await this.core.apis.UserApi.getUserDetailInfo(self.uid);
return await this.core.apis.UserApi.modifySelfProfile({
nick: payload.nickname,
longNick: (payload?.personal_note ?? OldProfile?.longNick) || '',
sex: parseInt(payload?.sex ? payload?.sex.toString() : OldProfile?.sex!.toString()),
birthday: {
birthday_year: OldProfile?.birthday_year!.toString(),
birthday_month: OldProfile?.birthday_month!.toString(),
birthday_day: OldProfile?.birthday_day!.toString(),
},
location: undefined,
});
}
}
@@ -0,0 +1,65 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { ChatType, Peer, ElementType } from 'snowcat-core/types';
import fs from 'fs';
import { uriToLocalFile } from 'snowcat-common/src/file';
import { SendMessageContext } from '@/snowcat-onebot/api';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
export const GoCQHTTPUploadGroupFilePayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
file: Type.String({ description: '资源路径或URL' }),
name: Type.String({ description: '文件名' }),
folder: Type.Optional(Type.String({ description: '父目录 ID' })),
folder_id: Type.Optional(Type.String({ description: '父目录 ID (兼容性字段)' })), // 临时扩展
upload_file: Type.Boolean({ default: true, description: '是否执行上传' }),
});
export type GoCQHTTPUploadGroupFilePayload = Static<typeof GoCQHTTPUploadGroupFilePayloadSchema>;
export const GoCQHTTPUploadGroupFileReturnSchema = Type.Object({
file_id: Type.Union([Type.String(), Type.Null()], { description: '文件 ID' }),
});
export type GoCQHTTPUploadGroupFileResponse = Static<typeof GoCQHTTPUploadGroupFileReturnSchema>;
export default class GoCQHTTPUploadGroupFile extends OneBotAction<GoCQHTTPUploadGroupFilePayload, GoCQHTTPUploadGroupFileResponse> {
override actionName = ActionName.GoCQHTTP_UploadGroupFile;
override payloadSchema = GoCQHTTPUploadGroupFilePayloadSchema;
override returnSchema = GoCQHTTPUploadGroupFileReturnSchema;
override actionSummary = '上传群文件';
override actionDescription = '上传资源路径或URL指定的文件到指定群聊的文件系统中';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.UploadGroupFile.payload;
override returnExample = GoCQHTTPActionsExamples.UploadGroupFile.response;
async _handle (payload: GoCQHTTPUploadGroupFilePayload): Promise<GoCQHTTPUploadGroupFileResponse> {
let file = payload.file;
if (fs.existsSync(file)) {
file = `file://${file}`;
}
const downloadResult = await uriToLocalFile(this.core.SnowCatTempPath, file);
const peer: Peer = {
chatType: ChatType.KCHATTYPEGROUP,
peerUid: payload.group_id.toString(),
};
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg);
}
const msgContext: SendMessageContext = {
peer,
deleteAfterSentFiles: [],
};
const sendFileEle = await this.obContext.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name, payload.folder ?? payload.folder_id, payload.upload_file, downloadResult.isLocal);
if (!downloadResult.isLocal) {
msgContext.deleteAfterSentFiles.push(downloadResult.path);
}
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [sendFileEle], msgContext.deleteAfterSentFiles);
const fileElement = returnMsg.elements.find(ele => ele.elementType === ElementType.FILE);
return {
file_id: fileElement?.fileElement?.fileUuid || null,
};
}
}
@@ -0,0 +1,75 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { ChatType, Peer, SendFileElement, ElementType } from 'snowcat-core/types';
import fs from 'fs';
import { uriToLocalFile } from 'snowcat-common/src/file';
import { SendMessageContext } from '@/snowcat-onebot/api';
import { ContextMode, createContext } from '@/snowcat-onebot/action/msg/SendMsg';
import { Static, Type } from '@sinclair/typebox';
import { GoCQHTTPActionsExamples } from '../example/GoCQHTTPActionsExamples';
export const GoCQHTTPUploadPrivateFilePayloadSchema = Type.Object({
user_id: Type.String({ description: '用户 QQ' }),
file: Type.String({ description: '资源路径或URL' }),
name: Type.String({ description: '文件名' }),
upload_file: Type.Boolean({ default: true, description: '是否执行上传' }),
});
export type GoCQHTTPUploadPrivateFilePayload = Static<typeof GoCQHTTPUploadPrivateFilePayloadSchema>;
export const GoCQHTTPUploadPrivateFileReturnSchema = Type.Object({
file_id: Type.Union([Type.String(), Type.Null()], { description: '文件 ID' }),
});
export type GoCQHTTPUploadPrivateFileResponse = Static<typeof GoCQHTTPUploadPrivateFileReturnSchema>;
export default class GoCQHTTPUploadPrivateFile extends OneBotAction<GoCQHTTPUploadPrivateFilePayload, GoCQHTTPUploadPrivateFileResponse> {
override actionName = ActionName.GOCQHTTP_UploadPrivateFile;
override payloadSchema = GoCQHTTPUploadPrivateFilePayloadSchema;
override returnSchema = GoCQHTTPUploadPrivateFileReturnSchema;
override actionSummary = '上传私聊文件';
override actionDescription = '上传本地文件到指定私聊会话中';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.UploadPrivateFile.payload;
override returnExample = GoCQHTTPActionsExamples.UploadPrivateFile.response;
async getPeer (payload: GoCQHTTPUploadPrivateFilePayload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!peerUid) {
throw new Error(`私聊${payload.user_id}不存在`);
}
const isBuddy = await this.core.apis.FriendApi.isBuddy(peerUid);
return { chatType: isBuddy ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, peerUid };
}
throw new Error('缺少参数 user_id');
}
async _handle (payload: GoCQHTTPUploadPrivateFilePayload): Promise<GoCQHTTPUploadPrivateFileResponse> {
let file = payload.file;
if (fs.existsSync(file)) {
file = `file://${file}`;
}
const downloadResult = await uriToLocalFile(this.core.SnowCatTempPath, file);
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg);
}
const msgContext: SendMessageContext = {
peer: await createContext(this.core, {
user_id: payload.user_id.toString(),
}, ContextMode.Private),
deleteAfterSentFiles: [],
};
const sendFileEle: SendFileElement = await this.obContext.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name, '', payload.upload_file, downloadResult.isLocal);
if (!downloadResult.isLocal) {
msgContext.deleteAfterSentFiles.push(downloadResult.path);
}
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(await this.getPeer(payload), [sendFileEle], msgContext.deleteAfterSentFiles);
const fileElement = returnMsg.elements.find(ele => ele.elementType === ElementType.FILE);
return {
file_id: fileElement?.fileElement?.fileUuid || null,
};
}
}