Fix SendPokePayloadSchema type definitions

Corrected the type definitions for user_id and target_id to only allow strings, and fixed a syntax error in group_id. This ensures payload validation is consistent and accurate.

Refactor fileset ID API response and schema handling

Updated GetFilesetId action to return a structured object with fileset_id and adjusted its return schema accordingly. Improved frontend TypeBox schema parsing to support allOf (intersection) merging and updated API debug component to construct response schemas in a more robust way for object recognition.

Refactor OneBot API schema handling to use TypeBox

Replaces Zod-based static API schema definitions with dynamic fetching of schemas from the backend using TypeBox. Removes legacy static schema files, updates frontend API debug components to use TypeBox utilities, and adds @sinclair/typebox as a dependency. Backend now exposes a /schemas endpoint for all OneBot actions. Various schema and description fields are updated for clarity and consistency.
This commit is contained in:
手瓜一十雪 2026-01-25 20:10:43 +08:00
parent b36efeb4d1
commit 819224b788
26 changed files with 268 additions and 2390 deletions

View File

@ -4,8 +4,8 @@ import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
no_code_finger_open: Type.Optional(Type.Number({ description: '是否开启无码指纹' })),
no_finger_open: Type.Optional(Type.Number({ description: '是否开启无指纹' })),
no_code_finger_open: Type.Optional(Type.Number({ description: '未知' })),
no_finger_open: Type.Optional(Type.Number({ description: '未知' })),
});
type PayloadType = Static<typeof PayloadSchema>;

View File

@ -8,10 +8,12 @@ export const GetFilesetIdPayloadSchema = Type.Object({
export type GetFilesetIdPayload = Static<typeof GetFilesetIdPayloadSchema>;
export class GetFilesetId extends OneBotAction<GetFilesetIdPayload, any> {
export class GetFilesetId extends OneBotAction<GetFilesetIdPayload, { fileset_id: string }> {
override actionName = ActionName.GetFilesetId;
override payloadSchema = GetFilesetIdPayloadSchema;
override returnSchema = Type.Any({ description: '文件集 ID' });
override returnSchema = Type.Object({
fileset_id: Type.String({ description: '文件集 ID' })
});
override actionSummary = '获取文件集 ID';
override actionTags = ['文件扩展'];
override payloadExample = {
@ -24,6 +26,7 @@ export class GetFilesetId extends OneBotAction<GetFilesetIdPayload, any> {
async _handle (payload: GetFilesetIdPayload) {
// 适配share_link 防止被传 Link无法解析
const code = payload.share_code.includes('=') ? payload.share_code.split('=').slice(1).join('=') : payload.share_code;
return await this.core.apis.FlashApi.fromShareLinkFindSetId(code);
const result = await this.core.apis.FlashApi.fromShareLinkFindSetId(code);
return { fileset_id: result.fileSetId };
}
}

View File

@ -14,7 +14,7 @@ export const GoCQHTTPGetModelShowReturnSchema = Type.Array(Type.Object({
model_show: Type.String({ description: '显示名称' }),
need_pay: Type.Boolean({ description: '是否需要付费' }),
}),
}), { description: '型显示列表' });
}), { description: '型显示列表' });
export type GoCQHTTPGetModelShowReturn = Static<typeof GoCQHTTPGetModelShowReturnSchema>;
@ -22,8 +22,8 @@ export class GoCQHTTPGetModelShow extends OneBotAction<GoCQHTTPGetModelShowPaylo
override actionName = ActionName.GoCQHTTP_GetModelShow;
override payloadSchema = GoCQHTTPGetModelShowPayloadSchema;
override returnSchema = GoCQHTTPGetModelShowReturnSchema;
override actionSummary = '获取型显示';
override actionDescription = '获取当前账号可用的设备型显示名称列表';
override actionSummary = '获取型显示';
override actionDescription = '获取当前账号可用的设备型显示名称列表';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GoCQHTTPGetModelShow.payload;
override returnExample = GoCQHTTPActionsExamples.GoCQHTTPGetModelShow.response;

View File

@ -8,8 +8,8 @@ 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 actionSummary = '设置机型';
override actionDescription = '设置当前账号的设备机型名称';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.GoCQHTTPSetModelShow.payload;
override returnExample = GoCQHTTPActionsExamples.GoCQHTTPSetModelShow.response;

View File

@ -9,7 +9,7 @@ import { GoCQHTTPActionsExamples } from './examples';
export const GoCQHTTPUploadGroupFilePayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
file: 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 (兼容性字段)' })), // 临时扩展
@ -29,7 +29,7 @@ export default class GoCQHTTPUploadGroupFile extends OneBotAction<GoCQHTTPUpload
override payloadSchema = GoCQHTTPUploadGroupFilePayloadSchema;
override returnSchema = GoCQHTTPUploadGroupFileReturnSchema;
override actionSummary = '上传群文件';
override actionDescription = '上传本地文件到指定群聊的文件系统中';
override actionDescription = '上传资源路径或URL指定的文件到指定群聊的文件系统中';
override actionTags = ['Go-CQHTTP'];
override payloadExample = GoCQHTTPActionsExamples.UploadGroupFile.payload;
override returnExample = GoCQHTTPActionsExamples.UploadGroupFile.response;

View File

@ -10,7 +10,7 @@ import { GoCQHTTPActionsExamples } from './examples';
export const GoCQHTTPUploadPrivateFilePayloadSchema = Type.Object({
user_id: Type.String({ description: '用户 QQ' }),
file: Type.String({ description: '本地文件路径' }),
file: Type.String({ description: '资源路径或URL' }),
name: Type.String({ description: '文件名' }),
upload_file: Type.Boolean({ default: true, description: '是否执行上传' }),
});

View File

@ -6,8 +6,8 @@ import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
user_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '用户QQ' })),
group_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '群号' })),
message_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '消息ID' })),
group_id: Type.Optional(Type.String({ description: '群号' })),
message_id: Type.Optional(Type.String({ description: '消息ID' })),
});
type PayloadType = Static<typeof PayloadSchema>;

View File

@ -195,7 +195,7 @@ export class SendMsgBase extends OneBotAction<SendMsgPayload, ReturnDataType> {
// const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic;
// if (music) {
// }
// }\r
// }
}
// log("send msg:", peer, sendElements)

View File

@ -5,9 +5,9 @@ import { Static, Type } from '@sinclair/typebox';
import { PacketActionsExamples } from './examples';
export const SendPokePayloadSchema = Type.Object({
group_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '群号' })),
user_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '用户QQ' })),
target_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '目标QQ' })),
group_id: Type.Optional(Type.String({ description: '群号' })),
user_id: Type.Optional(Type.String({ description: '用户QQ' })),
target_id: Type.Optional(Type.String({ description: '目标QQ' })),
});
export type SendPokePayload = Static<typeof SendPokePayloadSchema>;

View File

@ -1,6 +1,6 @@
import { getAllHandlers } from '@/napcat-onebot/action/index';
import { AutoRegisterRouter } from '@/napcat-onebot/action/auto-register';
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { writeFileSync, existsSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { TSchema } from '@sinclair/typebox';
import { fileURLToPath } from 'node:url';
@ -17,7 +17,7 @@ interface ActionSchemaInfo {
tags?: string[];
payloadExample?: unknown;
returnExample?: unknown;
errorExamples?: Array<{ code: number, description: string }>;
errorExamples?: Array<{ code: number, description: string; }>;
}
export const actionSchemas: Record<string, ActionSchemaInfo> = {};
@ -191,7 +191,7 @@ export function generateOpenAPI () {
generateMissingReport();
}
function generateMissingReport() {
function generateMissingReport () {
const missingReport: string[] = [];
for (const [actionName, schemas] of Object.entries(actionSchemas)) {
const missing: string[] = [];

View File

@ -12,6 +12,7 @@ import { ActionMap } from '@/napcat-onebot/action';
import { NapCatCore } from '@/napcat-core/index';
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/napcat-onebot/network/index';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import json5 from 'json5';
type ActionNameType = typeof ActionName[keyof typeof ActionName];
@ -19,6 +20,40 @@ type ActionNameType = typeof ActionName[keyof typeof ActionName];
const router: Router = Router();
const DEFAULT_ADAPTER_NAME = 'debug-primary';
/**
* Action Schema
*/
router.get('/schemas', async (_req: Request, res: Response) => {
try {
const obContext = WebUiDataRuntime.getOneBotContext();
if (!obContext) {
return sendError(res, 'OneBot 未初始化');
}
const schemas: Record<string, any> = {};
// 遍历 ActionName 中定义的所有路由
for (const key in ActionName) {
const actionName = (ActionName as any)[key];
if (actionName === ActionName.Unknown) continue;
const handler = obContext.actions.get(actionName);
if (handler) {
const action = handler as OneBotAction<unknown, unknown>;
schemas[actionName] = {
description: action.actionSummary || action.actionDescription,
payload: action.payloadSchema,
response: action.returnSchema,
payloadExample: action.payloadExample,
};
}
}
sendSuccess(res, schemas);
} catch (error: unknown) {
sendError(res, (error as Error).message);
}
});
/**
*
* OneBot NetworkManager WebSocket

View File

@ -55,6 +55,7 @@
"@monaco-editor/loader": "^1.4.0",
"@react-aria/visually-hidden": "^3.8.19",
"@reduxjs/toolkit": "^2.5.1",
"@sinclair/typebox": "^0.34.41",
"@uidotdev/usehooks": "^2.4.1",
"@uiw/react-codemirror": "^4.25.4",
"@xterm/addon-canvas": "^0.7.0",

View File

@ -21,7 +21,8 @@ import PageLoading from '@/components/page_loading';
import { request } from '@/utils/request';
import { generateDefaultJson, parse } from '@/utils/zod';
import { BaseResponseSchema, parseTypeBox, generateDefaultFromTypeBox } from '@/utils/typebox';
import { Type } from '@sinclair/typebox';
import DisplayStruct from './display_struct';
@ -58,8 +59,16 @@ const OneBotApiDebug = forwardRef<OneBotApiDebugRef, OneBotApiDebugProps>((props
const [responseHeight, setResponseHeight] = useState(240);
const [storedHeight, setStoredHeight] = useLocalStorage('napcat_debug_response_height', 240);
const parsedRequest = parse(data.request);
const parsedResponse = parse(data.response);
const parsedRequest = parseTypeBox(data?.payload);
// 将返回值的 data 结构包装进 BaseResponseSchema 进行展示
// 使用解构属性的方式重新构建对象,确保 parseTypeBox 能够识别为 object 类型
const wrappedResponseSchema = Type.Object({
...BaseResponseSchema.properties,
data: data?.response || Type.Any({ description: '数据' })
});
const parsedResponse = parseTypeBox(wrappedResponseSchema);
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
const hasBackground = !!backgroundImage;
@ -75,7 +84,7 @@ const OneBotApiDebug = forwardRef<OneBotApiDebugRef, OneBotApiDebugProps>((props
// 如果有 adapterName走后端转发
if (adapterName) {
request.post(`/api/Debug/call/${adapterName}`, {
action: path.replace(/^\//, ''), // 去掉开头的 /
action: path,
params: parsedRequestBody
}, {
headers: {
@ -154,7 +163,11 @@ const OneBotApiDebug = forwardRef<OneBotApiDebugRef, OneBotApiDebugProps>((props
}));
useEffect(() => {
setRequestBody(generateDefaultJson(data.request));
if (data?.payloadExample) {
setRequestBody(JSON.stringify(data.payloadExample, null, 2));
} else {
setRequestBody(JSON.stringify(generateDefaultFromTypeBox(data?.payload), null, 2));
}
setResponseContent('');
setResponseStatus(null);
}, [path]);

View File

@ -1,744 +0,0 @@
import { z } from 'zod';
import messageNodeSchema from './message/node';
import { baseResponseSchema, commonResponseDataSchema } from './response';
const oneBotHttpApiGroup = {
'/set_group_kick': {
description: '群踢人',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
reject_add_request: z.boolean().describe('拒绝此人的加群请求'),
}),
response: baseResponseSchema,
},
'/set_group_ban': {
description: '群禁言',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
duration: z.number(),
}),
response: baseResponseSchema,
},
'/get_group_system_msg': {
description: '获取群系统消息',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
InvitedRequest: z
.array(
z
.object({
request_id: z.string().describe('请求 ID'),
invitor_uin: z.string().describe('邀请人 QQ 号'),
invitor_nick: z.string().describe('邀请人昵称'),
group_id: z.string().describe('群号'),
message: z.string().describe('入群回答'),
group_name: z.string().describe('群名称'),
checked: z.boolean().describe('是否已处理'),
actor: z.string().describe('处理人 QQ 号'),
})
.describe('邀请入群请求')
)
.describe('邀请入群请求列表'),
join_requests: z.array(
z.object({
request_id: z.string().describe('请求 ID'),
requester_uin: z.string().describe('请求人 QQ 号'),
requester_nick: z.string().describe('请求人昵称'),
group_id: z.string().describe('群号'),
message: z.string().describe('入群回答'),
group_name: z.string().describe('群名称'),
checked: z.boolean().describe('是否已处理'),
actor: z.string().describe('处理人 QQ 号'),
})
),
}),
}),
},
'/get_essence_msg_list': {
description: '获取精华消息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema.extend({
data: z
.array(
z
.object({
msg_seq: z.number().describe('消息序号'),
msg_random: z.number().describe('消息随机数'),
sender_id: z.number().describe('发送人 QQ 号'),
sender_nick: z.string().describe('发送人昵称'),
operator_id: z.number().describe('操作人 QQ 号'),
operator_nick: z.string().describe('操作人昵称'),
message_id: z.string().describe('消息 ID'),
operator_time: z.string().describe('操作时间'),
content: z.array(messageNodeSchema),
})
.describe('精华消息')
)
.describe('精华消息列表'),
}),
},
'/set_group_whole_ban': {
description: '全员禁言',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
enable: z.boolean().describe('是否开启'),
}),
response: baseResponseSchema,
},
'/set_group_portrait': {
description: '设置群头像',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
file: z.string().describe('图片文件路径,服务器本地路径或远程 URL'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/set_group_admin': {
description: '设置群管理',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
enable: z.boolean().describe('是否设置为管理员'),
}),
response: baseResponseSchema,
},
'/set_essence_msg': {
description: '设置群精华消息',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息 ID'),
}),
response: baseResponseSchema.extend({
data: z.object({
errCode: z.number().describe('错误码'),
errMsg: z.string().describe('错误信息'),
result: z
.object({
wording: z.string().describe('?'),
digestUin: z.string().describe('?QQ号'),
digestTime: z.number().describe('设置时间?'),
msg: z
.object({
groupCode: z.string().describe('群号'),
msgSeq: z.number().describe('消息序号'),
msgRandom: z.number().describe('消息随机数'),
msgContent: z.array(messageNodeSchema).describe('消息内容'),
textSize: z.string().describe('文本大小'),
picSize: z.string().describe('图片大小'),
videoSize: z.string().describe('视频大小'),
senderUin: z.string().describe('发送人 QQ 号'),
senderTime: z.number().describe('发送时间'),
addDigestUin: z.string().describe('添加精华消息人 QQ 号'),
addDigestTime: z.number().describe('添加精华消息时间'),
startTime: z.number().describe('开始时间'),
latestMsgSeq: z.number().describe('最新消息序号'),
opType: z.number().describe('操作类型'),
})
.describe('消息内容'),
errorCode: z.number().describe('错误码'),
})
.describe('结果'),
}),
}),
},
'/set_group_card': {
description: '设置群成员名片',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
card: z.string().describe('名片'),
}),
response: baseResponseSchema,
},
'/delete_essence_msg': {
description: '删除群精华消息',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息 ID'),
}),
response: baseResponseSchema.extend({
data: z.object({
errCode: z.number().describe('错误码'),
errMsg: z.string().describe('错误信息'),
result: z
.object({
wording: z.string().describe('?'),
digestUin: z.string().describe('?QQ号'),
digestTime: z.number().describe('设置时间?'),
msg: z.object({
groupCode: z.string().describe('群号'),
msgSeq: z.number().describe('消息序号'),
msgRandom: z.number().describe('消息随机数'),
msgContent: z.array(messageNodeSchema).describe('消息内容'),
textSize: z.string().describe('文本大小'),
picSize: z.string().describe('图片大小'),
videoSize: z.string().describe('视频大小'),
senderUin: z.string().describe('发送人 QQ 号'),
senderTime: z.number().describe('发送时间'),
addDigestUin: z.string().describe('添加精华消息人 QQ 号'),
addDigestTime: z.number().describe('添加精华消息时间'),
startTime: z.number().describe('开始时间'),
latestMsgSeq: z.number().describe('最新消息序号'),
opType: z.number().describe('操作类型'),
}),
errorCode: z.number().describe('错误码'),
})
.describe('结果'),
}),
}),
},
'/set_group_name': {
description: '设置群名称',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
group_name: z.string().describe('群名称'),
}),
response: baseResponseSchema,
},
'/set_group_leave': {
description: '退出群聊',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema,
},
'/_send_group_notice': {
description: '发送群公告',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
content: z.string().describe('公告内容'),
image: z.string().optional().describe('图片地址'),
}),
response: baseResponseSchema,
},
'/_get_group_notice': {
description: '获取群公告',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
notice_id: z.string().describe('公告 ID'),
sender_id: z.number().describe('发送人 QQ 号'),
publish_time: z.number().describe('发布时间'),
message: z.object({
text: z.string().describe('文本内容'),
image: z
.array(
z
.object({
id: z.string().describe('图片 ID'),
height: z.string().describe('高度'),
width: z.string().describe('宽度'),
})
.describe('图片信息')
)
.describe('图片内容列表'),
}),
})
),
}),
},
'/set_group_special_title': {
description: '设置群成员专属头衔',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
special_title: z.string().describe('专属头衔内容'),
}),
response: baseResponseSchema,
},
'/upload_group_file': {
description: '上传群文件',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
file: z.string().describe('文件路径'),
name: z.string().describe('文件名'),
folder_id: z.string().describe('文件夹 ID'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/set_group_add_request': {
description: '处理加群请求',
request: z.object({
flag: z.string().describe('请求ID'),
approve: z.boolean().describe('是否同意'),
reason: z.string().optional().describe('拒绝理由'),
}),
response: baseResponseSchema,
},
'/get_group_info': {
description: '获取群信息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema.extend({
data: z.object({}),
}),
},
'/get_group_info_ex': {
description: '获取群信息扩展',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema.extend({
data: z
.object({
groupCode: z.string().describe('群号'),
resultCode: z.number().describe('结果码'),
extInfo: z
.object({
groupInfoExtSeq: z.number().describe('群信息序列号'),
reserve: z.number().describe('?'),
luckyWordId: z.string().describe('幸运字符ID'),
lightCharNum: z.number().describe('?'),
luckyWord: z.string().describe('幸运字符'),
starId: z.number().describe('?'),
essentialMsgSwitch: z.number().describe('精华消息开关'),
todoSeq: z.number().describe('?'),
blacklistExpireTime: z.number().describe('黑名单过期时间'),
isLimitGroupRtc: z.number().describe('是否限制群视频通话'),
companyId: z.number().describe('公司ID'),
hasGroupCustomPortrait: z.number().describe('是否有群自定义头像'),
bindGuildId: z.string().describe('绑定频道ID'),
groupOwnerId: z
.object({
memberUin: z.string().describe('群主QQ号'),
memberUid: z.string().describe('群主ID'),
memberQid: z.string().describe('群主QID'),
})
.describe('群主信息'),
essentialMsgPrivilege: z.number().describe('精华消息权限'),
msgEventSeq: z.string().describe('消息事件序列号'),
inviteRobotSwitch: z.number().describe('邀请机器人开关'),
gangUpId: z.string().describe('?'),
qqMusicMedalSwitch: z.number().describe('QQ音乐勋章开关'),
showPlayTogetherSwitch: z.number().describe('显示一起玩开关'),
groupFlagPro1: z.string()?.describe('群标识1'),
groupBindGuildIds: z
.object({
guildIds: z.array(z.string()),
})
.describe('绑定频道ID列表?'),
viewedMsgDisappearTime: z.string().describe('消息消失时间'),
groupExtFlameData: z.object({
switchState: z.number().describe('开关状态'),
state: z.number().describe('状态'),
dayNums: z.array(z.number()).describe('天数列表'),
version: z.number().describe('版本号'),
updateTime: z.string().describe('更新时间'),
isDisplayDayNum: z.boolean().describe('是否显示天数'),
}),
groupBindGuildSwitch: z.number().describe('绑定频道开关'),
groupAioBindGuildId: z.string().describe('AIO绑定频道ID'),
groupExcludeGuildIds: z
.object({
guildIds: z.array(z.string()).describe('排除频道ID'),
})
.describe('排除频道ID列表?'),
fullGroupExpansionSwitch: z.number().describe('全员群扩容开关'),
fullGroupExpansionSeq: z.string().describe('全员群扩容序列号'),
inviteRobotMemberSwitch: z
.number()
.describe('邀请机器人成员开关'),
inviteRobotMemberExamine: z
.number()
.describe('邀请机器人成员审核'),
groupSquareSwitch: z.number().describe('群广场开关'),
})
.describe('扩展信息'),
})
.describe('结果'),
}),
},
'/create_group_file_folder': {
description: '创建群文件夹',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
folder_name: z.string().describe('文件夹名称'),
}),
response: baseResponseSchema.extend({
data: z
.object({
result: z
.object({
retCode: z.number().describe('结果码'),
retMsg: z.string().describe('结果信息'),
clientWording: z.string().describe('客户端提示'),
})
.describe('结果'),
groupItem: z
.object({
peerId: z.string().describe('?'),
type: z.string().describe('类型'),
folderInfo: z
.object({
folderId: z.string().describe('文件夹 ID'),
parentFolderId: z.string().describe('父文件夹 ID'),
folderName: z.string().describe('文件夹名称'),
createTime: z.number().describe('创建时间'),
modifyTime: z.number().describe('修改时间'),
createUin: z.string().describe('创建人 QQ 号'),
creatorName: z.string().describe('创建人昵称'),
totalFileCount: z.string().describe('文件总数'),
modifyUin: z.string().describe('修改人 QQ 号'),
modifyName: z.string().describe('修改人昵称'),
usedSpace: z.string().describe('已使用空间'),
})
.describe('文件夹信息'),
})
.describe('群文件夹信息'),
})
.describe('数据'),
}),
},
'/delete_group_file': {
description: '删除群文件',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
file_id: z.string().describe('文件 ID'),
}),
response: baseResponseSchema.extend({
data: z
.object({
result: z.number().describe('结果码'),
errMsg: z.string().describe('错误信息'),
transGroupFileResult: z
.object({
result: z
.object({
retCode: z.number().describe('结果码'),
retMsg: z.string().describe('结果信息'),
clientWording: z.string().describe('客户端提示'),
})
.describe('结果'),
successFileIdList: z
.array(z.string())
.describe('成功文件 ID 列表'),
failFileIdList: z.array(z.string()).describe('失败文件 ID 列表'),
})
.describe('删除群文件结果'),
})
.describe('结果'),
}),
},
'/delete_group_folder': {
description: '删除群文件夹',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
folder_id: z.string().describe('文件夹 ID'),
}),
response: baseResponseSchema.extend({
data: z.object({
retCode: z.number().describe('结果码'),
retMsg: z.string().describe('结果信息'),
clientWording: z.string().describe('客户端提示'),
}),
}),
},
'/get_group_file_system_info': {
description: '获取群文件系统信息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema.extend({
data: z.object({
file_count: z.number().describe('文件总数'),
limit_count: z.number().describe('文件总数限制'),
used_space: z.number().describe('已使用空间'),
total_space: z.number().describe('总空间'),
}),
}),
},
'/get_group_root_files': {
description: '获取群根目录文件列表',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
files: z
.array(
z
.object({
group_id: z.number().describe('群号'),
file_id: z.string().describe('文件 ID'),
file_name: z.string().describe('文件名'),
busid: z.number().describe('?'),
size: z.number().describe('文件大小'),
upload_time: z.number().describe('上传时间'),
dead_time: z.number().describe('过期时间'),
modify_time: z.number().describe('修改时间'),
download_times: z.number().describe('下载次数'),
uploader: z.number().describe('上传人 QQ 号'),
uploader_name: z.string().describe('上传人昵称'),
})
.describe('文件信息')
)
.describe('文件列表'),
folders: z
.array(
z
.object({
group_id: z.number().describe('群号'),
folder_id: z.string().describe('文件夹 ID'),
folder: z.string().describe('文件夹?'),
folder_name: z.string().describe('文件夹名称'),
create_time: z.string().describe('创建时间'),
creator: z.string().describe('创建人 QQ 号'),
creator_name: z.string().describe('创建人昵称'),
total_file_count: z.string().describe('文件总数'),
})
.describe('文件夹信息')
)
.describe('文件夹列表'),
})
),
}),
},
'/get_group_files_by_folder': {
description: '获取群子目录文件列表',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
folder_id: z.string().describe('文件夹 ID'),
file_count: z.number().describe('文件数量'),
}),
response: baseResponseSchema.extend({
data: z.object({
files: z
.array(
z
.object({
group_id: z.number().describe('群号'),
file_id: z.string().describe('文件 ID'),
file_name: z.string().describe('文件名'),
busid: z.number().describe('?'),
size: z.number().describe('文件大小'),
upload_time: z.number().describe('上传时间'),
dead_time: z.number().describe('过期时间'),
modify_time: z.number().describe('修改时间'),
download_times: z.number().describe('下载次数'),
uploader: z.number().describe('上传人 QQ 号'),
uploader_name: z.string().describe('上传人昵称'),
})
.describe('文件信息')
)
.describe('文件列表'),
folders: z
.array(
z
.object({
group_id: z.number().describe('群号'),
folder_id: z.string().describe('文件夹 ID'),
folder: z.string().describe('文件夹?'),
folder_name: z.string().describe('文件夹名称'),
create_time: z.string().describe('创建时间'),
creator: z.string().describe('创建人 QQ 号'),
creator_name: z.string().describe('创建人昵称'),
total_file_count: z.string().describe('文件总数'),
})
.describe('文件夹信息')
)
.describe('文件夹列表'),
}),
}),
},
'/get_group_file_url': {
description: '获取群文件下载链接',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
file_id: z.string().describe('文件 ID'),
}),
response: baseResponseSchema.extend({
data: z.object({
url: z.string().describe('下载链接'),
}),
}),
},
'/get_group_list': {
description: '获取群列表',
request: z.object({
next_token: z.string().optional().describe('下一页标识'),
}),
response: baseResponseSchema.extend({
data: z.array(z.object({})),
}),
},
'/get_group_member_info': {
description: '获取群成员信息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
no_cache: z.boolean().describe('是否不使用缓存'),
}),
response: baseResponseSchema.extend({
data: z.object({}),
}),
},
'/get_group_member_list': {
description: '获取群成员列表',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
no_cache: z.boolean().describe('是否不使用缓存'),
}),
response: baseResponseSchema.extend({
data: z.array(z.object({})),
}),
},
'/get_group_honor_info': {
description: '获取群荣誉',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema.extend({
data: z
.object({
group_id: z.number().describe('群号'),
current_talkative: z
.object({
user_id: z.number().describe('QQ 号'),
avatar: z.string().describe('头像 URL'),
nickname: z.string().describe('昵称'),
day_count: z.number().describe('天数'),
description: z.string().describe('描述'),
})
.describe('当前龙王'),
talkative_list: z
.array(
z.object({
user_id: z.number().describe('QQ 号'),
avatar: z.string().describe('头像 URL'),
nickname: z.string().describe('昵称'),
day_count: z.number().describe('天数'),
description: z.string().describe('描述'),
})
)
.describe('龙王榜'),
performer_list: z
.array(
z.object({
user_id: z.number().describe('QQ 号'),
avatar: z.string().describe('头像 URL'),
nickname: z.string().describe('昵称'),
description: z.string().describe('描述'),
})
)
.describe('?'),
legend_list: z.array(z.string()).describe('?'),
emotion_list: z.array(z.string()).describe('?'),
strong_newbie_list: z.array(z.string()).describe('?'),
})
.describe('群荣誉信息'),
}),
},
'/get_group_at_all_remain': {
description: '获取群 @全体成员 剩余次数',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema.extend({
data: z.object({
can_at_all: z.boolean().describe('是否可以 @全体成员'),
remain_at_all_count_for_group: z.number().describe('剩余次数(group?)'),
remain_at_all_count_for_uin: z.number().describe('剩余次数(qq?)'),
}),
}),
},
'/get_group_ignored_notifies': {
description: '获取群过滤系统消息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema.extend({
data: z.object({
join_requests: z
.array(
z.object({
request_id: z.string().describe('请求 ID'),
requester_uin: z.string().describe('请求人 QQ 号'),
requester_nick: z.string().describe('请求人昵称'),
group_id: z.string().describe('群号'),
group_name: z.string().describe('群名称'),
checked: z.boolean().describe('是否已处理'),
actor: z.string().describe('处理人 QQ 号'),
})
)
.describe('入群请求列表'),
}),
}),
},
'/set_group_sign': {
description: '设置群打卡',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema,
},
'/send_group_sign': {
description: '发送群打卡',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema,
},
'/get_ai_characters': {
description: '获取AI语音人物',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
chat_type: z.union([z.string(), z.number()]).describe('聊天类型'),
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
type: z.string().describe('类型'),
characters: z.array(
z
.object({
character_id: z.string().describe('人物 ID'),
character_name: z.string().describe('人物名称'),
preview_url: z.string().describe('预览音频地址'),
})
.describe('人物信息')
),
})
),
}),
},
'/send_group_ai_record': {
description: '发送群AI语音',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
character: z.string().describe('人物ID'),
text: z.string().describe('文本内容'),
}),
response: baseResponseSchema.extend({
data: z.object({
message_id: z.string().describe('消息 ID'),
}),
}),
},
'/get_ai_record': {
description: '获取AI语音',
request: z.object({
group_id: z.string().describe('群号'),
character: z.string().describe('人物ID'),
text: z.string().describe('文本内容'),
}),
response: baseResponseSchema.extend({
data: z.string(),
}),
},
} as const;
export default oneBotHttpApiGroup;

View File

@ -1,34 +1,39 @@
import { ZodSchema } from 'zod';
import { TSchema } from '@sinclair/typebox';
import oneBotHttpApiGroup from './group';
import oneBotHttpApiMessage from './message';
import oneBotHttpApiSystem from './system';
import oneBotHttpApiUser from './user';
export interface OneBotHttpApiContent {
description?: string;
payload: TSchema;
response: TSchema;
payloadExample?: any;
}
type AllKey =
| keyof typeof oneBotHttpApiUser
| keyof typeof oneBotHttpApiMessage
| keyof typeof oneBotHttpApiGroup
| keyof typeof oneBotHttpApiSystem;
export type OneBotHttpApi = Record<string, OneBotHttpApiContent>;
export type OneBotHttpApi = Record<
AllKey,
{
description?: string
request: ZodSchema
response: ZodSchema
let oneBotHttpApi: OneBotHttpApi = {};
export async function fetchOneBotHttpApi (): Promise<OneBotHttpApi> {
try {
const response = await fetch('/api/Debug/schemas', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
if (data.code === 0) {
oneBotHttpApi = data.data;
return oneBotHttpApi;
}
} catch (error) {
console.error('Failed to fetch OneBot HTTP API schemas:', error);
}
>;
return {};
}
const oneBotHttpApi: OneBotHttpApi = {
...oneBotHttpApiUser,
...oneBotHttpApiMessage,
...oneBotHttpApiGroup,
...oneBotHttpApiSystem,
} as const;
export function getOneBotHttpApi () {
return oneBotHttpApi;
}
export type OneBotHttpApiPath = keyof OneBotHttpApi;
export type OneBotHttpApiContent = OneBotHttpApi[OneBotHttpApiPath];
export type OneBotHttpApiPath = string;
export default oneBotHttpApi;

View File

@ -1,84 +0,0 @@
import { z } from 'zod';
import type { ZodSchema } from 'zod';
import { baseResponseSchema, commonResponseDataSchema } from '../response';
import messageNodeSchema, { nodeMessage } from './node';
const oneBotHttpApiMessageGroup: Record<
string,
{
description?: string
request: ZodSchema
response: ZodSchema
}
> = {
'/send_group_msg': {
description: '发送群消息',
request: z
.object({
group_id: z
.union([z.string(), z.number()])
.describe('群号')
.describe('群号'),
message: z.array(messageNodeSchema).describe('消息内容'),
})
.refine(
(data) => {
const hasReply = data.message.some((item) => item.type === 'reply');
if (hasReply) {
return data.message[0].type === 'reply';
}
return true;
},
{
message:
'如果 message 包含 reply 类型的消息,那么只能包含一个,而且排在最前面',
}
),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/send_group_forward_msg': {
description: '发送群合并转发消息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
messages: z.array(nodeMessage).describe('消息内容'),
news: z
.array(
z.object({
text: z.string(),
})
)
.describe('?'),
prompt: z.string().describe('外显'),
summary: z.string().describe('底下文本'),
source: z.string().describe('内容'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/forward_group_single_msg': {
description: '消息转发到群',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
message_id: z.union([z.string(), z.number()]).describe('消息 ID'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/group_poke': {
description: '发送戳一戳',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
}),
response: baseResponseSchema,
},
};
export default oneBotHttpApiMessageGroup;

View File

@ -1,290 +0,0 @@
import { z } from 'zod';
import { baseResponseSchema, commonResponseDataSchema } from '../response';
import oneBotHttpApiMessageGroup from './group';
import messageNodeSchema from './node';
import oneBotHttpApiMessagePrivate from './private';
const fileSchema = z
.object({
file: z.string().describe('路径或链接'),
url: z.string().describe('路径或链接'),
file_size: z.string().describe('文件大小'),
file_name: z.string().describe('文件名'),
base64: z.string().describe('文件base64编码'),
})
.describe('文件');
const messageSchema = z
.object({
self_id: z.number().describe('自己QQ号'),
user_id: z.number().describe('发送人QQ号'),
time: z.number().describe('发送时间'),
message_id: z.number().describe('消息ID'),
message_seq: z.number().describe('消息序号'),
real_id: z.number().describe('?ID'),
message_type: z.string().describe('消息类型'),
sender: z
.object({
user_id: z.number().describe('发送人QQ号'),
nickname: z.string().describe('昵称'),
sex: z.enum(['male', 'female', 'unknown']).describe('性别'),
age: z.number().describe('年龄'),
card: z.string().describe('名片'),
role: z.enum(['owner', 'admin', 'member']).describe('角色'),
})
.describe('发送人信息'),
raw_message: z.string().describe('原始消息'),
font: z.number().describe('字体'),
sub_type: z.string().describe('子类型'),
message: z.array(messageNodeSchema).describe('消息内容'),
message_format: z.string().describe('消息格式'),
post_type: z.string().describe('?'),
message_sent_type: z.string().describe('消息发送类型'),
group_id: z.number().describe('群号'),
})
.describe('消息');
const oneBotHttpApiMessage = {
...oneBotHttpApiMessagePrivate,
...oneBotHttpApiMessageGroup,
'/mark_msg_as_read': {
description: '标记消息已读',
request: z
.object({
group_id: z
.union([z.string(), z.number()])
.optional()
.describe('群号,与 user_id 二选一'),
user_id: z
.union([z.string(), z.number()])
.optional()
.describe('用户QQ号与 group_id 二选一'),
})
.refine(
(data) =>
(data.group_id && !data.user_id) || (!data.group_id && data.user_id),
{
message: 'group_id 和 user_id 必须二选一,且不能同时存在或同时为空',
path: ['group_id', 'user_id'],
}
),
response: baseResponseSchema,
},
'/mark_group_msg_as_read': {
description: '标记群消息已读',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
}),
response: baseResponseSchema,
},
'/mark_private_msg_as_read': {
description: '标记私聊消息已读',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('用户QQ号'),
}),
response: baseResponseSchema,
},
'/_mark_all_as_read': {
description: '标记所有消息已读',
request: z.object({}),
response: baseResponseSchema,
},
'/delete_msg': {
description: '撤回消息',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
}),
response: baseResponseSchema,
},
'/get_msg': {
description: '获取消息',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
}),
response: baseResponseSchema.extend({
data: z.object({}),
}),
},
'/get_image': {
description: '获取图片',
request: z.object({
file_id: z.string().describe('文件ID'),
}),
response: baseResponseSchema.extend({
data: fileSchema,
}),
},
'/get_record': {
description: '获取语音',
request: z.object({
file_id: z.string().describe('文件ID'),
out_format: z
.enum(['mp3', 'amr', 'wma', 'm4a', 'spx', 'ogg', 'wav', 'flac'])
.describe('输出格式'),
}),
response: baseResponseSchema.extend({
data: fileSchema,
}),
},
'/get_file': {
description: '获取文件',
request: z.object({
file_id: z.string().describe('文件ID'),
}),
response: baseResponseSchema.extend({
data: fileSchema,
}),
},
'/get_group_msg_history': {
description: '获取群消息历史',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号'),
message_seq: z.union([z.string(), z.number()]).describe('消息序号'),
count: z.number().int().positive().describe('获取数量'),
reverse_order: z.boolean().describe('是否倒序'),
}),
response: baseResponseSchema.extend({
data: z.object({
messages: z.array(messageSchema).describe('消息列表'),
}),
}),
},
'/set_msg_emoji_like': {
description: '贴表情',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
emoji_id: z.number().describe('表情ID'),
set: z.boolean().describe('?'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/get_friend_msg_history': {
description: '获取好友消息历史',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('用户QQ号'),
message_seq: z.union([z.string(), z.number()]).describe('消息序号'),
count: z.number().int().positive().describe('获取数量'),
reverse_order: z.boolean().describe('是否倒序'),
}),
response: baseResponseSchema.extend({
data: z.object({
messages: z.array(messageSchema),
}),
}),
},
'/get_recent_contact': {
description: '最近消息列表',
request: z.object({
count: z.number().int().positive().describe('获取数量'),
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
lastestMsg: messageSchema,
peerUin: z.string().describe('对方QQ号'),
remark: z.string().describe('备注'),
msgTime: z.string().describe('消息时间'),
chatType: z.number().describe('聊天类型'),
msgId: z.string().describe('消息ID'),
sendNickName: z.string().describe('发送人昵称'),
sendMemberName: z.string().describe('发送人?昵称'),
peerName: z.string().describe('对方昵称'),
})
),
}),
},
'/fetch_emoji_like': {
description: '获取贴表情详情',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
emojiId: z.string().describe('表情ID'),
emojiType: z.string().describe('表情类型'),
count: z.number().int().positive().optional().describe('获取数量'),
cookie: z.string().describe('cookie,首次为空,后续为上次返回'),
}),
response: baseResponseSchema.extend({
data: z.object({
result: z.number().describe('结果'),
errMsg: z.string().describe('错误信息'),
emojiLikesList: z
.array(
z
.object({
tinyId: z.string().describe('点击者QQ号'),
nickName: z.string().describe('昵称?'),
headUrl: z.string().describe('头像?'),
})
.describe('表情点击列表')
)
.describe('表情点击列表'),
cookie: z.string().describe('cookie'),
isLastPage: z.boolean().describe('是否最后一页'),
isFirstPage: z.boolean().describe('是否第一页'),
}),
}),
},
'/get_emoji_likes': {
description: '获取贴表情详情列表',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
emojiId: z.string().describe('表情ID'),
emojiType: z.string().describe('表情类型'),
}),
response: baseResponseSchema.extend({
data: z.object({
result: z.number().describe('结果'),
errMsg: z.string().describe('错误信息'),
emojiLikesList: z
.array(
z
.object({
tinyId: z.string().describe('点击者QQ号'),
nickName: z.string().describe('昵称?'),
headUrl: z.string().describe('头像?'),
})
.describe('表情点击列表')
)
.describe('表情点击列表'),
}),
}),
},
'/get_forward_msg': {
description: '获取合并转发消息',
request: z.object({
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
}),
response: baseResponseSchema.extend({
data: z.object({}),
}),
},
'/send_forward_msg': {
description: '发送合并转发消息',
request: z.object({
group_id: z.union([z.string(), z.number()]).optional().describe('群号'),
user_id: z
.union([z.string(), z.number()])
.optional()
.describe('用户QQ号'),
messages: z.array(messageNodeSchema).describe('消息内容'),
news: z
.array(
z.object({
text: z.string(),
})
)
.describe('?'),
prompt: z.string().describe('外显'),
summary: z.string().describe('底下文字'),
source: z.string().describe('内容'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema.extend({
data: z.object({}),
}),
}),
},
} as const;
export default oneBotHttpApiMessage;

View File

@ -1,120 +0,0 @@
import { z } from 'zod';
const messageNode = z.union([
z
.object({
type: z.literal('text'),
data: z.object({
text: z.string(),
}),
})
.describe('文本消息'),
z
.object({
type: z.literal('at'),
data: z.object({
qq: z.string(),
}),
})
.describe('@某人'),
z
.object({
type: z.literal('image'),
data: z.object({
file: z.string(),
}),
})
.describe('图片消息'),
z
.object({
type: z.literal('face'),
data: z.object({
id: z.number(),
}),
})
.describe('表情消息'),
z
.object({
type: z.literal('json'),
data: z.object({
data: z.string(),
}),
})
.describe('json 卡片消息'),
z
.object({
type: z.literal('record'),
data: z.object({
file: z.string(),
}),
})
.describe('语音消息'),
z
.object({
type: z.literal('video'),
data: z.object({
file: z.string(),
}),
})
.describe('视频消息'),
z
.object({
type: z.literal('reply'),
data: z.object({
id: z.number().optional(),
seq: z.number().optional(),
}),
})
.describe('回复消息'),
z
.object({
type: z.literal('music'),
data: z.union([
z.object({
type: z.enum(['qq', '163']),
id: z.string(),
}),
z.object({
type: z.literal('custom'),
url: z.string(),
audio: z.string(),
title: z.string(),
image: z.string(),
}),
]),
})
.describe('音乐消息'),
z
.object({
type: z.literal('dice'),
})
.describe('掷骰子'),
z
.object({
type: z.literal('rps'),
})
.describe('猜拳'),
z
.object({
type: z.literal('file'),
data: z.object({
file: z.string().describe('文件路径,服务器本地或者网络文件均可'),
}),
})
.describe('发送消息'),
]);
export const nodeMessage = z
.object({
type: z.literal('node'),
data: z.object({
user_id: z.string(),
nickname: z.string(),
content: z.array(messageNode),
}),
})
.describe('消息节点');
const messageNodeSchema = z.union([messageNode, nodeMessage]);
export default messageNodeSchema;

View File

@ -1,80 +0,0 @@
import { z } from 'zod';
import type { ZodSchema } from 'zod';
import { baseResponseSchema, commonResponseDataSchema } from '../response';
import messageNodeSchema, { nodeMessage } from './node';
const oneBotHttpApiMessagePrivate: Record<
string,
{
description?: string
request: ZodSchema
response: ZodSchema
}
> = {
'/send_private_msg': {
description: '发送私聊消息',
request: z
.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
message: z.array(messageNodeSchema).describe('消息内容'),
})
.refine(
(data) => {
const hasReply = data.message.some((item) => item.type === 'reply');
if (hasReply) {
return data.message[0].type === 'reply';
}
return true;
},
{
message:
'如果 message 包含 reply 类型的消息,那么只能包含一个,而且排在最前面',
}
),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/send_private_forward_msg': {
description: '发送私聊合并转发消息',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
messages: z.array(nodeMessage).describe('消息内容'),
news: z
.array(
z.object({
text: z.string(),
})
)
.describe('?'),
prompt: z.string().describe('外显'),
summary: z.string().describe('底下文本'),
source: z.string().describe('内容'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/forward_friend_single_msg': {
description: '消息转发到私聊',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/group_poke': {
description: '发送私聊戳一戳',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
}),
response: baseResponseSchema,
},
};
export default oneBotHttpApiMessagePrivate;

View File

@ -1,335 +0,0 @@
import { z } from 'zod';
// 定义 set_online_status 的 data 格式
const onlineStatusDataSchema = z.union([
// 在线
z
.object({
status: z.literal(10),
ext_status: z.literal(0),
battery_status: z.literal(0),
})
.describe('在线'),
// Q我吧
z
.object({
status: z.literal(60),
ext_status: z.literal(0),
battery_status: z.literal(0),
})
.describe('Q我吧'),
// 离开
z
.object({
status: z.literal(30),
ext_status: z.literal(0),
battery_status: z.literal(0),
})
.describe('离开'),
// 忙碌
z
.object({
status: z.literal(50),
ext_status: z.literal(0),
battery_status: z.literal(0),
})
.describe('忙碌'),
// 请勿打扰
z
.object({
status: z.literal(70),
ext_status: z.literal(0),
battery_status: z.literal(0),
})
.describe('请勿打扰'),
// 隐身
z
.object({
status: z.literal(40),
ext_status: z.literal(0),
battery_status: z.literal(0),
})
.describe('隐身'),
// 听歌中
z
.object({
status: z.literal(10),
ext_status: z.literal(1028),
battery_status: z.literal(0),
})
.describe('听歌中'),
// 春日限定
z
.object({
status: z.literal(10),
ext_status: z.literal(2037),
battery_status: z.literal(0),
})
.describe('春日限定'),
// 一起元梦
z
.object({
status: z.literal(10),
ext_status: z.literal(2025),
battery_status: z.literal(0),
})
.describe('一起元梦'),
// 求星搭子
z
.object({
status: z.literal(10),
ext_status: z.literal(2026),
battery_status: z.literal(0),
})
.describe('求星搭子'),
// 被掏空
z
.object({
status: z.literal(10),
ext_status: z.literal(2014),
battery_status: z.literal(0),
})
.describe('被掏空'),
// 今日天气
z
.object({
status: z.literal(10),
ext_status: z.literal(1030),
battery_status: z.literal(0),
})
.describe('今日天气'),
// 我crash了
z
.object({
status: z.literal(10),
ext_status: z.literal(2019),
battery_status: z.literal(0),
})
.describe('我crash了'),
// 爱你
z
.object({
status: z.literal(10),
ext_status: z.literal(2006),
battery_status: z.literal(0),
})
.describe('爱你'),
// 恋爱中
z
.object({
status: z.literal(10),
ext_status: z.literal(1051),
battery_status: z.literal(0),
})
.describe('恋爱中'),
// 好运锦鲤
z
.object({
status: z.literal(10),
ext_status: z.literal(1071),
battery_status: z.literal(0),
})
.describe('好运锦鲤'),
// 水逆退散
z
.object({
status: z.literal(10),
ext_status: z.literal(1201),
battery_status: z.literal(0),
})
.describe('水逆退散'),
// 嗨到飞起
z
.object({
status: z.literal(10),
ext_status: z.literal(1056),
battery_status: z.literal(0),
})
.describe('嗨到飞起'),
// 元气满满
z
.object({
status: z.literal(10),
ext_status: z.literal(1058),
battery_status: z.literal(0),
})
.describe('元气满满'),
// 宝宝认证
z
.object({
status: z.literal(10),
ext_status: z.literal(1070),
battery_status: z.literal(0),
})
.describe('宝宝认证'),
// 一言难尽
z
.object({
status: z.literal(10),
ext_status: z.literal(1063),
battery_status: z.literal(0),
})
.describe('一言难尽'),
// 难得糊涂
z
.object({
status: z.literal(10),
ext_status: z.literal(2001),
battery_status: z.literal(0),
})
.describe('难得糊涂'),
// emo中
z
.object({
status: z.literal(10),
ext_status: z.literal(1401),
battery_status: z.literal(0),
})
.describe('emo中'),
// 我太难了
z
.object({
status: z.literal(10),
ext_status: z.literal(1062),
battery_status: z.literal(0),
})
.describe('我太难了'),
// 我想开了
z
.object({
status: z.literal(10),
ext_status: z.literal(2013),
battery_status: z.literal(0),
})
.describe('我想开了'),
// 我没事
z
.object({
status: z.literal(10),
ext_status: z.literal(1052),
battery_status: z.literal(0),
})
.describe('我没事'),
// 想静静
z
.object({
status: z.literal(10),
ext_status: z.literal(1061),
battery_status: z.literal(0),
})
.describe('想静静'),
// 悠哉哉
z
.object({
status: z.literal(10),
ext_status: z.literal(1059),
battery_status: z.literal(0),
})
.describe('悠哉哉'),
// 去旅行
z
.object({
status: z.literal(10),
ext_status: z.literal(2015),
battery_status: z.literal(0),
})
.describe('去旅行'),
// 信号弱
z
.object({
status: z.literal(10),
ext_status: z.literal(1011),
battery_status: z.literal(0),
})
.describe('信号弱'),
// 出去浪
z
.object({
status: z.literal(10),
ext_status: z.literal(2003),
battery_status: z.literal(0),
})
.describe('出去浪'),
// 肝作业
z
.object({
status: z.literal(10),
ext_status: z.literal(2012),
battery_status: z.literal(0),
})
.describe('肝作业'),
// 学习中
z
.object({
status: z.literal(10),
ext_status: z.literal(1018),
battery_status: z.literal(0),
})
.describe('学习中'),
// 搬砖中
z
.object({
status: z.literal(10),
ext_status: z.literal(2023),
battery_status: z.literal(0),
})
.describe('搬砖中'),
// 摸鱼中
z
.object({
status: z.literal(10),
ext_status: z.literal(1300),
battery_status: z.literal(0),
})
.describe('摸鱼中'),
// 无聊中
z
.object({
status: z.literal(10),
ext_status: z.literal(1060),
battery_status: z.literal(0),
})
.describe('无聊中'),
// timi中
z
.object({
status: z.literal(10),
ext_status: z.literal(1027),
battery_status: z.literal(0),
})
.describe('timi中'),
// 睡觉中
z
.object({
status: z.literal(10),
ext_status: z.literal(1016),
battery_status: z.literal(0),
})
.describe('睡觉中'),
// 熬夜中
z
.object({
status: z.literal(10),
ext_status: z.literal(1032),
battery_status: z.literal(0),
})
.describe('熬夜中'),
// 追剧中
z
.object({
status: z.literal(10),
ext_status: z.literal(1021),
battery_status: z.literal(0),
})
.describe('追剧中'),
// 我的电量
z
.object({
status: z.literal(10),
ext_status: z.literal(1000),
battery_status: z.literal(0),
})
.describe('我的电量'),
]);
export default onlineStatusDataSchema;

View File

@ -1,16 +0,0 @@
import { z } from 'zod';
// 通用响应格式
export const baseResponseSchema = z.object({
status: z.enum(['ok', 'error']).describe('请求状态'), // 状态
retcode: z.number().describe('响应🐎'), // 返回码
data: z.null(),
message: z.string().describe('提示信息'), // 提示信息
wording: z.string().describe('提示信息(人性化)'), // 人性化提示
echo: z.string().describe('回显'), // 请求回显内容
});
export const commonResponseDataSchema = z.object({
result: z.number(),
errMsg: z.string(),
});

View File

@ -1,366 +0,0 @@
import { z } from 'zod';
import { baseResponseSchema, commonResponseDataSchema } from './response';
const oneBotHttpApiSystem = {
'/get_online_clients': {
description: '获取当前账号在线客户端列表',
request: z.object({
no_cache: z.boolean().optional().describe('是否不使用缓存'),
}),
response: baseResponseSchema.extend({
data: z.object({
clients: z.object({}),
}),
}),
},
'/get_robot_uin_range': {
description: '获取机器人账号范围',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
minUin: z.string(),
maxUin: z.string(),
})
),
}),
},
'/ocr_image': {
description: 'OCR图片识别',
request: z.object({
image: z.string(),
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
text: z.string(),
pt1: z.object({
x: z.string(),
y: z.string(),
}),
pt2: z.object({
x: z.string(),
y: z.string(),
}),
pt3: z.object({
x: z.string(),
y: z.string(),
}),
pt4: z.object({
x: z.string(),
y: z.string(),
}),
charBox: z.array(
z.object({
charText: z.string(),
charBox: z.object({
pt1: z.object({
x: z.string(),
y: z.string(),
}),
pt2: z.object({
x: z.string(),
y: z.string(),
}),
pt3: z.object({
x: z.string(),
y: z.string(),
}),
pt4: z.object({
x: z.string(),
y: z.string(),
}),
}),
})
),
score: z.string(),
})
),
}),
},
'/.ocr_image': {
description: '.OCR图片识别',
request: z.object({
image: z.string(),
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
text: z.string(),
pt1: z.object({
x: z.string(),
y: z.string(),
}),
pt2: z.object({
x: z.string(),
y: z.string(),
}),
pt3: z.object({
x: z.string(),
y: z.string(),
}),
pt4: z.object({
x: z.string(),
y: z.string(),
}),
charBox: z.array(
z.object({
charText: z.string(),
charBox: z.object({
pt1: z.object({
x: z.string(),
y: z.string(),
}),
pt2: z.object({
x: z.string(),
y: z.string(),
}),
pt3: z.object({
x: z.string(),
y: z.string(),
}),
pt4: z.object({
x: z.string(),
y: z.string(),
}),
}),
})
),
score: z.string(),
})
),
}),
},
'/translate_en2zh': {
description: '英文翻译为中文',
request: z.object({
words: z.array(z.string()),
}),
response: baseResponseSchema.extend({
data: z.array(z.string()),
}),
},
'/get_login_info': {
description: '获取登录号信息',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
user_id: z.number(),
nickname: z.string(),
}),
}),
},
'/set_input_status': {
description: '设置输入状态',
request: z.object({
eventType: z.union([z.literal(0), z.literal(1)]),
user_id: z.union([z.number(), z.string()]),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/download_file': {
description: '下载文件到缓存目录',
request: z
.object({
base64: z.string().optional(),
url: z.string().optional(),
thread_count: z.number(),
headers: z.union([z.string(), z.array(z.string())]),
name: z.string().optional(),
})
.refine(
(data) => (data.base64 && !data.url) || (!data.base64 && data.url),
{
message: 'base64 和 url 必须二选一,且不能同时存在或同时为空',
path: ['base64', 'url'],
}
),
response: baseResponseSchema.extend({
data: z.object({
file: z.string(),
}),
}),
},
'/get_cookies': {
description: '获取cookies',
request: z.object({
domain: z.string(),
}),
response: baseResponseSchema.extend({
data: z.object({
cookies: z.string(),
bkn: z.string(),
}),
}),
},
'/.handle_quick_operation': {
description: '.对事件执行快速操作',
request: z.object({
context: z.object({}),
operation: z.object({}),
}),
response: baseResponseSchema,
},
'/get_csrf_token': {
description: '获取CSRF Token',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
token: z.number(),
}),
}),
},
'/_del_group_notice': {
description: '_删除群公告',
request: z.object({
group_id: z.union([z.number(), z.string()]),
notice_id: z.number(),
}),
response: baseResponseSchema,
},
'/get_credentials': {
description: '获取 QQ 相关接口凭证',
request: z.object({
domain: z.string(),
}),
response: baseResponseSchema.extend({
data: z.object({
cookies: z.string(),
token: z.number(),
}),
}),
},
'/_get_model_show': {
description: '_获取在线机型',
request: z.object({
model: z.string(),
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
variants: z.object({
model_show: z.string(),
need_pay: z.boolean(),
}),
})
),
}),
},
'/_set_model_show': {
description: '_设置在线机型',
request: z.object({
model: z.string(),
model_show: z.string(),
}),
response: baseResponseSchema,
},
'/can_send_image': {
description: '检查是否可以发送图片',
request: z.object({}),
response: baseResponseSchema.extend({
yes: z.boolean(),
}),
},
'/nc_get_packet_status': {
description: '获取packet状态',
request: z.object({}),
response: baseResponseSchema,
},
'/can_send_record': {
description: '检查是否可以发送语音',
request: z.object({}),
response: baseResponseSchema.extend({
yes: z.boolean(),
}),
},
'/get_status': {
description: '获取状态',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
online: z.boolean(),
good: z.boolean(),
stat: z.object({}),
}),
}),
},
'/nc_get_rkey': {
description: '获取rkey',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
rkey: z.string(),
ttl: z.string(),
time: z.number(),
type: z.number(),
})
),
}),
},
'/get_version_info': {
description: '获取版本信息',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
app_name: z.string(),
protocol_version: z.string(),
app_version: z.string(),
}),
}),
},
'/get_group_shut_list': {
description: '获取群禁言列表',
request: z.object({
group_id: z.union([z.number(), z.string()]),
}),
response: baseResponseSchema.extend({
data: z.object({
uid: z.string(),
qid: z.string(),
uin: z.string(),
nick: z.string(),
remark: z.string(),
cardType: z.number(),
cardName: z.string(),
role: z.number(),
avatarPath: z.string(),
shutUpTime: z.number(),
isDelete: z.boolean(),
isSpecialConcerned: z.boolean(),
isSpecialShield: z.boolean(),
isRobot: z.boolean(),
groupHonor: z.record(z.number()),
memberRealLevel: z.number(),
memberLevel: z.number(),
globalGroupLevel: z.number(),
globalGroupPoint: z.number(),
memberTitleId: z.number(),
memberSpecialTitle: z.string(),
specialTitleExpireTime: z.string(),
userShowFlag: z.number(),
userShowFlagNew: z.number(),
richFlag: z.number(),
mssVipType: z.number(),
bigClubLevel: z.number(),
bigClubFlag: z.number(),
autoRemark: z.string(),
creditLevel: z.number(),
joinTime: z.number(),
lastSpeakTime: z.number(),
memberFlag: z.number(),
memberFlagExt: z.number(),
memberMobileFlag: z.number(),
memberFlagExt2: z.number(),
isSpecialShielded: z.boolean(),
cardNameId: z.number(),
}),
}),
},
} as const;
export default oneBotHttpApiSystem;

View File

@ -1,278 +0,0 @@
import { z } from 'zod';
import onlineStatusDataSchema from './online_status';
import { baseResponseSchema, commonResponseDataSchema } from './response';
const oneBotHttpApiUser = {
'/set_qq_profile': {
description: '设置账号信息',
request: z.object({
nickname: z.string().describe('昵称'),
personal_note: z.string().describe('个性签名'),
sex: z.string().optional().describe('性别'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/send_ark_share': {
description: '获取推荐好友/群聊卡片',
request: z
.object({
group_id: z
.union([z.string(), z.number()])
.optional()
.describe('群聊ID与 user_id 二选一'),
user_id: z
.union([z.string(), z.number()])
.optional()
.describe('用户ID与 group_id 二选一'),
phone_number: z.string().optional().describe('对方手机号码'),
})
.refine(
(data) =>
(data.group_id && !data.user_id) || (!data.group_id && data.user_id),
{
message: 'group_id 和 user_id 必须二选一,且不能同时存在或同时为空',
path: ['group_id', 'user_id'], // 错误路径
}
),
response: baseResponseSchema.extend({
data: z.object({
errCode: z.number(),
errMsg: z.string(),
arkJson: z.string(),
}),
}),
},
'/send_group_ark_share': {
description: '获取推荐群聊卡片',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群聊ID'),
}),
response: baseResponseSchema.extend({
data: z.string(),
}),
},
'/set_online_status': {
description: '设置在线状态',
request: z.object({
data: onlineStatusDataSchema,
}),
response: baseResponseSchema,
},
'/get_friends_with_category': {
description: '获取好友分组列表',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
categoryId: z.number(),
categorySortId: z.number(),
categoryName: z.string(),
categoryMbCount: z.number(),
onlineCount: z.number(),
buddyList: z.array(
z.object({
qid: z.string(),
longNick: z.string(),
birthday_year: z.number(),
birthday_month: z.number(),
birthday_day: z.number(),
age: z.number(),
sex: z.string(),
eMail: z.string(),
phoneNum: z.string(),
categoryId: z.number(),
richTime: z.number(),
richBuffer: z.object({}),
uid: z.string(),
uin: z.string(),
nick: z.string(),
remark: z.string(),
user_id: z.number(),
nickname: z.string(),
level: z.number(),
})
),
})
),
}),
},
'/set_qq_avatar': {
description: '设置头像',
request: z.object({
file: z.string().describe('图片文件路径(服务器本地或者远程均可)'),
}),
response: baseResponseSchema,
},
'/send_like': {
description: '点赞',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
times: z.number().describe('点赞次数'),
}),
response: baseResponseSchema,
},
'/create_collection': {
description: '创建收藏',
request: z.object({
rawData: z.string().describe('收藏内容'),
brief: z.string().describe('收藏简介'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/set_friend_add_request': {
description: '处理好友请求',
request: z.object({
flag: z.string().describe('请求ID'),
approve: z.boolean().describe('是否同意'),
remark: z.string().describe('好友备注'),
}),
response: baseResponseSchema,
},
'/set_self_longnick': {
description: '设置个性签名',
request: z.object({
longNick: z.string().describe('签名内容'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/get_stranger_info': {
description: '获取账号信息',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
}),
response: baseResponseSchema.extend({
data: z.object({
user_id: z.number(),
uid: z.string(),
uin: z.string(),
nickname: z.string(),
age: z.number(),
qid: z.string(),
qqLevel: z.number(),
sex: z.string(),
long_nick: z.string(),
reg_time: z.number(),
is_vip: z.boolean(),
is_years_vip: z.boolean(),
vip_level: z.number(),
remark: z.string(),
status: z.number(),
login_days: z.number(),
}),
}),
},
'/get_friend_list': {
description: '获取好友列表',
request: z.object({
no_cache: z.boolean().describe('是否不使用缓存'),
}),
response: baseResponseSchema.extend({
data: z.array(
z.object({
qid: z.string(),
longNick: z.string(),
birthday_year: z.number(),
birthday_month: z.number(),
birthday_day: z.number(),
age: z.number(),
sex: z.string(),
eMail: z.string(),
phoneNum: z.string(),
categoryId: z.number(),
richTime: z.number(),
richBuffer: z.object({}),
uid: z.string(),
uin: z.string(),
nick: z.string(),
remark: z.string(),
user_id: z.number(),
nickname: z.string(),
level: z.number(),
})
),
}),
},
'/get_profile_like': {
description: '获取点赞列表',
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
total_count: z.number(),
new_count: z.number(),
new_nearby_count: z.number(),
last_visit_time: z.number(),
userInfos: z.array(
z.object({
uid: z.string(),
src: z.number(),
latestTime: z.number(),
count: z.number(),
giftCount: z.number(),
customId: z.number(),
lastCharged: z.number(),
bAvailableCnt: z.number(),
bTodayVotedCnt: z.number(),
nick: z.string(),
gender: z.number(),
age: z.number(),
isFriend: z.boolean(),
isvip: z.boolean(),
isSvip: z.boolean(),
uin: z.number(),
})
),
}),
}),
},
'/fetch_custom_face': {
description: '获取收藏表情',
request: z.object({
count: z.number().optional().describe('获取数量'),
}),
response: baseResponseSchema.extend({
data: z.array(z.string()),
}),
},
'/upload_private_file': {
description: '上传私聊文件',
request: z.object({
user_id: z.union([z.string(), z.number()]),
file: z.string(),
name: z.string(),
}),
response: baseResponseSchema,
},
'/delete_friend': {
description: '删除好友',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('自己QQ号'),
friend_id: z.union([z.string(), z.number()]).describe('好友QQ号'),
temp_block: z.boolean().describe('是否加入黑名单'),
temp_both_del: z.boolean().describe('是否双向删除'),
}),
response: baseResponseSchema.extend({
data: commonResponseDataSchema,
}),
},
'/nc_get_user_status': {
description: '获取用户在线状态',
request: z.object({
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
}),
response: baseResponseSchema.extend({
data: z.object({
status: z.number(),
ext_status: z.number(),
}),
}),
},
} as const;
export default oneBotHttpApiUser;

View File

@ -6,19 +6,20 @@ import { IoClose } from 'react-icons/io5';
import { TbSearch } from 'react-icons/tb';
import key from '@/const/key';
import oneBotHttpApi from '@/const/ob_api';
import type { OneBotHttpApiPath } from '@/const/ob_api';
import { fetchOneBotHttpApi, OneBotHttpApiPath } from '@/const/ob_api';
import type { OneBotHttpApi } from '@/const/ob_api';
import OneBotApiDebug from '@/components/onebot/api/debug';
import CommandPalette from '@/components/command_palette';
import type { CommandPaletteCommand, CommandPaletteExecuteMode } from '@/components/command_palette';
import { generateDefaultJson } from '@/utils/zod';
import { generateDefaultFromTypeBox } from '@/utils/typebox';
import type { OneBotApiDebugRef } from '@/components/onebot/api/debug';
export default function HttpDebug () {
const [activeApi, setActiveApi] = useState<OneBotHttpApiPath | null>(null);
const [openApis, setOpenApis] = useState<OneBotHttpApiPath[]>([]);
const [oneBotHttpApi, setOneBotHttpApi] = useState<OneBotHttpApi>({});
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
const hasBackground = !!backgroundImage;
@ -40,30 +41,34 @@ export default function HttpDebug () {
return () => window.removeEventListener('keydown', handler);
}, []);
// Initialize Debug Adapter
// Initialize Debug Adapter and fetch schemas
useEffect(() => {
let currentAdapterName = '';
const initAdapter = async () => {
const init = async () => {
try {
const response = await fetch('/api/Debug/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
if (data.code === 0) {
currentAdapterName = data.data.adapterName;
setAdapterName(currentAdapterName);
}
const [apiData] = await Promise.all([
fetchOneBotHttpApi(),
fetch('/api/Debug/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
}).then(res => res.json()).then(data => {
if (data.code === 0) {
currentAdapterName = data.data.adapterName;
setAdapterName(currentAdapterName);
}
})
]);
setOneBotHttpApi(apiData);
} catch (error) {
console.error('Failed to create debug adapter:', error);
console.error('Failed to initialize debug:', error);
}
};
initAdapter();
init();
return () => {
// 不再主动关闭 adapter由后端自动管理活跃状态
@ -92,21 +97,24 @@ export default function HttpDebug () {
return Object.keys(oneBotHttpApi).map((p) => {
const path = p as OneBotHttpApiPath;
const item = oneBotHttpApi[path];
const displayPath = '/' + path;
// 简单分组:按描述里已有分类不可靠,这里只用 path 前缀推断
const group = path.startsWith('/get_') ? 'GET' : (path.startsWith('/set_') ? 'SET' : 'API');
const group = path.startsWith('get_') ? 'GET' : (path.startsWith('set_') ? 'SET' : 'API');
return {
id: path,
title: item?.description || path,
subtitle: item?.request ? '回车发送 · Shift+Enter 仅打开' : undefined,
title: item?.description || displayPath,
subtitle: item?.payload ? '回车发送 · Shift+Enter 仅打开' : undefined,
group,
};
});
}, []);
}, [oneBotHttpApi]);
const executeCommand = (commandId: string, mode: CommandPaletteExecuteMode) => {
const api = commandId as OneBotHttpApiPath;
const item = oneBotHttpApi[api];
const body = item?.request ? generateDefaultJson(item.request) : '{}';
const body = item?.payloadExample
? JSON.stringify(item.payloadExample, null, 2)
: (item?.payload ? JSON.stringify(generateDefaultFromTypeBox(item.payload), null, 2) : '{}');
handleSelectApi(api);
// 确保请求参数可见

View File

@ -0,0 +1,123 @@
import { TSchema, Type } from '@sinclair/typebox';
export type ParsedSchema = {
name?: string;
type: string | string[];
optional: boolean;
value?: any;
enum?: any[];
children?: ParsedSchema[];
description?: string;
};
// 定义基础响应结构 (TypeBox 格式)
export const BaseResponseSchema = Type.Object({
status: Type.Union([Type.Literal('ok'), Type.Literal('failed')], { description: '状态 (ok/failed)' }),
retcode: Type.Number({ description: '返回码' }),
data: Type.Any({ description: '数据' }),
message: Type.String({ description: '消息' }),
wording: Type.String({ description: '提示' }),
echo: Type.Optional(Type.String({ description: '回显' })),
});
export function parseTypeBox (schema: TSchema | undefined, name?: string, isRoot = true): ParsedSchema | ParsedSchema[] {
if (!schema) {
return isRoot ? [] : { name, type: 'unknown', optional: false };
}
// 如果是根节点解析,且我们需要将其包装在 BaseResponse 中(通常用于 response
// 但这里我们根据传入的 schema 决定
const description = schema.description;
const optional = false; // TypeBox schema doesn't store optionality in the same way Zod does, usually handled by parent object
// Handle specific types
const type = schema.type;
if (schema.const !== undefined) {
return { name, type: 'value', value: schema.const, optional, description };
}
if (schema.enum) {
return { name, type: 'enum', enum: schema.enum, optional, description };
}
if (schema.anyOf || schema.oneOf) {
const options = (schema.anyOf || schema.oneOf) as TSchema[];
const children = options.map(opt => parseTypeBox(opt, undefined, false) as ParsedSchema);
return { name, type: 'union', children, optional, description };
}
if (schema.allOf) {
const parts = schema.allOf as TSchema[];
// 如果全是对象,尝试合并属性
const allProperties: Record<string, TSchema> = {};
const allRequired: string[] = [];
let canMerge = true;
parts.forEach(part => {
if (part.type === 'object' && part.properties) {
Object.assign(allProperties, part.properties);
if (part.required) allRequired.push(...part.required);
} else {
canMerge = false;
}
});
if (canMerge) {
return parseTypeBox({ ...schema, type: 'object', properties: allProperties, required: allRequired }, name, isRoot);
}
// 无法简单合并,当作联合展示
const children = parts.map(part => parseTypeBox(part, undefined, false) as ParsedSchema);
return { name, type: 'intersection', children, optional, description };
}
if (type === 'object') {
const properties = schema.properties || {};
const required = schema.required || [];
const children = Object.keys(properties).map(key => {
const child = parseTypeBox(properties[key], key, false) as ParsedSchema;
child.optional = !required.includes(key);
return child;
});
if (isRoot) return children;
return { name, type: 'object', children, optional, description };
}
if (type === 'array') {
const items = schema.items as TSchema;
const child = parseTypeBox(items, undefined, false) as ParsedSchema;
return { name, type: 'array', children: [child], optional, description };
}
if (type === 'string') return { name, type: 'string', optional, description };
if (type === 'number' || type === 'integer') return { name, type: 'number', optional, description };
if (type === 'boolean') return { name, type: 'boolean', optional, description };
if (type === 'null') return { name, type: 'null', optional, description };
return { name, type: type || 'unknown', optional, description };
}
export function generateDefaultFromTypeBox (schema: TSchema | undefined): any {
if (!schema) return {};
if (schema.const !== undefined) return schema.const;
if (schema.default !== undefined) return schema.default;
if (schema.enum) return schema.enum[0];
if (schema.anyOf || schema.oneOf) return generateDefaultFromTypeBox((schema.anyOf || schema.oneOf)[0]);
const type = schema.type;
if (type === 'object') {
const obj: any = {};
const props = schema.properties || {};
for (const key in props) {
// Only generate defaults for required properties or if we want a full example
obj[key] = generateDefaultFromTypeBox(props[key]);
}
return obj;
}
if (type === 'array') return [];
if (type === 'string') return '';
if (type === 'number' || type === 'integer') return 0;
if (type === 'boolean') return false;
if (type === 'null') return null;
return null;
}

View File

@ -509,6 +509,9 @@ importers:
'@reduxjs/toolkit':
specifier: ^2.5.1
version: 2.10.1(react-redux@9.2.0(@types/react@19.2.4)(react@19.2.0)(redux@5.0.1))(react@19.2.0)
'@sinclair/typebox':
specifier: ^0.34.41
version: 0.34.41
'@uidotdev/usehooks':
specifier: ^2.4.1
version: 2.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)