diff --git a/.github/workflows/publishNPM.yml b/.github/workflows/publishNPM.yml
index 28aad33..22b835b 100644
--- a/.github/workflows/publishNPM.yml
+++ b/.github/workflows/publishNPM.yml
@@ -21,6 +21,10 @@ jobs:
# 安装 pnpm
- name: Install pnpm
run: npm install -g pnpm
+
+ # 构建包必要文件
+ - name: Build package files
+ run: pnpm run build:cli
# 安装依赖
- name: Install Dependencies
diff --git a/README.md b/README.md
index 36e57b7..0bfc599 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
[🚢 Docker 镜像](https://hub.docker.com/repository/docker/dannicool/docker-wechatbot-webhook/general) | [📦 NPM包](https://www.npmjs.com/package/wechatbot-webhook)|[🔍 FAQ](https://github.com/danni-cool/wechatbot-webhook/issues/72)
-开箱即用的微信webhook机器人,通过 http 接口调用即可实现微信消息的发送和接收,作为基于 wechaty 的消息机器人服务在稳定性上做了较多优化。
+一个小小的微信机器人webhook,帮你抹平了很多自己开发的障碍,基于 http 请求
## ✨ Features
@@ -34,7 +34,8 @@
| 接收文件 | ✅ | |
| 接收公众号推文链接 | ✅ | |
| 接收系统通知 | ✅ 上线通知 / 掉线通知 / 异常通知 | |
-| [快捷回复](https://github.com/danni-cool/wechatbot-webhook?tab=readme-ov-file#2-%E6%94%B6%E6%B6%88%E6%81%AF-api) | ✅ | ✅ |
+| [头像获取](#33-获取静态资源接口) | ✅ | |
+| [快捷回复](#2-%E6%94%B6%E6%B6%88%E6%81%AF-api) | ✅ | ✅ |
| **<群管理>** | | |
| **<好友管理>** | | |
| 接收好友申请 | ✅ | |
@@ -410,12 +411,11 @@ curl --location 'https://your.recvdapi.com' \
#### token 配置说明
> 除了在 docker 启动时配置token,在默认缺省 token 的情况,会默认生成一个写入 `.env` 文件中
-#### `/login?token=[YOUR_PERSONAL_TOKEN]`
-
-- **描述**:获取登录二维码接口。
+#### 3.1 获取登录二维码接口
+- **地址**:`/login`
- **methods**: `GET`
- **query**: token
-
+- **example**: http://localhost:3001/login?token=[YOUR_PERSONAL_TOKEN]
**status**: `200`
##### 登录成功
@@ -430,15 +430,55 @@ curl --location 'https://your.recvdapi.com' \
展示微信登录扫码页面
-#### `/healthz?token=[YOUR_PERSONAL_TOKEN]`
+#### 3.2 健康检测接口
+
+可以主动轮询该接口,检查服务是否正常运行
-- **描述**:健康检测接口。
+- **地址**:`/healthz`
- **methods**: `GET`
- **query**: token
- **status**: `200`
+- **example**: http://localhost:3001/healthz?token=[YOUR_PERSONAL_TOKEN]
微信已登录, 返回纯文本 `healthy`,否则返回 `unHealthy`
+#### 3.3 获取静态资源接口
+
+从 2.8.0 版本开始,可以通过本接口访问到头像等静态资源,具体见 [recvd_api 数据结构示例的 avatar 字段](/docs/recvdApi.example.md#2-formdatasource-string)
+
+注意所有上报 recvd_api 的静态资源地址不会默认带上 token, 需要自己拼接,否则会返回 401 错误, 请确保自己微信已登录,需要通过登录态去获取资源
+
+- **地址**:`/resouces`
+- **methods**: `GET`
+- **query**:
+ - token: 登录token
+ - media: encode过的相对路径,比如 `/avatar/1234567890.jpg` encode为 `avatar%2F1234567890.jpg`
+- **status**: `200` `404` `401`
+
+- **example**:http://localhost:3001/resouces?media=%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxgetheadimg%3Fseq%3D83460%26username%3D%40%4086815a%26skey%3D&token=[YOUR_PERSONAL_TOKEN]
+
+##### status: `200`
+
+成功获取资源, 返回静态资源文件
+
+##### status: `404`
+
+获取资源失败
+
+##### status: `401` 未携带登录token
+
+```json
+{"success":false, "message":"Unauthorized: Access is denied due to invalid credentials."}
+```
+
+##### status: `401` 微信登录态已过期
+
+```json
+{
+ "success": false, "message": "you must login first"
+}
+```
+
## 🌟 Star History
@@ -446,7 +486,9 @@ curl --location 'https://your.recvdapi.com' \
## Contributors
-![](https://contrib.rocks/image?repo=danni-cool/wechatbot-webhook)
+Thanks to all our contributors!
+
+![](https://contrib.rocks/image?repo=danni-cool/wechatbot-webhook)
## ⏫ 更新日志
diff --git a/docs/recvdApi.example.md b/docs/recvdApi.example.md
index 64ff03e..40869ca 100644
--- a/docs/recvdApi.example.md
+++ b/docs/recvdApi.example.md
@@ -73,7 +73,7 @@
"id": "@xxxasdfsf",
"payload": {
"alias": "",
- "avatar": "",
+ "avatar": "http://localhost:3001/resouces?media=%2Fcgi-bin%2Fmmwebwx-bixxx", //请配合 token=[YOUR_PERSONAL_TOKEN] 解密
"friend": false,
"gender": 1,
"id": "@xxx",
@@ -98,7 +98,7 @@
"id": "@xxxasdfsf",
"payload": {
"alias": "",
- "avatar": "",
+ "avatar": "http://localhost:3001/resouces?media=%2Fcgi-bin%2Fmmwebwx-bixxx", //请配合 token=[YOUR_PERSONAL_TOKEN] 解密
"friend": false,
"gender": 1,
"id": "@xxx",
@@ -125,7 +125,7 @@
"id": "@xxxasdfsf",
"payload": {
"alias": "",
- "avatar": "",
+ "avatar": "http://localhost:3001/resouces?media=%2Fcgi-bin%2Fmmwebwx-bixxx", //请配合 token=[YOUR_PERSONAL_TOKEN] 解密
"friend": false,
"gender": 1,
"id": "@xxx",
@@ -166,7 +166,11 @@
"adminIdList": [],
"avatar": "xxxx", // 相对路径,应该要配合解密
"memberList": [
- {id: '@xxxx', name:'昵称', alias: '备注名'/** 个人备注名,非群备注名 */ }
+ {
+ id: '@xxxx',
+ avatar: "http://localhost:3001/resouces?media=%2Fcgi-bin%2Fmmwebwx-bixxx", //请配合 token=[YOUR_PERSONAL_TOKEN] 解密
+ name:'昵称',
+ alias: '备注名'/** 个人备注名,非群备注名 */ }
]
},
//以下暂不清楚什么用途,如有兴趣,请查阅 wechaty 官网文档
@@ -181,7 +185,7 @@
"payload": {
"alias": "", //备注名
- "avatar": "xxx",
+ "avatar": "http://localhost:3001/resouces?media=%2Fcgi-bin%2Fmmwebwx-bixxx", //请配合 token=[YOUR_PERSONAL_TOKEN] 解密
"friend": false,
"gender": 1,
"id": "@xxx",
@@ -202,7 +206,7 @@
"payload": {
"alias": "",
- "avatar": "xxx",
+ "avatar": "http://localhost:3001/resouces?media=%2Fcgi-bin%2Fmmwebwx-bixxx", //请配合 token=[YOUR_PERSONAL_TOKEN] 解密
"city": "北京",
"friend": true,
"gender": 1,
diff --git a/package.json b/package.json
index 8a32393..ceb8b6b 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"form-data": "^4.0.0",
"gerror": "^1.0.16",
"hono": "^3.11.11",
+ "lodash.clonedeep": "^4.5.0",
"log4js": "^6.9.1",
"mime": "^3.0.0",
"node-fetch-commonjs": "^3.3.2",
@@ -64,6 +65,8 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.2",
"husky": "^8.0.3",
+ "i": "^0.3.7",
+ "npm": "^10.5.0",
"prettier": "^3.1.1",
"tsc-files": "^1.1.4",
"typescript": "^5.3.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9538783..4d5c3b0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -40,6 +40,9 @@ importers:
hono:
specifier: ^3.11.11
version: 3.11.11
+ lodash.clonedeep:
+ specifier: ^4.5.0
+ version: 4.5.0
log4js:
specifier: ^6.9.1
version: 6.9.1
@@ -86,6 +89,12 @@ importers:
husky:
specifier: ^8.0.3
version: 8.0.3
+ i:
+ specifier: ^0.3.7
+ version: 0.3.7
+ npm:
+ specifier: ^10.5.0
+ version: 10.5.0
prettier:
specifier: ^3.1.1
version: 3.1.1
@@ -2147,6 +2156,11 @@ packages:
hasBin: true
dev: true
+ /i@0.3.7:
+ resolution: {integrity: sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==}
+ engines: {node: '>=0.4'}
+ dev: true
+
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@@ -2452,6 +2466,10 @@ packages:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
dev: false
+ /lodash.clonedeep@4.5.0:
+ resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
+ dev: false
+
/lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
dev: false
@@ -2631,6 +2649,83 @@ packages:
resolution: {integrity: sha512-XdkOuXGx0DTwlqb0DWTcDqelgU/F3YyZ+PTRaecpDVpkYskcnh3OeUYKfvjcRQ2D1diTIGxi/a3eHVjW5yPupQ==}
dev: false
+ /npm@10.5.0:
+ resolution: {integrity: sha512-Ejxwvfh9YnWVU2yA5FzoYLTW52vxHCz+MHrOFg9Cc8IFgF/6f5AGPAvb5WTay5DIUP1NIfN3VBZ0cLlGO0Ys+A==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+ hasBin: true
+ dev: true
+ bundledDependencies:
+ - '@isaacs/string-locale-compare'
+ - '@npmcli/arborist'
+ - '@npmcli/config'
+ - '@npmcli/fs'
+ - '@npmcli/map-workspaces'
+ - '@npmcli/package-json'
+ - '@npmcli/promise-spawn'
+ - '@npmcli/run-script'
+ - '@sigstore/tuf'
+ - abbrev
+ - archy
+ - cacache
+ - chalk
+ - ci-info
+ - cli-columns
+ - cli-table3
+ - columnify
+ - fastest-levenshtein
+ - fs-minipass
+ - glob
+ - graceful-fs
+ - hosted-git-info
+ - ini
+ - init-package-json
+ - is-cidr
+ - json-parse-even-better-errors
+ - libnpmaccess
+ - libnpmdiff
+ - libnpmexec
+ - libnpmfund
+ - libnpmhook
+ - libnpmorg
+ - libnpmpack
+ - libnpmpublish
+ - libnpmsearch
+ - libnpmteam
+ - libnpmversion
+ - make-fetch-happen
+ - minimatch
+ - minipass
+ - minipass-pipeline
+ - ms
+ - node-gyp
+ - nopt
+ - normalize-package-data
+ - npm-audit-report
+ - npm-install-checks
+ - npm-package-arg
+ - npm-pick-manifest
+ - npm-profile
+ - npm-registry-fetch
+ - npm-user-validate
+ - npmlog
+ - p-map
+ - pacote
+ - parse-conflict-json
+ - proc-log
+ - qrcode-terminal
+ - read
+ - semver
+ - spdx-expression-parse
+ - ssri
+ - supports-color
+ - tar
+ - text-table
+ - tiny-relative-date
+ - treeverse
+ - validate-npm-package-name
+ - which
+ - write-file-atomic
+
/nth-check@1.0.2:
resolution: {integrity: sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==}
dependencies:
diff --git a/src/config/const.js b/src/config/const.js
index e0ec497..d43cd72 100644
--- a/src/config/const.js
+++ b/src/config/const.js
@@ -1,11 +1,14 @@
const path = require('path')
+const { PORT } = process.env
const config = {
/**
* 上报消息的api群成员缓存多久(单位:ms)
* @type {number}
*/
- roomCachedTime: 1000 * 60 * 5
+ roomCachedTime: 1000 * 60 * 5,
+ /** 服务启动地址 */
+ localUrl: `http://localhost:${PORT}`
}
const { homeEnvCfg, homeMemoryCardPath } = process.env
diff --git a/src/middleware/loginCheck.js b/src/middleware/loginCheck.js
index d85aec3..f812d9b 100644
--- a/src/middleware/loginCheck.js
+++ b/src/middleware/loginCheck.js
@@ -8,7 +8,7 @@ module.exports.loginCheck = async (c, next) => {
c.status(401)
return c.json({
success: false,
- message: 'you must login first before sending messages'
+ message: 'you must login first'
})
}
diff --git a/src/route/index.js b/src/route/index.js
index b920fd7..5efd50d 100644
--- a/src/route/index.js
+++ b/src/route/index.js
@@ -21,4 +21,5 @@ module.exports = function registerRoute({ app, bot }) {
require('./msg')({ app, bot })
require('./login')({ app, bot })
+ require('./resouces')({ app, bot })
}
diff --git a/src/route/resouces.js b/src/route/resouces.js
new file mode 100644
index 0000000..511dc30
--- /dev/null
+++ b/src/route/resouces.js
@@ -0,0 +1,39 @@
+const { downloadFile } = require('../utils/index')
+const middleware = require('../middleware')
+/**
+ * 通过该接口代理获取微信静态资源
+ * @param {Object} param
+ * @param {import('hono').Hono} param.app
+ * @param {import('wechaty').Wechaty} param.bot
+ */
+module.exports = function registerResourceAgentRoute({ app, bot }) {
+ app.get(
+ '/resouces',
+ middleware.loginCheck,
+ /** @param {import('hono').Context} c */
+ async (c) => {
+ // 暂时不考虑其他puppet的情况
+ const cookie =
+ // @ts-ignore 私有变量
+ bot.__puppet._memory.payload['\rpuppet\nPUPPET-WECHAT4U'].COOKIE
+ const mediaUrl = c.req.query('media')
+ const fullResouceUrl = `https://wx2.qq.com${decodeURIComponent(
+ mediaUrl || ''
+ )}`
+
+ const { buffer, contentType } = await downloadFile(fullResouceUrl, {
+ Cookie: Object.entries(cookie).reduce(
+ (pre, next) => (pre += `${next[0]}=${next[1]};`),
+ ''
+ )
+ })
+ if (buffer) {
+ contentType && c.header('Content-Type', contentType)
+ return c.body(buffer)
+ } else {
+ c.status(404)
+ return c.json({ success: false, message: '获取资源失败' })
+ }
+ }
+ )
+}
diff --git a/src/service/msgUploader.js b/src/service/msgUploader.js
index 41bf9a7..f71d431 100644
--- a/src/service/msgUploader.js
+++ b/src/service/msgUploader.js
@@ -5,7 +5,7 @@ const FormData = require('form-data')
const { LOCAL_RECVD_MSG_API, RECVD_MSG_API } = process.env
const { MSG_TYPE_ENUM } = require('../config/const')
const cacheTool = require('../service/cache')
-
+const cloneDeep = require('lodash.clonedeep')
/**
* 收到消息上报接受url
* @typedef {{type:'text'|'fileUrl'}} baseMsgInterface
@@ -39,7 +39,7 @@ async function sendMsg2RecvdApi(msg) {
// 有webhookurl才发送
if (!webhookUrl) return
- /** @type {import('wechaty/impls').RoomInterface & {payload: { memberList: {id:string,name:string,alias:string|undefined}[]}}} */
+ /** @type {roomInfoForUpload} */
//@ts-expect-errors 强制as配合 ts-expect-errors 实用更佳
const roomInfo = msg.room()
@@ -59,6 +59,8 @@ async function sendMsg2RecvdApi(msg) {
})
}
roomInfo.payload.memberList = roomMemberInfo.map((item) => ({
+ // @ts-expect-error wechaty定义问题,数据在payload里
+ avatar: Utils.getAssetsAgentUrl(item.payload.avatar),
// @ts-expect-error wechaty定义问题,数据在payload里
id: item.payload.id,
// @ts-expect-error wechaty定义问题,数据在payload里
@@ -74,11 +76,32 @@ async function sendMsg2RecvdApi(msg) {
}
const source = {
- /** room的话解析群成员信息,原始信息不会带 */
- room: roomInfo ?? '',
- to: msg.to() ?? '',
+ room: cloneDeep(roomInfo || {}),
+ /** @type { import('wechaty').Message['to'] } */
+ // @ts-ignore
+ to: cloneDeep(msg.to() || {}),
+ from: cloneDeep(msg.talker() || {})
+ }
+
+ // @ts-ignore
+ if (source.to && source.to.payload?.avatar) {
+ // @ts-ignore
+ source.to.payload.avatar = Utils.getAssetsAgentUrl(source.to.payload.avatar)
+ }
+
+ // @ts-ignore
+ if (source.from.payload?.avatar) {
// @ts-ignore
- from: msg.talker() ?? ''
+ source.from.payload.avatar = Utils.getAssetsAgentUrl(
+ // @ts-ignore
+ source.from.payload.avatar
+ )
+ }
+
+ if (source.room.payload?.avatar) {
+ source.room.payload.avatar = Utils.getAssetsAgentUrl(
+ source.room.payload.avatar
+ )
}
// let passed = true
diff --git a/src/utils/index.js b/src/utils/index.js
index 826afbc..9f4e19f 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -2,31 +2,34 @@ const { FileBox } = require('file-box')
const MIME = require('mime')
const { logger } = require('./log')
const { URL } = require('url')
+
/**
* 下载媒体文件转化为Buffer
* @param {string} fileUrl
- * @returns {Promise<{buffer?: Buffer, fileName?: string, fileNameAlias?: string}>}
+ * @returns {Promise<{buffer?: Buffer, fileName?: string, fileNameAlias?: string, contentType?: null | string}>}
*/
-const downloadFile = async (fileUrl) => {
+const downloadFile = async (fileUrl, headers = {}) => {
try {
- const response = await fetch(fileUrl)
+ const response = await fetch(fileUrl, { headers })
if (response.ok) {
const buffer = Buffer.from(await response.arrayBuffer())
// 使用自定义文件名,解决URL无文件后缀名时,文件被微信解析成不正确的后缀问题
let { fileName, query } = getFileInfoFromUrl(fileUrl)
+ let contentType = response.headers.get('content-type')
// deal with unValid Url format like https://pangji-home.com/Fi5DimeGHBLQ3KcELn3DolvENjVU
if (fileName === '') {
// 有些资源文件链接是不会返回文件后缀的 例如 https://pangji-home.com/Fi5DimeGHBLQ3KcELn3DolvENjVU 其实是一张图片
//@ts-expect-errors 不考虑无content-type的情况
- const extName = MIME.getExtension(response.headers.get('content-type'))
+ const extName = MIME.getExtension(contentType)
fileName = `${Date.now()}.${extName}`
}
return {
buffer,
fileName,
+ contentType,
fileNameAlias: query?.$alias
}
}
@@ -194,6 +197,8 @@ module.exports = {
...require('./nextTick.js'),
...require('./paramsValid.js'),
...require('./log.js'),
+ ...require('./res'),
+ downloadFile,
getMediaFromUrl,
getBufferFile,
generateToken,
diff --git a/src/utils/msg.js b/src/utils/msg.js
index ad54b0f..3203427 100644
--- a/src/utils/msg.js
+++ b/src/utils/msg.js
@@ -1,4 +1,6 @@
const { MSG_TYPE_ENUM, legacySystemMsgStrMap } = require('../config/const')
+const { getAssetsAgentUrl } = require('./res')
+const cloneDeep = require('lodash.clonedeep')
class CommonMsg {
/**
* @param {commonMsgPayload} payload
@@ -108,8 +110,15 @@ class SystemEvent extends CommonMsg {
* @param {systemEventPayload} payload
*/
constructor(payload) {
+ const payloadClone = cloneDeep(payload)
+ if (payloadClone.user?.payload) {
+ payloadClone.user.payload.avatar = getAssetsAgentUrl(
+ payloadClone.user.payload.avatar
+ )
+ }
+
super({
- text: JSON.stringify(payload),
+ text: JSON.stringify(payloadClone),
type: legacySystemMsgStrMap[payload.event],
isSystemEvent: true
})
diff --git a/src/utils/res.js b/src/utils/res.js
new file mode 100644
index 0000000..fff957f
--- /dev/null
+++ b/src/utils/res.js
@@ -0,0 +1,14 @@
+const {
+ config: { localUrl }
+} = require('../config/const')
+
+/**
+ * 将相对资源路径转为代理获取资源路径
+ * @param {string} relativePath
+ * @returns {string}
+ */
+module.exports.getAssetsAgentUrl = (relativePath) => {
+ if (!relativePath) return ''
+
+ return `${localUrl}/resouces?media=${encodeURIComponent(relativePath)}`
+}
diff --git a/src/wechaty/init.js b/src/wechaty/init.js
index 1d090dc..504673e 100644
--- a/src/wechaty/init.js
+++ b/src/wechaty/init.js
@@ -4,8 +4,11 @@ const { SystemEvent } = require('../utils/msg.js')
const Service = require('../service')
const Utils = require('../utils/index')
const chalk = require('chalk')
-const { PORT } = process.env
-const { memoryCardName, logOutUnofficialCodeList } = require('../config/const')
+const {
+ memoryCardName,
+ logOutUnofficialCodeList,
+ config: { localUrl }
+} = require('../config/const')
const token = Service.initLoginApiToken()
const cacheTool = require('../service/cache')
const bot =
@@ -31,7 +34,7 @@ module.exports = function init() {
Utils.logger.info(
[
'Or Access the URL to login: ' +
- chalk.cyan(`http://localhost:${PORT}/login?token=${token}`)
+ chalk.cyan(`${localUrl}/login?token=${token}`)
].join('\n')
)
})
@@ -42,7 +45,7 @@ module.exports = function init() {
Utils.logger.info(
'💬 ' +
`你的推消息 api 为:${chalk.cyan(
- `http://localhost:${PORT}/webhook/msg/v2?token=${token}`
+ `${localUrl}/webhook/msg/v2?token=${token}`
)}`
)
Utils.logger.info(
diff --git a/typings/global.d.ts b/typings/global.d.ts
index 96bc4be..402e37f 100644
--- a/typings/global.d.ts
+++ b/typings/global.d.ts
@@ -150,3 +150,14 @@ type commonMsgPayload = {
room?: import('wechaty').Room | ''
file?: string | (import('file-box').FileBoxInterface & { _name: string })
}
+
+type roomInfoForUpload = import('wechaty/impls').RoomInterface & {
+ payload: {
+ memberList: {
+ id: string
+ name: string
+ alias: string | undefined
+ avatar: string
+ }[]
+ }
+}
diff --git a/typings/extend.d.ts b/typings/hono.d.ts
similarity index 100%
rename from typings/extend.d.ts
rename to typings/hono.d.ts
diff --git a/typings/lodash.d.ts b/typings/lodash.d.ts
new file mode 100644
index 0000000..d405925
--- /dev/null
+++ b/typings/lodash.d.ts
@@ -0,0 +1,6 @@
+declare module 'lodash.clonedeep' {
+ // 定义 cloneDeep 函数,接受任意类型的参数,并返回相同的类型
+ function cloneDeep(value: T): T
+ // commonjs 导出
+ export = cloneDeep
+}