From 2b225700f733d25f5541b8cb8ff28d23d45e193d Mon Sep 17 00:00:00 2001 From: bietiaop <1527109126@qq.com> Date: Wed, 18 Dec 2024 15:25:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(debug):=20=E6=94=AF=E6=8C=81HTTP=E8=B0=83?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 75 ++- src/components/button/add_button.tsx | 84 ++- src/components/code_editor.tsx | 2 +- src/components/display_card/common_card.tsx | 2 +- src/components/onebot/api/debug.tsx | 211 ++++++ src/components/onebot/api/display_struct.tsx | 182 +++++ src/components/onebot/api/nav_list.tsx | 69 ++ src/components/onebot/render_message.tsx | 2 +- src/components/onebot/ws_status.tsx | 4 +- src/components/switch_card.tsx | 10 - src/const/ob_api/group.ts | 667 +++++++++++++++++++ src/const/ob_api/index.ts | 32 + src/const/ob_api/message/group.ts | 78 +++ src/const/ob_api/message/index.ts | 250 +++++++ src/const/ob_api/message/node.ts | 93 +++ src/const/ob_api/message/private.ts | 77 +++ src/const/ob_api/online_status.ts | 253 +++++++ src/const/ob_api/response.ts | 16 + src/const/ob_api/system.ts | 365 ++++++++++ src/const/ob_api/user.ts | 277 ++++++++ src/pages/dashboard/debug/http/index.tsx | 26 +- src/pages/web_login.tsx | 3 +- src/utils/url.ts | 21 + src/utils/zod.ts | 263 ++++++++ 24 files changed, 2993 insertions(+), 69 deletions(-) create mode 100644 src/components/onebot/api/debug.tsx create mode 100644 src/components/onebot/api/display_struct.tsx create mode 100644 src/components/onebot/api/nav_list.tsx create mode 100644 src/const/ob_api/group.ts create mode 100644 src/const/ob_api/index.ts create mode 100644 src/const/ob_api/message/group.ts create mode 100644 src/const/ob_api/message/index.ts create mode 100644 src/const/ob_api/message/node.ts create mode 100644 src/const/ob_api/message/private.ts create mode 100644 src/const/ob_api/online_status.ts create mode 100644 src/const/ob_api/response.ts create mode 100644 src/const/ob_api/system.ts create mode 100644 src/const/ob_api/user.ts create mode 100644 src/utils/zod.ts diff --git a/package.json b/package.json index 0934a81..72e0df7 100644 --- a/package.json +++ b/package.json @@ -16,31 +16,31 @@ "@monaco-editor/react": "^4.6.0", "@nextui-org/avatar": "^2.2.4", "@nextui-org/breadcrumbs": "^2.2.4", - "@nextui-org/button": "2.2.4", - "@nextui-org/card": "^2.2.4", - "@nextui-org/checkbox": "^2.3.4", + "@nextui-org/button": "2.2.7", + "@nextui-org/card": "^2.2.7", + "@nextui-org/checkbox": "^2.3.6", "@nextui-org/chip": "^2.2.4", "@nextui-org/code": "2.2.4", - "@nextui-org/dropdown": "^2.3.4", + "@nextui-org/dropdown": "^2.3.7", "@nextui-org/image": "^2.2.3", - "@nextui-org/input": "2.4.4", + "@nextui-org/input": "2.4.6", "@nextui-org/kbd": "2.2.4", - "@nextui-org/link": "2.2.4", - "@nextui-org/listbox": "^2.3.4", - "@nextui-org/modal": "^2.2.4", - "@nextui-org/navbar": "2.2.4", - "@nextui-org/popover": "^2.3.4", - "@nextui-org/select": "^2.4.4", - "@nextui-org/slider": "^2.4.4", - "@nextui-org/snippet": "2.2.5", + "@nextui-org/link": "2.2.5", + "@nextui-org/listbox": "^2.3.7", + "@nextui-org/modal": "^2.2.5", + "@nextui-org/navbar": "2.2.5", + "@nextui-org/popover": "^2.3.7", + "@nextui-org/select": "^2.4.7", + "@nextui-org/slider": "^2.4.5", + "@nextui-org/snippet": "2.2.8", "@nextui-org/spinner": "^2.2.4", - "@nextui-org/switch": "2.2.4", - "@nextui-org/system": "2.4.1", - "@nextui-org/tabs": "^2.2.4", - "@nextui-org/theme": "2.4.1", - "@nextui-org/tooltip": "^2.2.4", + "@nextui-org/switch": "2.2.6", + "@nextui-org/system": "2.4.4", + "@nextui-org/tabs": "^2.2.5", + "@nextui-org/theme": "2.4.3", + "@nextui-org/tooltip": "^2.2.5", "@react-aria/visually-hidden": "3.8.18", - "@reduxjs/toolkit": "^2.4.0", + "@reduxjs/toolkit": "^2.5.0", "@uidotdev/usehooks": "^2.4.1", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", @@ -50,40 +50,41 @@ "axios": "^1.7.9", "clsx": "2.1.1", "event-source-polyfill": "^1.0.31", - "framer-motion": "^11.13.1", - "monaco-editor": "^0.52.0", - "motion": "^11.13.1", + "framer-motion": "^11.14.4", + "monaco-editor": "^0.52.2", + "motion": "^11.14.4", "qface": "^1.4.1", - "qrcode.react": "^4.1.0", - "react": "18.3.1", - "react-dom": "18.3.1", + "qrcode.react": "^4.2.0", + "react": "19.0.0", + "react-dom": "19.0.0", "react-error-boundary": "^4.1.2", - "react-hook-form": "^7.54.0", + "react-hook-form": "^7.54.1", "react-hot-toast": "^2.4.1", "react-icons": "^5.4.0", - "react-redux": "^9.1.2", + "react-redux": "^9.2.0", "react-responsive": "^10.0.0", "react-router-dom": "7.0.2", "react-use-websocket": "^4.11.1", "react-window": "^1.8.10", "tailwind-variants": "0.3.0", - "tailwindcss": "3.4.16" + "tailwindcss": "3.4.16", + "zod": "^3.24.1" }, "devDependencies": { - "@commitlint/cli": "^19.6.0", + "@commitlint/cli": "^19.6.1", "@commitlint/config-conventional": "^19.6.0", - "@eslint/js": "^9.16.0", + "@eslint/js": "^9.17.0", "@react-types/shared": "^3.26.0", "@types/event-source-polyfill": "^1.0.5", - "@types/node": "22.10.1", - "@types/react": "18.3.3", - "@types/react-dom": "18.3.0", + "@types/node": "22.10.2", + "@types/react": "19.0.1", + "@types/react-dom": "19.0.2", "@types/react-window": "^1.8.8", - "@typescript-eslint/eslint-plugin": "8.17.0", - "@typescript-eslint/parser": "8.17.0", + "@typescript-eslint/eslint-plugin": "8.18.0", + "@typescript-eslint/parser": "8.18.0", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "10.4.20", - "eslint": "^9.16.0", + "eslint": "^9.17.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.2", @@ -94,7 +95,7 @@ "eslint-plugin-unused-imports": "4.1.4", "globals": "^15.13.0", "husky": "^9.1.7", - "lint-staged": "^15.2.10", + "lint-staged": "^15.2.11", "postcss": "8.4.49", "prettier": "3.4.2", "typescript": "5.7.2", diff --git a/src/components/button/add_button.tsx b/src/components/button/add_button.tsx index 7c0e52e..4cf79d5 100644 --- a/src/components/button/add_button.tsx +++ b/src/components/button/add_button.tsx @@ -15,6 +15,8 @@ import { import { Button } from '@nextui-org/button' import { IoAddCircleOutline } from 'react-icons/io5' +import { Tooltip } from '@nextui-org/tooltip' +import { FaRegCircleQuestion } from 'react-icons/fa6' export interface AddButtonProps { onOpen: (key: keyof OneBotConfig['network']) => void @@ -49,7 +51,7 @@ const AddButton: React.FC = (props) => {
@@ -61,43 +63,115 @@ const AddButton: React.FC = (props) => {
} > - HTTP服务器 +
+ HTTP服务器 + + + +
} > - HTTP客户端 +
+ HTTP客户端 + + + +
} > - Websocket服务器 +
+ Websocket服务器 + + + +
} > - Websocket客户端 +
+ Websocket客户端 + + + +
diff --git a/src/components/code_editor.tsx b/src/components/code_editor.tsx index 448a9f7..ad43c84 100644 --- a/src/components/code_editor.tsx +++ b/src/components/code_editor.tsx @@ -37,7 +37,7 @@ const CodeEditor = React.forwardRef( if (typeof ref === 'function') { ref(editor) } else { - ;(ref as React.MutableRefObject).current = editor + ;(ref as React.RefObject).current = editor } } if (props.onMount) { diff --git a/src/components/display_card/common_card.tsx b/src/components/display_card/common_card.tsx index 7b24e83..cd0ba5b 100644 --- a/src/components/display_card/common_card.tsx +++ b/src/components/display_card/common_card.tsx @@ -88,7 +88,7 @@ const NetworkDisplayCard = ({ } tag={showType && typeLabel} diff --git a/src/components/onebot/api/debug.tsx b/src/components/onebot/api/debug.tsx new file mode 100644 index 0000000..362c90a --- /dev/null +++ b/src/components/onebot/api/debug.tsx @@ -0,0 +1,211 @@ +import { useEffect, useState } from 'react' +import { motion } from 'motion/react' +import { OneBotHttpApiContent, OneBotHttpApiPath } from '@/const/ob_api' +import { parse, generateDefaultJson } from '@/utils/zod' +import DisplayStruct from './display_struct' +import { useLocalStorage } from '@uidotdev/usehooks' +import key from '@/const/key' +import { Input } from '@nextui-org/input' +import { Button } from '@nextui-org/button' +import { IoLink, IoSend } from 'react-icons/io5' +import { PiCatDuotone } from 'react-icons/pi' +import CodeEditor from '@/components/code_editor' +import { Card, CardBody, CardHeader } from '@nextui-org/card' +import { request } from '@/utils/request' +import { parseAxiosResponse } from '@/utils/url' +import toast from 'react-hot-toast' +import PageLoading from '@/components/page_loading' +import { Snippet } from '@nextui-org/snippet' + +export interface OneBotApiDebugProps { + path: OneBotHttpApiPath + data: OneBotHttpApiContent +} + +const OneBotApiDebug: React.FC = (props) => { + const { path, data } = props + const [url] = useLocalStorage(key.storeURL, 'http://127.0.0.1:6099') + const defaultHttpUrl = url.replace(':6099', ':3000') + const [httpConfig, setHttpConfig] = useState({ + url: defaultHttpUrl, + token: '' + }) + const [requestBody, setRequestBody] = useState('{}') + const [responseContent, setResponseContent] = useState('') + const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false) + const [isResponseOpen, setIsResponseOpen] = useState(false) + const [isFetching, setIsFetching] = useState(false) + const parsedRequest = parse(data.request) + const parsedResponse = parse(data.response) + + const sendRequest = async () => { + if (isFetching) return + setIsFetching(true) + const r = toast.loading('正在发送请求...') + request + .post(httpConfig.url + path, { + headers: { + Authorization: `Bearer ${httpConfig.token}` + }, + responseType: 'text', + body: requestBody + }) + .then((res) => { + setResponseContent(parseAxiosResponse(res)) + }) + .catch((err) => { + setResponseContent(parseAxiosResponse(err.response)) + }) + .finally(() => { + setIsFetching(false) + toast.dismiss(r) + toast.success('请求发送成功') + }) + } + + useEffect(() => { + setRequestBody(generateDefaultJson(data.request)) + setResponseContent('') + }, [path]) + + return ( +
+

+ + {data.description} +

+

+ } + > + {path} + +

+
+ + setHttpConfig({ ...httpConfig, url: e.target.value }) + } + /> + + setHttpConfig({ ...httpConfig, token: e.target.value }) + } + /> + +
+ + + 请求体 + + + + + setRequestBody(value ?? '')} + language="json" + height="200px" + /> + +
+ +
+
+
+
+ + + + 响应 + + + + + +
+              
+                {responseContent || (
+                  
暂无响应
+ )} +
+
+
+
+
+
+

请求体结构

+ +

响应体结构

+ +
+
+ ) +} + +export default OneBotApiDebug diff --git a/src/components/onebot/api/display_struct.tsx b/src/components/onebot/api/display_struct.tsx new file mode 100644 index 0000000..8e992c8 --- /dev/null +++ b/src/components/onebot/api/display_struct.tsx @@ -0,0 +1,182 @@ +import React, { useState } from 'react' +import type { LiteralValue, ParsedSchema } from '@/utils/zod' +import { Chip } from '@nextui-org/chip' +import { motion } from 'motion/react' +import { TbSquareRoundedChevronRightFilled } from 'react-icons/tb' +import { Tooltip } from '@nextui-org/tooltip' +import toast from 'react-hot-toast' + +interface DisplayStructProps { + schema: ParsedSchema | ParsedSchema[] +} + +const SchemaType = ({ + type, + value +}: { + type: string + value?: LiteralValue +}) => { + let name = type + switch (type) { + case 'union': + name = '联合类型' + break + case 'value': + name = '固定值' + break + } + let chipColor: 'primary' | 'success' | 'danger' | 'warning' | 'secondary' = + 'primary' + switch (type) { + case 'enum': + chipColor = 'warning' + break + case 'union': + chipColor = 'secondary' + break + case 'array': + chipColor = 'danger' + break + case 'object': + chipColor = 'success' + break + } + + return ( + + {name} + {type === 'value' && ( + + {value} + + )} + + ) +} + +const SchemaLabel: React.FC<{ + schema: ParsedSchema +}> = ({ schema }) => ( + <> + {Array.isArray(schema.type) ? ( + schema.type.map((type) => ( + + )) + ) : ( + + )} + {schema.optional && ( + + 可选 + + )} + {schema.description && ( + {schema.description} + )} + +) + +const SchemaContainer: React.FC<{ + schema: ParsedSchema + children: React.ReactNode +}> = ({ schema, children }) => { + const [expanded, setExpanded] = useState(false) + + const toggleExpand = () => setExpanded(!expanded) + + return ( +
+
+ + + + + { + e.stopPropagation() + navigator.clipboard.writeText(schema.name || '') + toast.success('已复制') + }} + > + {schema.name} + + + +
+ +
+ {children} +
+
+ ) +} + +const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => { + if (schema.type === 'object') { + return ( + + {schema.children && schema.children.length > 0 ? ( + schema.children.map((child, i) => ( + + )) + ) : ( +
{`{}`}
+ )} +
+ ) + } + + if (schema.type === 'array' || schema.type === 'union') { + return ( + + {schema.children?.map((child, i) => ( + + ))} + + ) + } + + return ( +
+ + { + e.stopPropagation() + navigator.clipboard.writeText(schema.name || '') + toast.success('已复制') + }} + > + {schema.name} + + + +
+ ) +} + +const DisplayStruct: React.FC = ({ schema }) => { + return ( +
+ {Array.isArray(schema) ? ( + schema.map((s, i) => ) + ) : ( + + )} +
+ ) +} + +export default DisplayStruct diff --git a/src/components/onebot/api/nav_list.tsx b/src/components/onebot/api/nav_list.tsx new file mode 100644 index 0000000..b494b12 --- /dev/null +++ b/src/components/onebot/api/nav_list.tsx @@ -0,0 +1,69 @@ +import type { OneBotHttpApi, OneBotHttpApiPath } from '@/const/ob_api' +import { Card, CardBody } from '@nextui-org/card' +import { Input } from '@nextui-org/input' +import clsx from 'clsx' +import { useState } from 'react' + +export interface OneBotApiNavListProps { + data: OneBotHttpApi + selectedApi: OneBotHttpApiPath + onSelect: (apiName: OneBotHttpApiPath) => void +} + +const OneBotApiNavList: React.FC = (props) => { + const { data, selectedApi, onSelect } = props + const [searchValue, setSearchValue] = useState('') + + return ( +
+ setSearchValue(e.target.value)} + isClearable + onClear={() => setSearchValue('')} + /> + {Object.entries(data).map(([apiName, api]) => ( + onSelect(apiName as OneBotHttpApiPath)} + > + +

{api.description}

+
+ {apiName} +
+
+
+ ))} +
+ ) +} + +export default OneBotApiNavList diff --git a/src/components/onebot/render_message.tsx b/src/components/onebot/render_message.tsx index f75d84f..b5a8560 100644 --- a/src/components/onebot/render_message.tsx +++ b/src/components/onebot/render_message.tsx @@ -7,7 +7,7 @@ import { FaReply } from 'react-icons/fa6' export const renderMessageContent = ( segments: OB11Segment[], small = false -): JSX.Element[] => { +): React.ReactElement[] => { return segments.map((segment, index) => { switch (segment.type) { case 'text': diff --git a/src/components/onebot/ws_status.tsx b/src/components/onebot/ws_status.tsx index cfd1d44..a88714d 100644 --- a/src/components/onebot/ws_status.tsx +++ b/src/components/onebot/ws_status.tsx @@ -10,7 +10,7 @@ function StatusTag({ color }: { title: string - color: 'success' | 'danger' | 'error' | 'warning' + color: 'success' | 'danger' | 'warning' }) { const textClassName = `text-${color} text-sm` const bgClassName = `bg-${color}` @@ -27,7 +27,7 @@ export default function WSStatus({ state }: WSStatusProps) { return } if (state === ReadyState.CLOSED) { - return + return } if (state === ReadyState.CONNECTING) { return diff --git a/src/components/switch_card.tsx b/src/components/switch_card.tsx index 8743920..9968970 100644 --- a/src/components/switch_card.tsx +++ b/src/components/switch_card.tsx @@ -25,16 +25,6 @@ const SwitchCard = forwardRef( 'inline-flex flex-row-reverse w-full max-w-md bg-content1 hover:bg-content2 items-center', 'justify-between cursor-pointer rounded-lg gap-2 p-3 border-2 border-transparent', 'data-[selected=true]:border-primary' - ), - wrapper: 'p-0 h-4 overflow-visible w-12 flex-grow-0', - thumb: clsx( - 'w-6 h-6 border-2 shadow-lg flex-shrink-0 rounded-full', - 'group-data-[hover=true]:border-primary', - //selected - 'group-data-[selected=true]:ml-7', - // pressed - 'group-data-[pressed=true]:w-7', - 'group-data-[selected]:group-data-[pressed]:ml-4' ) }} {...props} diff --git a/src/const/ob_api/group.ts b/src/const/ob_api/group.ts new file mode 100644 index 0000000..4dfdeb3 --- /dev/null +++ b/src/const/ob_api/group.ts @@ -0,0 +1,667 @@ +import { z } from 'zod' +import { baseResponseSchema, commonResponseDataSchema } from './response' +import messageNodeSchema from './message/node' + +const oneBotHttpApiGroup = { + '/set_group_kick': { + description: '群踢人', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + user_id: z.union([z.string(), z.number()]), + reject_add_request: z.boolean() + }), + response: baseResponseSchema + }, + '/set_group_ban': { + description: '群禁言', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + user_id: z.union([z.string(), z.number()]), + duration: z.number() + }), + response: baseResponseSchema + }, + '/get_group_system_msg': { + description: '获取群系统消息', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({ + InvitedRequest: z.array( + z.object({ + request_id: z.string(), + invitor_uin: z.string(), + invitor_nick: z.string(), + group_id: z.string(), + group_name: z.string(), + checked: z.boolean(), + actor: z.string() + }) + ), + join_requests: z.array( + z.object({ + request_id: z.string(), + requester_uin: z.string(), + requester_nick: z.string(), + group_id: z.string(), + group_name: z.string(), + checked: z.boolean(), + actor: z.string() + }) + ) + }) + }) + }, + '/get_essence_msg_list': { + description: '获取精华消息', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.array( + z.object({ + msg_seq: z.number(), + msg_random: z.number(), + sender_id: z.number(), + sender_nick: z.string(), + operator_id: z.number(), + operator_nick: z.string(), + message_id: z.string(), + operator_time: z.string(), + content: z.array(messageNodeSchema) + }) + ) + }) + }, + '/set_group_whole_ban': { + description: '全员禁言', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + enable: z.boolean() + }), + response: baseResponseSchema + }, + '/set_group_portrait': { + description: '设置群头像', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + file: z.string() + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema + }) + }, + '/set_group_admin': { + description: '设置群管理', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + user_id: z.union([z.string(), z.number()]), + enable: z.boolean() + }), + response: baseResponseSchema + }, + '/set_essence_msg': { + description: '设置群精华消息', + request: z.object({ + message_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({ + errCode: z.number(), + errMsg: z.string(), + result: z.object({ + wording: z.string(), + digestUin: z.string(), + digestTime: z.number(), + msg: z.object({ + groupCode: z.string(), + msgSeq: z.number(), + msgRandom: z.number(), + msgContent: z.array(messageNodeSchema), + textSize: z.string(), + picSize: z.string(), + videoSize: z.string(), + senderUin: z.string(), + senderTime: z.number(), + addDigestUin: z.string(), + addDigestTime: z.number(), + startTime: z.number(), + latestMsgSeq: z.number(), + opType: z.number() + }), + errorCode: z.number() + }) + }) + }) + }, + '/set_group_card': { + description: '设置群成员名片', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + user_id: z.union([z.string(), z.number()]), + card: z.string() + }), + response: baseResponseSchema + }, + '/delete_essence_msg': { + description: '删除群精华消息', + request: z.object({ + message_id: z.union([z.string(), z.number()]) + }), + + response: baseResponseSchema.extend({ + data: z.object({ + errCode: z.number(), + errMsg: z.string(), + result: z.object({ + wording: z.string(), + digestUin: z.string(), + digestTime: z.number(), + msg: z.object({ + groupCode: z.string(), + msgSeq: z.number(), + msgRandom: z.number(), + msgContent: z.array(messageNodeSchema), + textSize: z.string(), + picSize: z.string(), + videoSize: z.string(), + senderUin: z.string(), + senderTime: z.number(), + addDigestUin: z.string(), + addDigestTime: z.number(), + startTime: z.number(), + latestMsgSeq: z.number(), + opType: z.number() + }), + errorCode: z.number() + }) + }) + }) + }, + '/set_group_name': { + description: '设置群名称', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + group_name: z.string() + }), + response: baseResponseSchema + }, + '/set_group_leave': { + description: '退出群聊', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema + }, + '/_send_group_notice': { + description: '发送群公告', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + content: z.string(), + image: z.string().optional() + }), + response: baseResponseSchema + }, + '/_get_group_notice': { + description: '获取群公告', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.array( + z.object({ + notice_id: z.string(), + sender_id: z.number(), + publish_time: z.number(), + message: z.object({ + text: z.string(), + image: z.array( + z.object({ + id: z.string(), + height: z.string(), + width: z.string() + }) + ) + }) + }) + ) + }) + }, + '/set_group_special_title': { + description: '设置群成员专属头衔', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + user_id: z.union([z.string(), z.number()]), + special_title: z.string() + }), + response: baseResponseSchema + }, + '/upload_group_file': { + description: '上传群文件', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + file: z.string(), + name: z.string(), + folder_id: z.string() + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema + }) + }, + '/set_group_add_request': { + description: '处理加群请求', + request: z.object({ + flag: z.string(), + approve: z.boolean(), + reason: z.string().optional() + }), + response: baseResponseSchema + }, + '/get_group_info': { + description: '获取群信息', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({}) + }) + }, + '/get_group_info_ex': { + description: '获取群信息扩展', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({ + groupCode: z.string(), + resultCode: z.number(), + extInfo: z.object({ + groupInfoExtSeq: z.number(), + reserve: z.number(), + luckyWordId: z.string(), + lightCharNum: z.number(), + luckyWord: z.string(), + starId: z.number(), + essentialMsgSwitch: z.number(), + todoSeq: z.number(), + blacklistExpireTime: z.number(), + isLimitGroupRtc: z.number(), + companyId: z.number(), + hasGroupCustomPortrait: z.number(), + bindGuildId: z.string(), + groupOwnerId: z.object({ + memberUin: z.string(), + memberUid: z.string(), + memberQid: z.string() + }), + essentialMsgPrivilege: z.number(), + msgEventSeq: z.string(), + inviteRobotSwitch: z.number(), + gangUpId: z.string(), + qqMusicMedalSwitch: z.number(), + showPlayTogetherSwitch: z.number(), + groupFlagPro1: z.string(), + groupBindGuildIds: z.object({ + guildIds: z.array(z.string()) + }), + viewedMsgDisappearTime: z.string(), + groupExtFlameData: z.object({ + switchState: z.number(), + state: z.number(), + dayNums: z.array(z.number()), + version: z.number(), + updateTime: z.string(), + isDisplayDayNum: z.boolean() + }), + groupBindGuildSwitch: z.number(), + groupAioBindGuildId: z.string(), + groupExcludeGuildIds: z.object({ + guildIds: z.array(z.string()) + }), + fullGroupExpansionSwitch: z.number(), + fullGroupExpansionSeq: z.string(), + inviteRobotMemberSwitch: z.number(), + inviteRobotMemberExamine: z.number(), + groupSquareSwitch: z.number() + }) + }) + }) + }, + '/create_group_file_folder': { + description: '创建群文件夹', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + folder_name: z.string() + }), + response: baseResponseSchema.extend({ + data: z.object({ + result: z.object({ + retCode: z.number(), + retMsg: z.string(), + clientWording: z.string() + }), + groupItem: z.object({ + peerId: z.string(), + type: z.string(), + folderInfo: z.object({ + folderId: z.string(), + parentFolderId: z.string(), + folderName: z.string(), + createTime: z.number(), + modifyTime: z.number(), + createUin: z.string(), + creatorName: z.string(), + totalFileCount: z.string(), + modifyUin: z.string(), + modifyName: z.string(), + usedSpace: z.string() + }) + }) + }) + }) + }, + '/delete_group_file': { + description: '删除群文件', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + file_id: z.string() + }), + response: baseResponseSchema.extend({ + data: z.object({ + result: z.number(), + errMsg: z.string(), + transGroupFileResult: z.object({ + result: z.object({ + retCode: z.number(), + retMsg: z.string(), + clientWording: z.string() + }), + successFileIdList: z.array(z.string()), + failFileIdList: z.array(z.string()) + }) + }) + }) + }, + '/delete_group_folder': { + description: '删除群文件夹', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + folder_id: z.string() + }), + response: baseResponseSchema.extend({ + data: z.object({ + retCode: z.number(), + retMsg: z.string(), + clientWording: z.string() + }) + }) + }, + '/get_group_file_system_info': { + description: '获取群文件系统信息', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({ + file_count: z.number(), + limit_count: z.number(), + used_space: z.number(), + total_space: z.number() + }) + }) + }, + '/get_group_root_files': { + description: '获取群根目录文件列表', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.array( + z.object({ + files: z.array( + z.object({ + group_id: z.number(), + file_id: z.string(), + file_name: z.string(), + busid: z.number(), + size: z.number(), + upload_time: z.number(), + dead_time: z.number(), + modify_time: z.number(), + download_times: z.number(), + uploader: z.number(), + uploader_name: z.string() + }) + ), + folders: z.array( + z.object({ + group_id: z.number(), + folder_id: z.string(), + folder: z.string(), + folder_name: z.string(), + create_time: z.string(), + creator: z.string(), + creator_name: z.string(), + total_file_count: z.string() + }) + ) + }) + ) + }) + }, + '/get_group_files_by_folder': { + description: '获取群子目录文件列表', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + folder_id: z.string(), + file_count: z.number() + }), + response: baseResponseSchema.extend({ + data: z.object({ + files: z.array( + z.object({ + group_id: z.number(), + file_id: z.string(), + file_name: z.string(), + busid: z.number(), + size: z.number(), + upload_time: z.number(), + dead_time: z.number(), + modify_time: z.number(), + download_times: z.number(), + uploader: z.number(), + uploader_name: z.string() + }) + ), + folders: z.array( + z.object({ + group_id: z.number(), + folder_id: z.string(), + folder: z.string(), + folder_name: z.string(), + create_time: z.string(), + creator: z.string(), + creator_name: z.string(), + total_file_count: z.string() + }) + ) + }) + }) + }, + '/get_group_file_url': { + description: '获取群文件下载链接', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + file_id: z.string() + }), + response: baseResponseSchema.extend({ + data: z.object({ + url: z.string() + }) + }) + }, + '/get_group_list': { + description: '获取群列表', + request: z.object({ + next_token: z.string().optional() + }), + response: baseResponseSchema.extend({ + data: z.array(z.object({})) + }) + }, + '/get_group_member_info': { + description: '获取群成员信息', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + user_id: z.union([z.string(), z.number()]), + no_cache: z.boolean() + }), + response: baseResponseSchema.extend({ + data: z.object({}) + }) + }, + '/get_group_member_list': { + description: '获取群成员列表', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + no_cache: z.boolean() + }), + response: baseResponseSchema.extend({ + data: z.array(z.object({})) + }) + }, + '/get_group_honor_info': { + description: '获取群荣誉', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({ + group_id: z.string(), + current_talkative: z.object({ + user_id: z.number(), + avatar: z.string(), + nickname: z.string(), + day_count: z.number(), + description: z.string() + }), + talkative_list: z.array( + z.object({ + user_id: z.number(), + avatar: z.string(), + nickname: z.string(), + day_count: z.number(), + description: z.string() + }) + ), + performer_list: z.array( + z.object({ + user_id: z.number(), + avatar: z.string(), + nickname: z.string(), + description: z.string() + }) + ), + legend_list: z.array(z.string()), + emotion_list: z.array(z.string()), + strong_newbie_list: z.array(z.string()) + }) + }) + }, + '/get_group_at_all_remain': { + description: '获取群 @全体成员 剩余次数', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({ + can_at_all: z.boolean(), + remain_at_all_count_for_group: z.number(), + remain_at_all_count_for_uin: z.number() + }) + }) + }, + '/get_group_ignored_notifies': { + description: '获取群过滤系统消息', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({ + join_requests: z.array( + z.object({ + request_id: z.string(), + requester_uin: z.string(), + requester_nick: z.string(), + group_id: z.string(), + group_name: z.string(), + checked: z.boolean(), + actor: z.string() + }) + ) + }) + }) + }, + '/set_group_sign': { + description: '设置群打卡', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema + }, + '/send_group_sign': { + description: '发送群打卡', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema + }, + '/get_ai_characters': { + description: '获取AI语音人物', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + chat_type: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.array( + z.object({ + type: z.string(), + characters: z.array( + z.object({ + character_id: z.string(), + character_name: z.string(), + preview_url: z.string() + }) + ) + }) + ) + }) + }, + '/send_group_ai_record': { + description: '发送群AI语音', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + character: z.string(), + text: z.string() + }), + response: baseResponseSchema.extend({ + data: z.object({ + message_id: z.string() + }) + }) + }, + '/get_ai_record': { + description: '获取AI语音', + request: z.object({ + group_id: z.string(), + character: z.string(), + text: z.string() + }), + response: baseResponseSchema.extend({ + data: z.string() + }) + } +} as const + +export default oneBotHttpApiGroup diff --git a/src/const/ob_api/index.ts b/src/const/ob_api/index.ts new file mode 100644 index 0000000..e9c2fc6 --- /dev/null +++ b/src/const/ob_api/index.ts @@ -0,0 +1,32 @@ +import { ZodSchema } from 'zod' +import oneBotHttpApiUser from './user' +import oneBotHttpApiMessage from './message' +import oneBotHttpApiGroup from './group' +import oneBotHttpApiSystem from './system' +type AllKey = + | keyof typeof oneBotHttpApiUser + | keyof typeof oneBotHttpApiMessage + | keyof typeof oneBotHttpApiGroup + | keyof typeof oneBotHttpApiSystem + +export type OneBotHttpApi = Record< + AllKey, + { + description?: string + request: ZodSchema + response: ZodSchema + } +> + +const oneBotHttpApi: OneBotHttpApi = { + ...oneBotHttpApiUser, + ...oneBotHttpApiMessage, + ...oneBotHttpApiGroup, + ...oneBotHttpApiSystem +} as const + +export type OneBotHttpApiPath = keyof OneBotHttpApi + +export type OneBotHttpApiContent = OneBotHttpApi[OneBotHttpApiPath] + +export default oneBotHttpApi diff --git a/src/const/ob_api/message/group.ts b/src/const/ob_api/message/group.ts new file mode 100644 index 0000000..d878b1d --- /dev/null +++ b/src/const/ob_api/message/group.ts @@ -0,0 +1,78 @@ +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()]), + message: z.array(messageNodeSchema) + }) + .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()]), + messages: z.array(nodeMessage), + news: z.array( + z.object({ + text: z.string() + }) + ), + prompt: z.string(), + summary: z.string(), + source: z.string() + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema + }) + }, + '/forward_group_single_msg': { + description: '消息转发到群', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + message_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema + }) + }, + '/group_poke': { + description: '发送戳一戳', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + user_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema + } +} + +export default oneBotHttpApiMessageGroup diff --git a/src/const/ob_api/message/index.ts b/src/const/ob_api/message/index.ts new file mode 100644 index 0000000..8d4ad3d --- /dev/null +++ b/src/const/ob_api/message/index.ts @@ -0,0 +1,250 @@ +import { z } from 'zod' +import oneBotHttpApiMessagePrivate from './private' +import oneBotHttpApiMessageGroup from './group' +import { baseResponseSchema, commonResponseDataSchema } from '../response' +import messageNodeSchema from './node' +const fileSchema = z.object({ + file: z.string(), + url: z.string(), + file_size: z.string(), + file_name: z.string(), + base64: z.string() +}) +const messageSchema = z.object({ + self_id: z.number(), + user_id: z.number(), + time: z.number(), + message_id: z.number(), + message_seq: z.number(), + real_id: z.number(), + message_type: z.string(), + sender: z.object({ + user_id: z.number(), + nickname: z.string(), + sex: z.enum(['male', 'female', 'unknown']), + age: z.number(), + card: z.string(), + role: z.enum(['owner', 'admin', 'member']) + }), + raw_message: z.string(), + font: z.number(), + sub_type: z.string(), + message: z.array(messageNodeSchema), + message_format: z.string(), + post_type: z.string(), + message_sent_type: z.string(), + group_id: z.number() +}) + +const oneBotHttpApiMessage = { + ...oneBotHttpApiMessagePrivate, + ...oneBotHttpApiMessageGroup, + '/mark_msg_as_read': { + description: '标记消息已读', + request: z + .object({ + group_id: z.union([z.string(), z.number()]).optional(), + user_id: z.union([z.string(), z.number()]).optional() + }) + .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()]) + }), + response: baseResponseSchema + }, + '/mark_private_msg_as_read': { + description: '标记私聊消息已读', + request: z.object({ + user_id: z.union([z.string(), z.number()]) + }), + 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()]) + }), + response: baseResponseSchema + }, + '/get_msg': { + description: '获取消息', + request: z.object({ + message_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({}) + }) + }, + '/get_image': { + description: '获取图片', + request: z.object({ + file_id: z.string() + }), + response: baseResponseSchema.extend({ + data: fileSchema + }) + }, + '/get_record': { + description: '获取语音', + request: z.object({ + file_id: z.string(), + out_format: z.enum([ + 'mp3', + 'amr', + 'wma', + 'm4a', + 'spx', + 'ogg', + 'wav', + 'flac' + ]) + }), + response: baseResponseSchema.extend({ + data: fileSchema + }) + }, + '/get_file': { + description: '获取文件', + request: z.object({ + file_id: z.string() + }), + response: baseResponseSchema.extend({ + data: fileSchema + }) + }, + '/get_group_msg_history': { + description: '获取群消息历史', + request: z.object({ + group_id: z.union([z.string(), z.number()]), + message_seq: z.union([z.string(), z.number()]), + count: z.number().int().positive(), + reverseOrder: z.boolean() + }), + response: baseResponseSchema.extend({ + data: z.object({ + messages: z.array(messageSchema) + }) + }) + }, + '/set_msg_emoji_like': { + description: '贴表情', + request: z.object({ + message_id: z.union([z.string(), z.number()]), + emoji_id: z.number(), + set: z.boolean() + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema + }) + }, + '/get_friend_msg_history': { + description: '获取好友消息历史', + request: z.object({ + user_id: z.union([z.string(), z.number()]), + message_seq: z.union([z.string(), z.number()]), + count: z.number().int().positive(), + reverseOrder: z.boolean() + }), + response: baseResponseSchema.extend({ + data: z.object({ + messages: z.array(messageSchema) + }) + }) + }, + '/get_recent_contact': { + description: '最近消息列表', + request: z.object({ + count: z.number().int().positive() + }), + response: baseResponseSchema.extend({ + data: z.array( + z.object({ + lastestMsg: messageSchema, + peerUin: z.string(), + remark: z.string(), + msgTime: z.string(), + chatType: z.number(), + msgId: z.string(), + sendNickName: z.string(), + sendMemberName: z.string(), + peerName: z.string() + }) + ) + }) + }, + '/fetch_emoji_like': { + description: '获取贴表情详情', + request: z.object({ + message_id: z.union([z.string(), z.number()]), + emojiId: z.string(), + emojiType: z.string(), + group_id: z.union([z.string(), z.number()]).optional(), + user_id: z.union([z.string(), z.number()]).optional(), + count: z.number().int().positive().optional() + }), + response: baseResponseSchema.extend({ + data: z.object({ + result: z.number(), + errMsg: z.string(), + emojiLikesList: z.array( + z.object({ + tinyId: z.string(), + nickName: z.string(), + headUrl: z.string() + }) + ), + cookie: z.string(), + isLastPage: z.boolean(), + isFirstPage: z.boolean() + }) + }) + }, + '/get_forward_msg': { + description: '获取合并转发消息', + request: z.object({ + message_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({}) + }) + }, + '/send_forward_msg': { + description: '发送合并转发消息', + request: z.object({ + group_id: z.union([z.string(), z.number()]).optional(), + user_id: z.union([z.string(), z.number()]).optional(), + messages: z.array(messageNodeSchema), + news: z.array( + z.object({ + text: z.string() + }) + ), + prompt: z.string(), + summary: z.string(), + source: z.string() + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema.extend({ + data: z.object({}) + }) + }) + } +} as const + +export default oneBotHttpApiMessage diff --git a/src/const/ob_api/message/node.ts b/src/const/ob_api/message/node.ts new file mode 100644 index 0000000..da2a73c --- /dev/null +++ b/src/const/ob_api/message/node.ts @@ -0,0 +1,93 @@ +import { z } from 'zod' + +const messageNode = z.union([ + z.object({ + type: z.literal('text'), + data: z.object({ + text: z.string() + }) + }), + z.object({ + type: z.literal('at'), + data: z.object({ + qq: z.string() + }) + }), + z.object({ + type: z.literal('image'), + data: z.object({ + file: z.string() + }) + }), + z.object({ + type: z.literal('face'), + data: z.object({ + id: z.number() + }) + }), + z.object({ + type: z.literal('json'), + data: z.object({ + data: z.string() + }) + }), + z.object({ + type: z.literal('record'), + data: z.object({ + file: z.string() + }) + }), + z.object({ + type: z.literal('video'), + data: z.object({ + file: z.string() + }) + }), + z.object({ + type: z.literal('reply'), + data: z.object({ + id: z.number() + }) + }), + 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() + }) + ]) + }), + z.object({ + type: z.literal('dice') + }), + z.object({ + type: z.literal('rps') + }), + z.object({ + type: z.literal('file'), + data: z.object({ + file: z.string() + }) + }) +]) + +export const nodeMessage = z.object({ + type: z.literal('node'), + data: z.object({ + user_id: z.string(), + nickname: z.string(), + content: z.array(messageNode) + }) +}) + +const messageNodeSchema = z.union([messageNode, nodeMessage]) + +export default messageNodeSchema diff --git a/src/const/ob_api/message/private.ts b/src/const/ob_api/message/private.ts new file mode 100644 index 0000000..202f9b7 --- /dev/null +++ b/src/const/ob_api/message/private.ts @@ -0,0 +1,77 @@ +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()]), + message: z.array(messageNodeSchema) + }) + .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()]), + messages: z.array(nodeMessage), + news: z.array( + z.object({ + text: z.string() + }) + ), + prompt: z.string(), + summary: z.string(), + source: z.string() + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema + }) + }, + '/forward_friend_single_msg': { + description: '消息转发到私聊', + request: z.object({ + user_id: z.union([z.string(), z.number()]), + message_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema + }) + }, + '/group_poke': { + description: '发送私聊戳一戳', + request: z.object({ + user_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema + } +} + +export default oneBotHttpApiMessagePrivate diff --git a/src/const/ob_api/online_status.ts b/src/const/ob_api/online_status.ts new file mode 100644 index 0000000..7da6c8d --- /dev/null +++ b/src/const/ob_api/online_status.ts @@ -0,0 +1,253 @@ +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) + }), + // Q我吧 + z.object({ + status: z.literal(60), + ext_status: z.literal(0), + battery_status: z.literal(0) + }), + // 离开 + z.object({ + status: z.literal(30), + ext_status: z.literal(0), + battery_status: z.literal(0) + }), + // 忙碌 + z.object({ + status: z.literal(50), + ext_status: z.literal(0), + battery_status: z.literal(0) + }), + // 请勿打扰 + z.object({ + status: z.literal(70), + ext_status: z.literal(0), + battery_status: z.literal(0) + }), + // 隐身 + z.object({ + status: z.literal(40), + ext_status: z.literal(0), + battery_status: z.literal(0) + }), + // 听歌中 + z.object({ + status: z.literal(10), + ext_status: z.literal(1028), + battery_status: z.literal(0) + }), + // 春日限定 + z.object({ + status: z.literal(10), + ext_status: z.literal(2037), + battery_status: z.literal(0) + }), + // 一起元梦 + z.object({ + status: z.literal(10), + ext_status: z.literal(2025), + battery_status: z.literal(0) + }), + // 求星搭子 + z.object({ + status: z.literal(10), + ext_status: z.literal(2026), + battery_status: z.literal(0) + }), + // 被掏空 + z.object({ + status: z.literal(10), + ext_status: z.literal(2014), + battery_status: z.literal(0) + }), + // 今日天气 + z.object({ + status: z.literal(10), + ext_status: z.literal(1030), + battery_status: z.literal(0) + }), + // 我crash了 + z.object({ + status: z.literal(10), + ext_status: z.literal(2019), + battery_status: z.literal(0) + }), + // 爱你 + z.object({ + status: z.literal(10), + ext_status: z.literal(2006), + battery_status: z.literal(0) + }), + // 恋爱中 + z.object({ + status: z.literal(10), + ext_status: z.literal(1051), + battery_status: z.literal(0) + }), + // 好运锦鲤 + z.object({ + status: z.literal(10), + ext_status: z.literal(1071), + battery_status: z.literal(0) + }), + // 水逆退散 + z.object({ + status: z.literal(10), + ext_status: z.literal(1201), + battery_status: z.literal(0) + }), + // 嗨到飞起 + z.object({ + status: z.literal(10), + ext_status: z.literal(1056), + battery_status: z.literal(0) + }), + // 元气满满 + z.object({ + status: z.literal(10), + ext_status: z.literal(1058), + battery_status: z.literal(0) + }), + // 宝宝认证 + z.object({ + status: z.literal(10), + ext_status: z.literal(1070), + battery_status: z.literal(0) + }), + // 一言难尽 + z.object({ + status: z.literal(10), + ext_status: z.literal(1063), + battery_status: z.literal(0) + }), + // 难得糊涂 + z.object({ + status: z.literal(10), + ext_status: z.literal(2001), + battery_status: z.literal(0) + }), + // emo中 + z.object({ + status: z.literal(10), + ext_status: z.literal(1401), + battery_status: z.literal(0) + }), + // 我太难了 + z.object({ + status: z.literal(10), + ext_status: z.literal(1062), + battery_status: z.literal(0) + }), + // 我想开了 + z.object({ + status: z.literal(10), + ext_status: z.literal(2013), + battery_status: z.literal(0) + }), + // 我没事 + z.object({ + status: z.literal(10), + ext_status: z.literal(1052), + battery_status: z.literal(0) + }), + // 想静静 + z.object({ + status: z.literal(10), + ext_status: z.literal(1061), + battery_status: z.literal(0) + }), + // 悠哉哉 + z.object({ + status: z.literal(10), + ext_status: z.literal(1059), + battery_status: z.literal(0) + }), + // 去旅行 + z.object({ + status: z.literal(10), + ext_status: z.literal(2015), + battery_status: z.literal(0) + }), + // 信号弱 + z.object({ + status: z.literal(10), + ext_status: z.literal(1011), + battery_status: z.literal(0) + }), + // 出去浪 + z.object({ + status: z.literal(10), + ext_status: z.literal(2003), + battery_status: z.literal(0) + }), + // 肝作业 + z.object({ + status: z.literal(10), + ext_status: z.literal(2012), + battery_status: z.literal(0) + }), + // 学习中 + z.object({ + status: z.literal(10), + ext_status: z.literal(1018), + battery_status: z.literal(0) + }), + // 搬砖中 + z.object({ + status: z.literal(10), + ext_status: z.literal(2023), + battery_status: z.literal(0) + }), + // 摸鱼中 + z.object({ + status: z.literal(10), + ext_status: z.literal(1300), + battery_status: z.literal(0) + }), + // 无聊中 + z.object({ + status: z.literal(10), + ext_status: z.literal(1060), + battery_status: z.literal(0) + }), + // timi中 + z.object({ + status: z.literal(10), + ext_status: z.literal(1027), + battery_status: z.literal(0) + }), + // 睡觉中 + z.object({ + status: z.literal(10), + ext_status: z.literal(1016), + battery_status: z.literal(0) + }), + // 熬夜中 + z.object({ + status: z.literal(10), + ext_status: z.literal(1032), + battery_status: z.literal(0) + }), + // 追剧中 + z.object({ + status: z.literal(10), + ext_status: z.literal(1021), + battery_status: z.literal(0) + }), + // 我的电量 + z.object({ + status: z.literal(10), + ext_status: z.literal(1000), + battery_status: z.literal(0) + }) +]) + +export default onlineStatusDataSchema diff --git a/src/const/ob_api/response.ts b/src/const/ob_api/response.ts new file mode 100644 index 0000000..0e07e74 --- /dev/null +++ b/src/const/ob_api/response.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' + +// 通用响应格式 +export const baseResponseSchema = z.object({ + status: z.enum(['ok', 'error']), // 状态 + retcode: z.number(), // 返回码 + data: z.null(), + message: z.string(), // 提示信息 + wording: z.string(), // 人性化提示 + echo: z.string() // 请求回显内容 +}) + +export const commonResponseDataSchema = z.object({ + result: z.number(), + errMsg: z.string() +}) diff --git a/src/const/ob_api/system.ts b/src/const/ob_api/system.ts new file mode 100644 index 0000000..7da2800 --- /dev/null +++ b/src/const/ob_api/system.ts @@ -0,0 +1,365 @@ +import { z } from 'zod' +import { baseResponseSchema, commonResponseDataSchema } from './response' + +const oneBotHttpApiSystem = { + '/get_online_clients': { + description: '获取当前账号在线客户端列表', + request: z.object({ + no_cache: z.boolean().optional() + }), + 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 diff --git a/src/const/ob_api/user.ts b/src/const/ob_api/user.ts new file mode 100644 index 0000000..896318c --- /dev/null +++ b/src/const/ob_api/user.ts @@ -0,0 +1,277 @@ +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 + }) + }, + '/ArkSharePeer': { + 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 二选一'), + phoneNumber: 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() + }) + }) + }, + '/ArkShareGroup': { + description: '获取推荐群聊卡片', + request: z.object({ + group_id: z.union([z.string(), z.number()]) + }), + 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() + }), + response: baseResponseSchema + }, + '/send_like': { + description: '点赞', + request: z.object({ + user_id: z.union([z.string(), z.number()]), + times: z.number() + }), + response: baseResponseSchema + }, + '/create_collection': { + description: '创建收藏', + request: z.object({ + rawData: z.string(), + brief: z.string() + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema + }) + }, + '/set_friend_add_request': { + description: '处理好友请求', + request: z.object({ + flag: z.string(), + approve: z.boolean(), + remark: z.string() + }), + response: baseResponseSchema + }, + '/set_self_longnick': { + description: '设置个性签名', + request: z.object({ + longNick: z.string() + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema + }) + }, + '/get_stranger_info': { + description: '获取账号信息', + request: z.object({ + user_id: z.union([z.string(), z.number()]) + }), + 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() + }), + 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() + }), + 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()]), + friend_id: z.union([z.string(), z.number()]), + temp_block: z.boolean(), + temp_both_del: z.boolean() + }), + response: baseResponseSchema.extend({ + data: commonResponseDataSchema + }) + }, + '/nc_get_user_status': { + description: '获取用户在线状态', + request: z.object({ + user_id: z.union([z.string(), z.number()]) + }), + response: baseResponseSchema.extend({ + data: z.object({ + status: z.number(), + ext_status: z.number() + }) + }) + } +} as const + +export default oneBotHttpApiUser diff --git a/src/pages/dashboard/debug/http/index.tsx b/src/pages/dashboard/debug/http/index.tsx index 6e6c51b..038a36f 100644 --- a/src/pages/dashboard/debug/http/index.tsx +++ b/src/pages/dashboard/debug/http/index.tsx @@ -1,17 +1,21 @@ -import { title } from '@/components/primitives' +import OneBotApiDebug from '@/components/onebot/api/debug' +import OneBotApiNavList from '@/components/onebot/api/nav_list' +import oneBotHttpApi from '@/const/ob_api' +import type { OneBotHttpApi } from '@/const/ob_api' +import { useState } from 'react' export default function HttpDebug() { + const [selectedApi, setSelectedApi] = + useState('/set_qq_profile') + const data = oneBotHttpApi[selectedApi] return ( -
-

- HTTP Debug -

-

Coming soon...

+
+ +
) } diff --git a/src/pages/web_login.tsx b/src/pages/web_login.tsx index 4e7e3a2..dd26fee 100644 --- a/src/pages/web_login.tsx +++ b/src/pages/web_login.tsx @@ -5,6 +5,7 @@ import { Image } from '@nextui-org/image' import { IoKeyOutline } from 'react-icons/io5' import { useEffect, useState } from 'react' import { toast } from 'react-hot-toast' +import type { Toast } from 'react-hot-toast' import { useLocalStorage } from '@uidotdev/usehooks' import { useNavigate } from 'react-router-dom' @@ -51,7 +52,7 @@ export default function WebLoginPage() { useEffect(() => { toast( - (t) => ( + (t: Toast) => (
遇到网络错误?
diff --git a/src/utils/url.ts b/src/utils/url.ts index fe7f4bb..7fe3186 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -1,4 +1,5 @@ import key from '@/const/key' +import { AxiosResponse } from 'axios' /** * 打开链接 @@ -23,3 +24,23 @@ export const getFullServerUrl = (path: string) => { _storeURL = JSON.parse(_storeURL) return _storeURL + path } + +/** + * 将Axios的响应转换为标准的HTTP报文 + * @param response Axios响应 + * @returns 标准的HTTP报文 + */ +export function parseAxiosResponse( + response: AxiosResponse, + http_version: string = 'HTTP/1.1' +) { + const statusLine = `${http_version} ${response.status} ${response.statusText}` + const headers = Object.entries(response.headers) + .map(([key, value]) => `${key}: ${value}`) + .join('\r\n') + const body = response.data + ? `\r\n\r\n${JSON.stringify(response.data, null, 2)}` + : '' + + return `${statusLine}\r\n${headers}${body}` +} diff --git a/src/utils/zod.ts b/src/utils/zod.ts new file mode 100644 index 0000000..6bd8ae4 --- /dev/null +++ b/src/utils/zod.ts @@ -0,0 +1,263 @@ +import { + ZodTypeAny, + ZodObject, + ZodArray, + ZodUnion, + ZodEnum, + ZodLiteral, + ZodOptional, + ZodNullable, + ZodDefault, + ZodString, + ZodNumber, + ZodBoolean, + ZodBigInt, + ZodDate, + ZodUnknown, + ZodRecord, + ZodTuple, + ZodNull, + ZodLazy, + ZodEffects, + ZodSchema +} from 'zod' + +export type LiteralValue = string | number | boolean | null + +export type ParsedSchema = { + name?: string + type: string | string[] + optional: boolean + value?: LiteralValue + enum?: LiteralValue[] + children?: ParsedSchema[] + description?: string +} + +export function parse( + schema: ZodTypeAny, + name?: string, + isRoot = true +): ParsedSchema | ParsedSchema[] { + const optional = schema.isOptional ? schema.isOptional() : false + const description = schema.description + + if (schema instanceof ZodString) { + return { name, type: 'string', optional, description } + } + + if (schema instanceof ZodNumber) { + return { name, type: 'number', optional, description } + } + + if (schema instanceof ZodBoolean) { + return { name, type: 'boolean', optional, description } + } + + if (schema instanceof ZodBigInt) { + return { name, type: 'bigint', optional, description } + } + + if (schema instanceof ZodDate) { + return { name, type: 'date', optional, description } + } + + if (schema instanceof ZodUnknown) { + return { name, type: 'unknown', optional, description } + } + + if (schema instanceof ZodLiteral) { + return { + name, + type: 'value', + optional, + value: schema._def.value as LiteralValue, + description + } + } + + if (schema instanceof ZodEnum) { + return { + name, + type: 'enum', + optional, + enum: schema._def.values as LiteralValue[], + description + } + } + + if (schema instanceof ZodUnion) { + const options = schema._def.options + const parsedOptions = options.map( + (option: ZodTypeAny) => parse(option, undefined, false) as ParsedSchema + ) + + const basicTypes = [ + 'string', + 'number', + 'boolean', + 'bigint', + 'date', + 'unknown', + 'value', + 'enum' + ] + const optionTypes: (string | string[])[] = parsedOptions.map( + (option: ParsedSchema) => option.type + ) + const isAllBasicTypes = optionTypes.every((type) => + basicTypes.includes(Array.isArray(type) ? type[0] : type) + ) + + if (isAllBasicTypes) { + const types = [ + ...new Set( + optionTypes.flatMap((type) => (Array.isArray(type) ? type[0] : type)) + ) + ] + return { name, type: types, optional, description } + } else { + return { + name, + type: 'union', + optional, + children: parsedOptions, + description + } + } + } + + if (schema instanceof ZodObject) { + const shape = schema._def.shape() + const children = Object.keys(shape).map((key) => + parse(shape[key], key, false) + ) as ParsedSchema[] + if (isRoot) { + return children + } else { + return { name, type: 'object', optional, children, description } + } + } + + if (schema instanceof ZodArray) { + const childSchema = parse( + schema._def.type, + undefined, + false + ) as ParsedSchema + return { + name, + type: 'array', + optional, + children: Array.isArray(childSchema) ? childSchema : [childSchema], + description + } + } + + if (schema instanceof ZodNullable || schema instanceof ZodDefault) { + return parse(schema._def.innerType, name) + } + + if (schema instanceof ZodOptional) { + const data = parse(schema._def.innerType, name) + if (Array.isArray(data)) { + data.forEach((item) => { + item.optional = true + item.description = description + }) + } else { + data.optional = true + data.description = description + } + return data + } + + if (schema instanceof ZodRecord) { + const valueType = parse(schema._def.valueType) as ParsedSchema + return { + name, + type: 'record', + optional, + children: [valueType], + description + } + } + + if (schema instanceof ZodTuple) { + const items: ParsedSchema[] = schema._def.items.map((item: ZodTypeAny) => + parse(item) + ) + return { name, type: 'tuple', optional, children: items, description } + } + + if (schema instanceof ZodNull) { + return { name, type: 'null', optional, description } + } + + if (schema instanceof ZodLazy) { + return parse(schema._def.getter(), name) + } + + if (schema instanceof ZodEffects) { + return parse(schema._def.schema, name) + } + + return { name, type: 'unknown', optional, description } +} + +const generateDefault = (schema: ZodSchema): unknown => { + if (schema instanceof ZodObject) { + const obj: Record = {} + for (const key in schema.shape) { + obj[key] = generateDefault(schema.shape[key]) + } + return obj + } + if (schema instanceof ZodString) { + return 'textValue' + } + if (schema instanceof ZodNumber) { + return 0 + } + if (schema instanceof ZodBoolean) { + return false + } + if (schema instanceof ZodArray) { + return [] + } + if (schema instanceof ZodUnion) { + return generateDefault(schema._def.options[0]) + } + if (schema instanceof ZodEnum) { + return schema._def.values[0] + } + if (schema instanceof ZodLiteral) { + return schema._def.value + } + if (schema instanceof ZodNullable) { + return null + } + if (schema instanceof ZodOptional) { + return generateDefault(schema._def.innerType) + } + if (schema instanceof ZodRecord) { + return {} + } + if (schema instanceof ZodTuple) { + return schema._def.items.map((item: ZodTypeAny) => generateDefault(item)) + } + if (schema instanceof ZodNull) { + return null + } + if (schema instanceof ZodLazy) { + return generateDefault(schema._def.getter()) + } + if (schema instanceof ZodEffects) { + return generateDefault(schema._def.schema) + } + return null +} + +export const generateDefaultJson = (schema: ZodSchema) => { + return JSON.stringify(generateDefault(schema), null, 2) +}