Compare commits
No commits in common. "6d1f97f202beb5e5e077d244c27d4991195a2c1a" and "7ae07a6fcb9030fb2fc6dede554aff5f2bc18ec9" have entirely different histories.
6d1f97f202
...
7ae07a6fcb
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,4 +5,3 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
*.sqlite3
|
|
||||||
|
|||||||
136
README.md
136
README.md
@ -1,24 +1,19 @@
|
|||||||
## API Proxy Mock
|
## API Proxy Mock
|
||||||
|
|
||||||
基于 Node.js 的轻量级 **HTTP 代理 + 本地 Mock**:所有配置(路由映射、服务器参数、接口列表)统一存储在 SQLite,mock 内容保存在 `mock/` 目录文件中,其余请求按配置(HTTP/HTTPS)转发到真实后端。通过 `/__admin` 管理面板进行可视化管理。
|
基于 Node.js 的轻量级 **HTTP 代理 + 本地 Mock**:在 `config.json` 里配置要拦截的路径和本地文件,其余请求按配置(HTTP/HTTPS)转发到真实后端。
|
||||||
|
|
||||||
### 功能说明
|
### 功能说明
|
||||||
|
|
||||||
- **代理转发**:未命中 Mock 的请求会转发到 `config.targetHost`(由 `targetHttps` 决定 HTTP/HTTPS,`targetPort` 可配)。
|
- **代理转发**:未命中 Mock 的请求会转发到 `config.targetHost`(由 `targetHttps` 决定 HTTP/HTTPS,`targetPort` 可配)。
|
||||||
- **本地 Mock**:命中路由时直接读取 `mock/` 目录下的文件作为响应体。
|
- **本地 Mock**:命中路由时直接读取 `mock` 目录下的文件作为响应体。
|
||||||
- **SQLite 存储**:路由列表、接口列表、服务器配置全部存储在 `data/mock-mappings.sqlite3`。
|
|
||||||
- **Mock 总开关**:`mockEnabled` 为 `false` 时**不拦截**任何 Mock 路由,全部走代理。
|
- **Mock 总开关**:`mockEnabled` 为 `false` 时**不拦截**任何 Mock 路由,全部走代理。
|
||||||
|
- **热更新**:`reloadOnChange` 为 `true` 时监听 `config.json` 变更并自动重载;也可通过管理接口手动重载。
|
||||||
- **Mock 响应**:带简单 CORS 头,以及 `X-Mock-Source`、`X-Mock-Timestamp` 便于排查。
|
- **Mock 响应**:带简单 CORS 头,以及 `X-Mock-Source`、`X-Mock-Timestamp` 便于排查。
|
||||||
- **管理面板**:访问 `/__admin` 进入 Element UI 可视化管理界面,支持以下功能:
|
|
||||||
- **路由配置**:增删改查路由映射,启用/禁用单条路由,已启用路由自动置顶排序。
|
|
||||||
- **Mock 数据配置**:新增/编辑/删除 Mock 文件,`mock/` 目录固定,只需输入文件名和后缀。
|
|
||||||
- **接口管理**:增删改查预置接口列表,支持 JSON 数组批量导入。
|
|
||||||
- **基础配置**:Mock 开关、默认 Content-Type(下拉选择)、代理端口(修改需重启)、目标主机/端口/HTTPS。
|
|
||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
- Node.js(建议 18+)
|
- Node.js(建议 18+)
|
||||||
- 依赖见 `package.json`:`typescript`、`ts-node`、`@types/node`(仅开发/类型)、`sqlite3`
|
- 依赖见 `package.json`:`typescript`、`ts-node`、`@types/node`(仅开发/类型)
|
||||||
|
|
||||||
### 安装与启动
|
### 安装与启动
|
||||||
|
|
||||||
@ -26,7 +21,7 @@
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
推荐使用 npm 脚本:
|
推荐使用 npm 脚本(使用项目内 `tsconfig.json`,避免 `tsc .\某文件.ts` 触发 TS5112):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
@ -46,38 +41,63 @@ npx ts-node --project tsconfig.json ./index.api.ts
|
|||||||
npm run typecheck
|
npm run typecheck
|
||||||
```
|
```
|
||||||
|
|
||||||
启动成功后,控制台会输出本地监听地址、目标主机等。
|
启动成功后,控制台会输出本地监听地址、目标主机与配置文件路径等。
|
||||||
|
|
||||||
### 数据存储
|
### 配置文件 `config.json`
|
||||||
|
|
||||||
所有配置统一存储在 SQLite 数据库 `data/mock-mappings.sqlite3` 中:
|
与 `index.api.ts` 同目录。若首次运行不存在,程序会生成一份默认配置。
|
||||||
|
|
||||||
| 表名 | 用途 |
|
基础结构示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"routes": {
|
||||||
|
"/api/example": "mock/example.txt"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"mockEnabled": true,
|
||||||
|
"cacheConfig": true,
|
||||||
|
"reloadOnChange": true,
|
||||||
|
"defaultContentType": "application/json",
|
||||||
|
"proxyPort": 8877,
|
||||||
|
"targetHost": "example.com",
|
||||||
|
"targetPort": 443,
|
||||||
|
"targetHttps": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `routes`
|
||||||
|
|
||||||
|
| 说明 | |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `route_mappings` | 路由 → mock 文件映射 + 状态码 + 启用状态 + 接口名称 |
|
| **key** | 请求路径,只匹配 **pathname**(不含域名;查询串不参与匹配),例如 `"/api/user/info"`。 |
|
||||||
| `api_list` | 预置接口清单(名称 + 路径) |
|
| **value** | 相对项目根目录(与 `index.api.ts` 同级)的文件路径,例如 `"mock/user.txt"`。 |
|
||||||
| `mock_files` | mock 文件路径 + 别名 |
|
| **注释** | 以 **`#`** 开头的 key 视为注释,**不参与** Mock。例如 `"#/api/old": "mock/x.txt"` 会被忽略。 |
|
||||||
| `server_config` | 服务器配置(端口、目标主机、Content-Type 等) |
|
|
||||||
|
|
||||||
首次启动时,如果 `mock_files` 表为空,会自动扫描 `mock/` 目录下的文件并导入数据库。
|
#### `config`
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `mockEnabled` | 可选。为 `false` 时关闭 Mock,所有请求走代理;缺省为 `true`。 |
|
||||||
|
| `cacheConfig` | 保留字段;当前实现中未参与逻辑,可忽略或与旧配置兼容。 |
|
||||||
|
| `reloadOnChange` | 是否监视 `config.json` 文件变化并自动重新加载。 |
|
||||||
|
| `defaultContentType` | Mock 成功时的 `Content-Type`,常用 `"application/json"`。 |
|
||||||
|
| `proxyPort` | 本机 HTTP 代理监听端口。 |
|
||||||
|
| `targetHost` | 上游主机名(不含协议与路径)。 |
|
||||||
|
| `targetPort` | 可选。上游端口;当 `targetHttps=true` 缺省为 `443`,当 `targetHttps=false` 缺省为 `80`。 |
|
||||||
|
| `targetHttps` | 可选。是否使用 HTTPS 连接上游;`true` 为 HTTPS,`false` 为 HTTP。缺省为 `true`。 |
|
||||||
|
|
||||||
### 管理接口
|
### 管理接口
|
||||||
|
|
||||||
将 `<proxyPort>` 换为实际监听端口:
|
将 `<proxyPort>` 换为 `config.proxyPort` 中的值:
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `GET` | `http://localhost:<proxyPort>/__config` | 查看当前路由与配置 |
|
| `GET` | `http://localhost:<proxyPort>/__config` | 查看当前路由与配置 |
|
||||||
| `POST` | `http://localhost:<proxyPort>/__config` | 保存配置(服务器配置 + 路由) |
|
| `POST` | `http://localhost:<proxyPort>/__config` | 保存 `config.json`(包含 `routes` 与 `config`) |
|
||||||
| `POST` | `http://localhost:<proxyPort>/__reload-config` | 从数据库重新加载配置 |
|
| `POST` | `http://localhost:<proxyPort>/__reload-config` | 手动重新加载 `config.json` |
|
||||||
| `POST` | `http://localhost:<proxyPort>/__routes` | 新增/编辑路由映射 |
|
| `POST` | `http://localhost:<proxyPort>/__routes` | 动态新增单个路由与 mock 文件 |
|
||||||
| `DELETE` | `http://localhost:<proxyPort>/__routes` | 删除路由映射 |
|
|
||||||
| `GET` | `http://localhost:<proxyPort>/__api-list` | 获取预置接口列表 |
|
|
||||||
| `POST` | `http://localhost:<proxyPort>/__api-list` | 新增/编辑预置接口 |
|
|
||||||
| `DELETE` | `http://localhost:<proxyPort>/__api-list` | 删除预置接口 |
|
|
||||||
| `GET` | `http://localhost:<proxyPort>/__mock-files` | 获取 mock 文件列表 |
|
|
||||||
| `POST` | `http://localhost:<proxyPort>/__mock-files` | 创建/更新 mock 文件 |
|
|
||||||
| `DELETE` | `http://localhost:<proxyPort>/__mock-files` | 删除 mock 文件 |
|
|
||||||
| `GET` | `http://localhost:<proxyPort>/__admin` | 配置管理页面(Element UI) |
|
| `GET` | `http://localhost:<proxyPort>/__admin` | 配置管理页面(Element UI) |
|
||||||
|
|
||||||
#### `POST /__routes` 请求示例
|
#### `POST /__routes` 请求示例
|
||||||
@ -86,62 +106,18 @@ npm run typecheck
|
|||||||
{
|
{
|
||||||
"route": "/api/new/mock",
|
"route": "/api/new/mock",
|
||||||
"filePath": "mock/new-api.json",
|
"filePath": "mock/new-api.json",
|
||||||
"apiName": "新接口",
|
"fileContent": "{\"code\":0,\"message\":\"ok\"}",
|
||||||
"statusCode": 200,
|
"overwrite": false
|
||||||
"enabled": true,
|
|
||||||
"useExistingFile": true
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `POST /__config` 请求示例
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"proxyPort": 8879,
|
|
||||||
"targetHost": "192.168.3.9",
|
|
||||||
"targetPort": 8092,
|
|
||||||
"targetHttps": false,
|
|
||||||
"defaultContentType": "application/json",
|
|
||||||
"mockEnabled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 批量导入接口示例
|
|
||||||
|
|
||||||
通过管理面板的「批量导入」按钮,输入以下格式的 JSON 数组即可批量添加预置接口:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "route": "/api/login", "name": "登录接口" },
|
|
||||||
{ "route": "/api/user/info", "name": "获取用户信息" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### TypeScript 与编译说明
|
### TypeScript 与编译说明
|
||||||
|
|
||||||
- 项目根目录包含 `tsconfig.json`,请使用 **`tsc -p .`** 或 **`npm run typecheck`** 做整项目检查。
|
- 项目根目录包含 `tsconfig.json`,请使用 **`tsc -p .`** 或 **`npm run typecheck`** 做整项目检查。
|
||||||
- **不要**使用 `tsc .\index.api.ts` 这类「命令行附带单个文件」的方式,否则会与 `tsconfig.json` 冲突并报 **TS5112**。
|
- **不要**使用 `tsc .\index.api.ts` 这类「命令行附带单个文件」的方式,否则会与 `tsconfig.json` 冲突并报 **TS5112**。
|
||||||
|
|
||||||
### 目录结构
|
### 目录说明
|
||||||
|
|
||||||
```
|
- `index.api.ts`:服务入口。
|
||||||
├── index.api.ts # 服务入口
|
- `config.json`:路由与运行参数。
|
||||||
├── admin.html # 管理面板(Vue 2 + Element UI 单文件)
|
- `mock/`:Mock 响应文件(文本内容原样返回,按需自行写成 JSON 等)。
|
||||||
├── src/
|
|
||||||
│ ├── types.ts # TypeScript 类型定义
|
|
||||||
│ ├── constants.ts # 路径常量(MOCK_DIR、DB_FILE 等)
|
|
||||||
│ ├── state.ts # 运行时内存状态
|
|
||||||
│ ├── db.ts # SQLite 数据库 CRUD
|
|
||||||
│ ├── config.ts # 配置加载/保存
|
|
||||||
│ ├── proxy.ts # HTTP 服务 + 代理转发 + Mock 响应
|
|
||||||
│ ├── admin-handlers.ts # 管理接口处理
|
|
||||||
│ ├── mock-files.ts # Mock 文件加载与管理
|
|
||||||
│ ├── route-matching.ts # 路由匹配逻辑
|
|
||||||
│ ├── api-list.ts # 预置接口列表管理
|
|
||||||
│ └── utils.ts # 工具函数
|
|
||||||
├── mock/ # Mock 响应文件(文本原样返回)
|
|
||||||
└── data/
|
|
||||||
└── mock-mappings.sqlite3 # SQLite 数据库
|
|
||||||
```
|
|
||||||
|
|||||||
546
admin.html
546
admin.html
@ -14,7 +14,7 @@
|
|||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
max-width: 1320px;
|
max-width: 1200px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
padding: 0 16px 24px;
|
padding: 0 16px 24px;
|
||||||
}
|
}
|
||||||
@ -30,15 +30,18 @@
|
|||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
.top-nav-inner {
|
.top-nav-inner {
|
||||||
max-width: 1320px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 14px 16px;
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.top-nav-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
}
|
||||||
.small-text {
|
.small-text {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
@ -48,16 +51,17 @@
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
.route-table .el-table__cell .cell {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="top-nav">
|
<div class="top-nav">
|
||||||
<div class="top-nav-inner">
|
<div class="top-nav-inner">
|
||||||
<span class="top-nav-title">企业级数据Mock管理系统</span>
|
<div class="actions">
|
||||||
|
<el-button type="primary" @click="saveConfig">保存配置到 config.json</el-button>
|
||||||
|
<el-button @click="reloadConfig">重载服务内存配置</el-button>
|
||||||
|
<el-button @click="loadConfig">刷新页面数据</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -66,38 +70,30 @@
|
|||||||
<el-card class="section-card">
|
<el-card class="section-card">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||||
<strong>路由配置(routes)</strong>
|
<strong>路由配置(routes)</strong>
|
||||||
<div style="display:flex;gap:8px;align-items:center;">
|
|
||||||
<el-input size="small" v-model="routeSearch" placeholder="搜索接口名称或路径" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input>
|
|
||||||
<el-button size="mini" type="primary" @click="openRouteDialogForCreate">新增路由</el-button>
|
<el-button size="mini" type="primary" @click="openRouteDialogForCreate">新增路由</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<el-table :data="pagedRoutes" border>
|
||||||
<el-table :data="pagedRoutes" border class="route-table" style="width: 100%;">
|
<el-table-column label="接口名称" min-width="220">
|
||||||
<el-table-column label="接口名称" min-width="180" show-overflow-tooltip>
|
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ scope.row.apiName || "-" }}</span>
|
<span>{{ scope.row.apiName || "-" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="请求路径" min-width="220" show-overflow-tooltip>
|
<el-table-column label="请求路径" min-width="260">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ scope.row.route }}</span>
|
<span>{{ scope.row.route }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="Mock 文件路径" min-width="240" show-overflow-tooltip>
|
<el-table-column label="Mock 文件路径" min-width="300">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ scope.row.filePath }}</span>
|
<span>{{ scope.row.filePath }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="返回状态码" width="120" align="center">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span>{{ scope.row.statusCode || 200 }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="启用" width="90" align="center">
|
<el-table-column label="启用" width="90" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-switch v-model="scope.row.enabled" @change="saveRoutes"></el-switch>
|
<el-switch v-model="scope.row.enabled"></el-switch>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="160">
|
<el-table-column label="操作" width="180">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
@ -117,9 +113,12 @@
|
|||||||
layout="total, prev, pager, next"
|
layout="total, prev, pager, next"
|
||||||
:current-page="routePagination.currentPage"
|
:current-page="routePagination.currentPage"
|
||||||
:page-size="routePagination.pageSize"
|
:page-size="routePagination.pageSize"
|
||||||
:total="filteredRoutes.length"
|
:total="form.routes.length"
|
||||||
@current-change="handleRoutePageChange"
|
@current-change="handleRoutePageChange"
|
||||||
></el-pagination>
|
></el-pagination>
|
||||||
|
<div class="small-text" style="margin-top:8px;">
|
||||||
|
说明:关闭“启用”后会以 # 注释路由,不参与 mock 命中;切换后请点击“保存配置到 config.json”生效到文件。
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
@ -127,17 +126,9 @@
|
|||||||
<el-card class="section-card">
|
<el-card class="section-card">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||||
<strong>Mock 数据配置</strong>
|
<strong>Mock 数据配置</strong>
|
||||||
<div style="display:flex;gap:8px;align-items:center;">
|
|
||||||
<el-input size="small" v-model="mockFileSearch" placeholder="搜索别名或路径" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input>
|
|
||||||
<el-button size="mini" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button>
|
<el-button size="mini" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<el-table :data="pagedMockFiles" border>
|
<el-table :data="pagedMockFiles" border>
|
||||||
<el-table-column label="别名" min-width="180">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span>{{ scope.row.alias || "-" }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="Mock 文件路径" min-width="320">
|
<el-table-column label="Mock 文件路径" min-width="320">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ scope.row.filePath }}</span>
|
<span>{{ scope.row.filePath }}</span>
|
||||||
@ -161,79 +152,29 @@
|
|||||||
layout="total, prev, pager, next"
|
layout="total, prev, pager, next"
|
||||||
:current-page="mockFilePagination.currentPage"
|
:current-page="mockFilePagination.currentPage"
|
||||||
:page-size="mockFilePagination.pageSize"
|
:page-size="mockFilePagination.pageSize"
|
||||||
:total="filteredMockFiles.length"
|
:total="mockFiles.length"
|
||||||
@current-change="handleMockFilePageChange"
|
@current-change="handleMockFilePageChange"
|
||||||
></el-pagination>
|
></el-pagination>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="接口管理" name="api">
|
|
||||||
<el-card class="section-card">
|
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
|
||||||
<strong>接口列表</strong>
|
|
||||||
<div style="display:flex;gap:8px;align-items:center;">
|
|
||||||
<el-input size="small" v-model="apiSearch" placeholder="搜索接口名称或路径" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input>
|
|
||||||
<el-button size="mini" type="primary" @click="openApiDialogForCreate">新增接口</el-button>
|
|
||||||
<el-button size="mini" type="success" @click="openBatchImportDialog">批量导入</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<el-table :data="pagedApiList" border style="width: 100%;">
|
|
||||||
<el-table-column label="接口名称" min-width="200">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span>{{ scope.row.name }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="请求路径" min-width="300">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span>{{ scope.row.route }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="160">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button size="mini" type="primary" plain @click="openApiDialogForEdit(scope.$index)">修改</el-button>
|
|
||||||
<el-button size="mini" type="danger" @click="removeApiItem(scope.$index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<el-pagination
|
|
||||||
class="table-pagination"
|
|
||||||
background
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
:current-page="apiPagination.currentPage"
|
|
||||||
:page-size="apiPagination.pageSize"
|
|
||||||
:total="filteredApiList.length"
|
|
||||||
@current-change="handleApiPageChange"
|
|
||||||
></el-pagination>
|
|
||||||
</el-card>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="基础配置" name="basic">
|
<el-tab-pane label="基础配置" name="basic">
|
||||||
<el-card class="section-card">
|
<el-card class="section-card">
|
||||||
<el-form :model="form.config" label-width="180px">
|
<el-form :model="form.config" label-width="180px">
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="Mock 开关">
|
<el-form-item label="Mock 开关">
|
||||||
<el-switch v-model="form.config.mockEnabled" @change="saveServerConfig"></el-switch>
|
<el-switch v-model="form.config.mockEnabled"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="自动重载配置文件">
|
||||||
|
<el-switch v-model="form.config.reloadOnChange"></el-switch>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="默认响应 Content-Type">
|
<el-form-item label="默认响应 Content-Type">
|
||||||
<el-select
|
<el-input v-model="form.config.defaultContentType"></el-input>
|
||||||
v-model="form.config.defaultContentType"
|
|
||||||
filterable
|
|
||||||
allow-create
|
|
||||||
default-first-option
|
|
||||||
placeholder="请选择"
|
|
||||||
@change="saveServerConfig"
|
|
||||||
style="width: 100%;"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in contentTypeOptions"
|
|
||||||
:key="item"
|
|
||||||
:label="item"
|
|
||||||
:value="item"
|
|
||||||
></el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@ -242,16 +183,12 @@
|
|||||||
v-model="form.config.proxyPort"
|
v-model="form.config.proxyPort"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="65535"
|
:max="65535"
|
||||||
@change="saveServerConfig"
|
|
||||||
></el-input-number>
|
></el-input-number>
|
||||||
<div style="color:#E6A23C;font-size:12px;margin-top:4px;">
|
|
||||||
<i class="el-icon-warning"></i> 修改端口后需重启应用才能生效
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="目标主机">
|
<el-form-item label="目标主机">
|
||||||
<el-input v-model="form.config.targetHost" @blur="saveServerConfig"></el-input>
|
<el-input v-model="form.config.targetHost"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@ -260,13 +197,12 @@
|
|||||||
v-model="form.config.targetPort"
|
v-model="form.config.targetPort"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="65535"
|
:max="65535"
|
||||||
@change="saveServerConfig"
|
|
||||||
></el-input-number>
|
></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="目标 HTTPS">
|
<el-form-item label="目标 HTTPS">
|
||||||
<el-switch v-model="form.config.targetHttps" @change="saveServerConfig"></el-switch>
|
<el-switch v-model="form.config.targetHttps"></el-switch>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@ -322,29 +258,11 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="item in mockFiles"
|
v-for="item in mockFiles"
|
||||||
:key="item.filePath"
|
:key="item.filePath"
|
||||||
:label="item.alias ? item.alias + ' (' + item.filePath + ')' : item.filePath"
|
:label="item.filePath"
|
||||||
:value="item.filePath"
|
:value="item.filePath"
|
||||||
></el-option>
|
></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="返回状态码">
|
|
||||||
<el-select
|
|
||||||
v-model="routeDialog.form.statusCode"
|
|
||||||
filterable
|
|
||||||
allow-create
|
|
||||||
default-first-option
|
|
||||||
clearable
|
|
||||||
placeholder="先选常用状态码,也可直接输入"
|
|
||||||
style="width: 100%;"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in commonStatusCodes"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
></el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="是否启用 Mock">
|
<el-form-item label="是否启用 Mock">
|
||||||
<el-switch v-model="routeDialog.form.enabled"></el-switch>
|
<el-switch v-model="routeDialog.form.enabled"></el-switch>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -363,20 +281,12 @@
|
|||||||
width="760px"
|
width="760px"
|
||||||
>
|
>
|
||||||
<el-form :model="mockFileDialog.form" label-width="180px">
|
<el-form :model="mockFileDialog.form" label-width="180px">
|
||||||
<el-form-item label="别名">
|
|
||||||
<el-input
|
|
||||||
v-model="mockFileDialog.form.alias"
|
|
||||||
placeholder="可选:用于展示,支持任意文本"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Mock 文件路径">
|
<el-form-item label="Mock 文件路径">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="mockFileDialog.form.fileName"
|
v-model="mockFileDialog.form.filePath"
|
||||||
placeholder="请输入文件名,如 test123.json"
|
placeholder="mock/new-api.json"
|
||||||
:disabled="mockFileDialog.mode === 'edit'"
|
:disabled="mockFileDialog.mode === 'edit'"
|
||||||
>
|
></el-input>
|
||||||
<template slot="prepend">mock/</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="文件内容">
|
<el-form-item label="文件内容">
|
||||||
<el-input
|
<el-input
|
||||||
@ -392,49 +302,6 @@
|
|||||||
<el-button type="primary" @click="submitMockFileDialog">保存 Mock 文件</el-button>
|
<el-button type="primary" @click="submitMockFileDialog">保存 Mock 文件</el-button>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
:title="apiDialog.mode === 'edit' ? '修改接口' : '新增接口'"
|
|
||||||
:visible.sync="apiDialog.visible"
|
|
||||||
width="500px"
|
|
||||||
>
|
|
||||||
<el-form :model="apiDialog.form" label-width="80px">
|
|
||||||
<el-form-item label="接口名称">
|
|
||||||
<el-input v-model="apiDialog.form.name" placeholder="如:获取用户信息"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="请求路径">
|
|
||||||
<el-input v-model="apiDialog.form.route" placeholder="如:/api2/user/info"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<span slot="footer" class="dialog-footer">
|
|
||||||
<el-button @click="apiDialog.visible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitApiDialog">确定</el-button>
|
|
||||||
</span>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
title="批量导入接口"
|
|
||||||
:visible.sync="batchImportDialog.visible"
|
|
||||||
width="600px"
|
|
||||||
>
|
|
||||||
<el-form label-width="80px">
|
|
||||||
<el-form-item label="JSON 数据">
|
|
||||||
<el-input
|
|
||||||
type="textarea"
|
|
||||||
:rows="10"
|
|
||||||
v-model="batchImportDialog.input"
|
|
||||||
placeholder='[{"route":"/api/xxxx","name":"登录接口"}]'
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<div style="color:#909399;font-size:12px;margin-top:-8px;padding-left:80px;">
|
|
||||||
格式:JSON 数组,每项包含 route(请求路径)和 name(接口名称)
|
|
||||||
</div>
|
|
||||||
<span slot="footer" class="dialog-footer">
|
|
||||||
<el-button @click="batchImportDialog.visible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitBatchImport">导入</el-button>
|
|
||||||
</span>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -447,41 +314,7 @@
|
|||||||
return {
|
return {
|
||||||
activeMainTab: "routes",
|
activeMainTab: "routes",
|
||||||
apiList: [],
|
apiList: [],
|
||||||
apiPagination: { currentPage: 1, pageSize: 10 },
|
|
||||||
apiDialog: {
|
|
||||||
visible: false,
|
|
||||||
mode: "create",
|
|
||||||
editingIndex: -1,
|
|
||||||
form: { name: "", route: "", originalRoute: "" },
|
|
||||||
},
|
|
||||||
batchImportDialog: {
|
|
||||||
visible: false,
|
|
||||||
input: "",
|
|
||||||
},
|
|
||||||
mockFiles: [],
|
mockFiles: [],
|
||||||
contentTypeOptions: [
|
|
||||||
"application/json",
|
|
||||||
"application/x-www-form-urlencoded",
|
|
||||||
],
|
|
||||||
commonStatusCodes: [
|
|
||||||
{ value: 200, label: "200 OK" },
|
|
||||||
{ value: 201, label: "201 Created" },
|
|
||||||
{ value: 204, label: "204 No Content" },
|
|
||||||
{ value: 400, label: "400 Bad Request" },
|
|
||||||
{ value: 401, label: "401 Unauthorized" },
|
|
||||||
{ value: 403, label: "403 Forbidden" },
|
|
||||||
{ value: 404, label: "404 Not Found" },
|
|
||||||
{ value: 409, label: "409 Conflict" },
|
|
||||||
{ value: 422, label: "422 Unprocessable Entity" },
|
|
||||||
{ value: 429, label: "429 Too Many Requests" },
|
|
||||||
{ value: 500, label: "500 Internal Server Error" },
|
|
||||||
{ value: 501, label: "501 Not Implemented" },
|
|
||||||
{ value: 502, label: "502 Bad Gateway" },
|
|
||||||
{ value: 503, label: "503 Service Unavailable" },
|
|
||||||
],
|
|
||||||
routeSearch: "",
|
|
||||||
mockFileSearch: "",
|
|
||||||
apiSearch: "",
|
|
||||||
routePagination: {
|
routePagination: {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@ -511,9 +344,9 @@
|
|||||||
apiName: "",
|
apiName: "",
|
||||||
selectedApiRoute: "",
|
selectedApiRoute: "",
|
||||||
originalRoute: "",
|
originalRoute: "",
|
||||||
|
originalRawRoute: "",
|
||||||
route: "",
|
route: "",
|
||||||
filePath: "",
|
filePath: "",
|
||||||
statusCode: 200,
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -521,18 +354,12 @@
|
|||||||
visible: false,
|
visible: false,
|
||||||
mode: "create",
|
mode: "create",
|
||||||
form: {
|
form: {
|
||||||
fileName: "",
|
filePath: "mock/",
|
||||||
alias: "",
|
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
routeSearch: function () { this.routePagination.currentPage = 1; },
|
|
||||||
mockFileSearch: function () { this.mockFilePagination.currentPage = 1; },
|
|
||||||
apiSearch: function () { this.apiPagination.currentPage = 1; },
|
|
||||||
},
|
|
||||||
created: async function () {
|
created: async function () {
|
||||||
await this.loadApiList();
|
await this.loadApiList();
|
||||||
await this.loadMockFiles();
|
await this.loadMockFiles();
|
||||||
@ -556,13 +383,6 @@
|
|||||||
var start = (currentPage - 1) * pageSize;
|
var start = (currentPage - 1) * pageSize;
|
||||||
return source.slice(start, start + pageSize);
|
return source.slice(start, start + pageSize);
|
||||||
},
|
},
|
||||||
normalizeStatusCode: function (value) {
|
|
||||||
var numeric = Number(value);
|
|
||||||
if (Number.isInteger(numeric) && numeric >= 100 && numeric <= 599) {
|
|
||||||
return numeric;
|
|
||||||
}
|
|
||||||
return 200;
|
|
||||||
},
|
|
||||||
handleRoutePageChange: function (page) {
|
handleRoutePageChange: function (page) {
|
||||||
this.routePagination.currentPage = page;
|
this.routePagination.currentPage = page;
|
||||||
},
|
},
|
||||||
@ -574,20 +394,19 @@
|
|||||||
return item.route === route;
|
return item.route === route;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
toRouteArray: function (routesObj, routeStatusesObj, routeEnabledMap, routeApiNameMap) {
|
toRouteArray: function (routesObj) {
|
||||||
var self = this;
|
var self = this;
|
||||||
return Object.keys(routesObj || {}).map(function (route) {
|
return Object.keys(routesObj || {}).map(function (rawRoute) {
|
||||||
var storedName = (routeApiNameMap || {})[route] || "";
|
var enabled = !rawRoute.startsWith("#");
|
||||||
var matched = storedName ? null : self.findApiByRoute(route);
|
var route = enabled ? rawRoute : rawRoute.replace(/^#+/, "");
|
||||||
|
var matched = self.findApiByRoute(route);
|
||||||
return {
|
return {
|
||||||
|
rawRoute: rawRoute,
|
||||||
route: route,
|
route: route,
|
||||||
filePath: routesObj[route],
|
filePath: routesObj[rawRoute],
|
||||||
statusCode: self.normalizeStatusCode(
|
apiName: matched ? matched.name : "",
|
||||||
routeStatusesObj && routeStatusesObj[route],
|
|
||||||
),
|
|
||||||
apiName: storedName || (matched ? matched.name : ""),
|
|
||||||
selectedApiRoute: matched ? matched.route : "",
|
selectedApiRoute: matched ? matched.route : "",
|
||||||
enabled: (routeEnabledMap || {})[route] !== false,
|
enabled: enabled,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -597,56 +416,26 @@
|
|||||||
var route = (item.route || "").trim();
|
var route = (item.route || "").trim();
|
||||||
var filePath = (item.filePath || "").trim();
|
var filePath = (item.filePath || "").trim();
|
||||||
if (route && filePath) {
|
if (route && filePath) {
|
||||||
obj[route] = filePath;
|
var routeKey = item.enabled === false ? "#" + route : route;
|
||||||
|
obj[routeKey] = filePath;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
toRouteStatusObject: function (routeArray) {
|
removeRoute: function (index) {
|
||||||
var self = this;
|
|
||||||
var obj = {};
|
|
||||||
(routeArray || []).forEach(function (item) {
|
|
||||||
var route = (item.route || "").trim();
|
|
||||||
if (!route) return;
|
|
||||||
obj[route] = self.normalizeStatusCode(item.statusCode);
|
|
||||||
});
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
toRouteEnabledMap: function (routeArray) {
|
|
||||||
var obj = {};
|
|
||||||
(routeArray || []).forEach(function (item) {
|
|
||||||
var route = (item.route || "").trim();
|
|
||||||
if (route) {
|
|
||||||
obj[route] = item.enabled !== false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
toRouteApiNameMap: function (routeArray) {
|
|
||||||
var obj = {};
|
|
||||||
(routeArray || []).forEach(function (item) {
|
|
||||||
var route = (item.route || "").trim();
|
|
||||||
if (route) {
|
|
||||||
obj[route] = item.apiName || "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
removeRoute: async function (index) {
|
|
||||||
var actualIndex =
|
var actualIndex =
|
||||||
(this.routePagination.currentPage - 1) * this.routePagination.pageSize +
|
(this.routePagination.currentPage - 1) * this.routePagination.pageSize +
|
||||||
index;
|
index;
|
||||||
this.form.routes.splice(actualIndex, 1);
|
this.form.routes.splice(actualIndex, 1);
|
||||||
await this.saveRoutes();
|
|
||||||
},
|
},
|
||||||
getDefaultRouteForm: function () {
|
getDefaultRouteForm: function () {
|
||||||
return {
|
return {
|
||||||
apiName: "",
|
apiName: "",
|
||||||
selectedApiRoute: "",
|
selectedApiRoute: "",
|
||||||
originalRoute: "",
|
originalRoute: "",
|
||||||
|
originalRawRoute: "",
|
||||||
route: "",
|
route: "",
|
||||||
filePath: "",
|
filePath: "",
|
||||||
statusCode: 200,
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -682,17 +471,16 @@
|
|||||||
apiName: matched ? matched.name : item.apiName || "",
|
apiName: matched ? matched.name : item.apiName || "",
|
||||||
selectedApiRoute: matched ? matched.route : "",
|
selectedApiRoute: matched ? matched.route : "",
|
||||||
originalRoute: item.route || "",
|
originalRoute: item.route || "",
|
||||||
|
originalRawRoute: item.rawRoute || item.route || "",
|
||||||
route: item.route || "",
|
route: item.route || "",
|
||||||
filePath: item.filePath || "mock/",
|
filePath: item.filePath || "mock/",
|
||||||
statusCode: this.normalizeStatusCode(item.statusCode),
|
|
||||||
enabled: item.enabled !== false,
|
enabled: item.enabled !== false,
|
||||||
};
|
};
|
||||||
this.routeDialog.visible = true;
|
this.routeDialog.visible = true;
|
||||||
},
|
},
|
||||||
getDefaultMockFileForm: function () {
|
getDefaultMockFileForm: function () {
|
||||||
return {
|
return {
|
||||||
fileName: "",
|
filePath: "mock/",
|
||||||
alias: "",
|
|
||||||
content: "",
|
content: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -702,12 +490,9 @@
|
|||||||
this.mockFileDialog.visible = true;
|
this.mockFileDialog.visible = true;
|
||||||
},
|
},
|
||||||
openMockFileDialogForEdit: function (item) {
|
openMockFileDialogForEdit: function (item) {
|
||||||
var full = item.filePath || "";
|
|
||||||
var name = full.indexOf("mock/") === 0 ? full.slice(5) : full;
|
|
||||||
this.mockFileDialog.mode = "edit";
|
this.mockFileDialog.mode = "edit";
|
||||||
this.mockFileDialog.form = {
|
this.mockFileDialog.form = {
|
||||||
fileName: name,
|
filePath: item.filePath || "mock/",
|
||||||
alias: String(item.alias || ""),
|
|
||||||
content: String(item.content || ""),
|
content: String(item.content || ""),
|
||||||
};
|
};
|
||||||
this.mockFileDialog.visible = true;
|
this.mockFileDialog.visible = true;
|
||||||
@ -727,23 +512,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitMockFileDialog: async function () {
|
submitMockFileDialog: async function () {
|
||||||
var fileName = (this.mockFileDialog.form.fileName || "").trim();
|
if (!this.mockFileDialog.form.filePath) {
|
||||||
if (!fileName) {
|
this.$message.error("Mock 文件路径不能为空");
|
||||||
this.$message.error("文件名不能为空");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (fileName.indexOf("/") !== -1 || fileName.indexOf("\\") !== -1) {
|
|
||||||
this.$message.error("文件名不能包含路径分隔符");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var filePath = "mock/" + fileName;
|
|
||||||
try {
|
try {
|
||||||
var resp = await fetch("/__mock-files", {
|
var resp = await fetch("/__mock-files", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
filePath: filePath,
|
filePath: this.mockFileDialog.form.filePath,
|
||||||
alias: this.mockFileDialog.form.alias,
|
|
||||||
content: this.mockFileDialog.form.content,
|
content: this.mockFileDialog.form.content,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -800,23 +578,16 @@
|
|||||||
var resp = await fetch("/__config");
|
var resp = await fetch("/__config");
|
||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
this.form.config = Object.assign({}, this.form.config, data.config || {});
|
this.form.config = Object.assign({}, this.form.config, data.config || {});
|
||||||
this.form.routes = this.toRouteArray(
|
this.form.routes = this.toRouteArray(data.routes || {});
|
||||||
data.routes || {},
|
|
||||||
data.routeStatuses || {},
|
|
||||||
data.routeEnabledMap || {},
|
|
||||||
data.routeApiNameMap || {},
|
|
||||||
);
|
|
||||||
this.routePagination.currentPage = 1;
|
this.routePagination.currentPage = 1;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$message.error("加载配置失败: " + err.message);
|
this.$message.error("加载配置失败: " + err.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
saveRoutes: async function () {
|
saveConfig: async function () {
|
||||||
var payload = {
|
var payload = {
|
||||||
|
config: this.form.config,
|
||||||
routes: this.toRouteObject(this.form.routes),
|
routes: this.toRouteObject(this.form.routes),
|
||||||
routeStatuses: this.toRouteStatusObject(this.form.routes),
|
|
||||||
routeEnabledMap: this.toRouteEnabledMap(this.form.routes),
|
|
||||||
routeApiNameMap: this.toRouteApiNameMap(this.form.routes),
|
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
var resp = await fetch("/__config", {
|
var resp = await fetch("/__config", {
|
||||||
@ -828,23 +599,22 @@
|
|||||||
if (!resp.ok || data.success === false) {
|
if (!resp.ok || data.success === false) {
|
||||||
throw new Error(data.error || "保存失败");
|
throw new Error(data.error || "保存失败");
|
||||||
}
|
}
|
||||||
|
this.$message.success("配置已保存");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$message.error("保存路由失败: " + err.message);
|
this.$message.error("保存失败: " + err.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
saveServerConfig: async function () {
|
reloadConfig: async function () {
|
||||||
try {
|
try {
|
||||||
var resp = await fetch("/__config", {
|
var resp = await fetch("/__reload-config", { method: "POST" });
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ config: this.form.config }),
|
|
||||||
});
|
|
||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
if (!resp.ok || data.success === false) {
|
if (!resp.ok || data.success === false) {
|
||||||
throw new Error(data.error || "保存失败");
|
throw new Error(data.error || "重载失败");
|
||||||
}
|
}
|
||||||
|
this.$message.success("配置已重载");
|
||||||
|
this.loadConfig();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$message.error("保存配置失败: " + err.message);
|
this.$message.error("重载失败: " + err.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitRouteDialog: async function () {
|
submitRouteDialog: async function () {
|
||||||
@ -858,7 +628,6 @@
|
|||||||
}
|
}
|
||||||
var payload = Object.assign({}, this.routeDialog.form, {
|
var payload = Object.assign({}, this.routeDialog.form, {
|
||||||
useExistingFile: true,
|
useExistingFile: true,
|
||||||
statusCode: this.normalizeStatusCode(this.routeDialog.form.statusCode),
|
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
var resp = await fetch("/__routes", {
|
var resp = await fetch("/__routes", {
|
||||||
@ -882,180 +651,13 @@
|
|||||||
this.$message.error("创建失败: " + err.message);
|
this.$message.error("创建失败: " + err.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleApiPageChange: function (page) {
|
|
||||||
this.apiPagination.currentPage = page;
|
|
||||||
},
|
|
||||||
openApiDialogForCreate: function () {
|
|
||||||
this.apiDialog.mode = "create";
|
|
||||||
this.apiDialog.editingIndex = -1;
|
|
||||||
this.apiDialog.form = { name: "", route: "", originalRoute: "" };
|
|
||||||
this.apiDialog.visible = true;
|
|
||||||
},
|
|
||||||
openApiDialogForEdit: function (index) {
|
|
||||||
var actualIndex =
|
|
||||||
(this.apiPagination.currentPage - 1) * this.apiPagination.pageSize + index;
|
|
||||||
var item = this.apiList[actualIndex];
|
|
||||||
this.apiDialog.mode = "edit";
|
|
||||||
this.apiDialog.editingIndex = actualIndex;
|
|
||||||
this.apiDialog.form = {
|
|
||||||
name: item.name,
|
|
||||||
route: item.route,
|
|
||||||
originalRoute: item.route,
|
|
||||||
};
|
|
||||||
this.apiDialog.visible = true;
|
|
||||||
},
|
|
||||||
submitApiDialog: async function () {
|
|
||||||
if (!this.apiDialog.form.name) {
|
|
||||||
this.$message.error("接口名称不能为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.apiDialog.form.route || !this.apiDialog.form.route.startsWith("/")) {
|
|
||||||
this.$message.error("请求路径必须以 / 开头");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
var resp = await fetch("/__api-list", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(this.apiDialog.form),
|
|
||||||
});
|
|
||||||
var data = await resp.json();
|
|
||||||
if (!resp.ok || data.success === false) {
|
|
||||||
throw new Error(data.error || "操作失败");
|
|
||||||
}
|
|
||||||
this.$message.success(this.apiDialog.mode === "edit" ? "接口已修改" : "接口已添加");
|
|
||||||
this.apiDialog.visible = false;
|
|
||||||
await this.loadApiList();
|
|
||||||
} catch (err) {
|
|
||||||
this.$message.error("操作失败: " + err.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeApiItem: async function (index) {
|
|
||||||
var actualIndex =
|
|
||||||
(this.apiPagination.currentPage - 1) * this.apiPagination.pageSize + index;
|
|
||||||
var item = this.apiList[actualIndex];
|
|
||||||
try {
|
|
||||||
await this.$confirm("确定删除接口「" + item.name + "」?", "提示", {
|
|
||||||
confirmButtonText: "确定",
|
|
||||||
cancelButtonText: "取消",
|
|
||||||
type: "warning",
|
|
||||||
});
|
|
||||||
var resp = await fetch("/__api-list", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ route: item.route }),
|
|
||||||
});
|
|
||||||
var data = await resp.json();
|
|
||||||
if (!resp.ok || data.success === false) {
|
|
||||||
throw new Error(data.error || "删除失败");
|
|
||||||
}
|
|
||||||
this.$message.success("接口已删除");
|
|
||||||
await this.loadApiList();
|
|
||||||
} catch (err) {
|
|
||||||
if (err !== "cancel") {
|
|
||||||
this.$message.error("删除失败: " + err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openBatchImportDialog: function () {
|
|
||||||
this.batchImportDialog.input = "";
|
|
||||||
this.batchImportDialog.visible = true;
|
|
||||||
},
|
|
||||||
submitBatchImport: async function () {
|
|
||||||
var raw = (this.batchImportDialog.input || "").trim();
|
|
||||||
if (!raw) {
|
|
||||||
this.$message.error("请输入 JSON 数据");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var list;
|
|
||||||
try {
|
|
||||||
list = JSON.parse(raw);
|
|
||||||
} catch (e) {
|
|
||||||
this.$message.error("JSON 格式不正确:" + e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(list) || list.length === 0) {
|
|
||||||
this.$message.error("请输入非空 JSON 数组");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var okCount = 0;
|
|
||||||
var failCount = 0;
|
|
||||||
for (var i = 0; i < list.length; i++) {
|
|
||||||
var item = list[i];
|
|
||||||
if (!item || !item.route || !item.name) {
|
|
||||||
failCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
var resp = await fetch("/__api-list", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ route: item.route, name: item.name }),
|
|
||||||
});
|
|
||||||
var data = await resp.json();
|
|
||||||
if (resp.ok && data.success !== false) {
|
|
||||||
okCount++;
|
|
||||||
} else {
|
|
||||||
failCount++;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
failCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (okCount > 0) {
|
|
||||||
this.$message.success("成功导入 " + okCount + " 条" + (failCount > 0 ? ",失败 " + failCount + " 条" : ""));
|
|
||||||
} else {
|
|
||||||
this.$message.error("导入失败,共 " + failCount + " 条");
|
|
||||||
}
|
|
||||||
this.batchImportDialog.visible = false;
|
|
||||||
await this.loadApiList();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredRoutes: function () {
|
|
||||||
var q = (this.routeSearch || "").toLowerCase().trim();
|
|
||||||
var list = this.form.routes;
|
|
||||||
if (q) {
|
|
||||||
list = list.filter(function (item) {
|
|
||||||
return (
|
|
||||||
(item.route || "").toLowerCase().indexOf(q) !== -1 ||
|
|
||||||
(item.apiName || "").toLowerCase().indexOf(q) !== -1 ||
|
|
||||||
(item.filePath || "").toLowerCase().indexOf(q) !== -1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return list.slice().sort(function (a, b) {
|
|
||||||
return (b.enabled !== false ? 1 : 0) - (a.enabled !== false ? 1 : 0);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
filteredMockFiles: function () {
|
|
||||||
var q = (this.mockFileSearch || "").toLowerCase().trim();
|
|
||||||
if (!q) return this.mockFiles;
|
|
||||||
return this.mockFiles.filter(function (item) {
|
|
||||||
return (
|
|
||||||
(item.filePath || "").toLowerCase().indexOf(q) !== -1 ||
|
|
||||||
(item.alias || "").toLowerCase().indexOf(q) !== -1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
filteredApiList: function () {
|
|
||||||
var q = (this.apiSearch || "").toLowerCase().trim();
|
|
||||||
if (!q) return this.apiList;
|
|
||||||
return this.apiList.filter(function (item) {
|
|
||||||
return (
|
|
||||||
(item.route || "").toLowerCase().indexOf(q) !== -1 ||
|
|
||||||
(item.name || "").toLowerCase().indexOf(q) !== -1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
pagedRoutes: function () {
|
pagedRoutes: function () {
|
||||||
return this.getPagedData(this.filteredRoutes, this.routePagination);
|
return this.getPagedData(this.form.routes, this.routePagination);
|
||||||
},
|
},
|
||||||
pagedMockFiles: function () {
|
pagedMockFiles: function () {
|
||||||
return this.getPagedData(this.filteredMockFiles, this.mockFilePagination);
|
return this.getPagedData(this.mockFiles, this.mockFilePagination);
|
||||||
},
|
|
||||||
pagedApiList: function () {
|
|
||||||
return this.getPagedData(this.filteredApiList, this.apiPagination);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
11
config.json
11
config.json
@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"routes": {
|
"routes": {
|
||||||
"/api2/test": "mock/test.txt"
|
"/api2/admin/banner/list": "mock/banner.txt",
|
||||||
|
"/api2/home/ad/list": "mock/basic-error.json"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
"mockEnabled": true,
|
||||||
"cacheConfig": true,
|
"cacheConfig": true,
|
||||||
"reloadOnChange": true,
|
"reloadOnChange": true,
|
||||||
"defaultContentType": "application/json",
|
"defaultContentType": "application/json",
|
||||||
"proxyPort": 443,
|
"proxyPort": 8877,
|
||||||
"targetHost": "localhost",
|
"targetHost": "192.168.3.9",
|
||||||
"targetPort": 443
|
"targetPort": 8092,
|
||||||
|
"targetHttps": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
925
index.api.ts
925
index.api.ts
@ -1,68 +1,903 @@
|
|||||||
|
import * as http from "http";
|
||||||
|
import * as https from "https";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { migrateApiListFromJsonIfNeeded } from "./src/api-list";
|
import * as zlib from "zlib";
|
||||||
import { loadConfig } from "./src/config";
|
|
||||||
import { getActiveRoutes } from "./src/route-matching";
|
|
||||||
import { initDatabase } from "./src/db";
|
|
||||||
import { initMockFilesFromFsIfNeeded } from "./src/mock-files";
|
|
||||||
import {
|
|
||||||
createProxyServer,
|
|
||||||
getEffectiveTargetHttps,
|
|
||||||
getEffectiveTargetPort,
|
|
||||||
} from "./src/proxy";
|
|
||||||
import { state } from "./src/state";
|
|
||||||
|
|
||||||
function startServer(): void {
|
// 配置文件路径
|
||||||
const proxyServer = createProxyServer();
|
const CONFIG_FILE = path.join(__dirname, "config.json");
|
||||||
proxyServer.listen(state.config.proxyPort, "0.0.0.0", () => {
|
const API_LIST_FILE = path.join(__dirname, "mock", "api-list.json");
|
||||||
const targetPort = getEffectiveTargetPort();
|
const MOCK_DIR = path.join(__dirname, "mock");
|
||||||
const https = getEffectiveTargetHttps();
|
|
||||||
const proto = https ? "https" : "http";
|
// 定义类型
|
||||||
const defaultPort = https ? 443 : 80;
|
interface RouteConfig {
|
||||||
|
[route: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppConfig {
|
||||||
|
/** 为 false 时所有请求走代理,不命中 mock 路由;缺省为 true */
|
||||||
|
mockEnabled?: boolean;
|
||||||
|
cacheConfig: boolean;
|
||||||
|
reloadOnChange: boolean;
|
||||||
|
defaultContentType: string;
|
||||||
|
proxyPort: number;
|
||||||
|
targetHost: string;
|
||||||
|
/** 代理转发的目标端口;缺省 HTTPS 为 443,HTTP 为 80 */
|
||||||
|
targetPort?: number;
|
||||||
|
/** 为 false 时用 HTTP 连接上游;缺省 true(HTTPS) */
|
||||||
|
targetHttps?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfigFile {
|
||||||
|
routes: RouteConfig;
|
||||||
|
config: AppConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiListItem {
|
||||||
|
name: string;
|
||||||
|
route: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockFileItem {
|
||||||
|
filePath: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储当前的路由配置
|
||||||
|
let MOCK_ROUTES: RouteConfig = {};
|
||||||
|
let RAW_ROUTES: RouteConfig = {};
|
||||||
|
let CONFIG: AppConfig = {
|
||||||
|
cacheConfig: true,
|
||||||
|
reloadOnChange: true,
|
||||||
|
defaultContentType: "application/json",
|
||||||
|
proxyPort: 443,
|
||||||
|
targetHost: "localhost",
|
||||||
|
targetPort: 443,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 过滤掉以 # 开头的路由(视为注释,不参与 mock)
|
||||||
|
function filterActiveRoutes(routes: RouteConfig): RouteConfig {
|
||||||
|
const filtered: RouteConfig = {};
|
||||||
|
for (const [route, filePath] of Object.entries(routes)) {
|
||||||
|
if (route.startsWith("#")) continue;
|
||||||
|
filtered[route] = filePath;
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTargetHttps(): boolean {
|
||||||
|
return CONFIG.targetHttps !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetPort(): number {
|
||||||
|
if (CONFIG.targetPort != null) return CONFIG.targetPort;
|
||||||
|
return isTargetHttps() ? 443 : 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
function upstreamRequest(
|
||||||
|
options: http.RequestOptions,
|
||||||
|
callback: (proxyRes: http.IncomingMessage) => void,
|
||||||
|
): http.ClientRequest {
|
||||||
|
return isTargetHttps()
|
||||||
|
? https.request(options, callback)
|
||||||
|
: http.request(options, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载配置文件
|
||||||
|
function loadConfig() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(CONFIG_FILE)) {
|
||||||
|
console.warn(`[CONFIG] 配置文件不存在: ${CONFIG_FILE}`);
|
||||||
|
console.warn(`[CONFIG] 正在创建默认配置文件...`);
|
||||||
|
|
||||||
|
// 创建默认配置
|
||||||
|
const defaultConfig: ConfigFile = {
|
||||||
|
routes: {
|
||||||
|
"/api2/test": "mock/test.txt",
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
cacheConfig: true,
|
||||||
|
reloadOnChange: true,
|
||||||
|
defaultContentType: "application/json",
|
||||||
|
proxyPort: 443,
|
||||||
|
targetHost: "localhost",
|
||||||
|
targetPort: 443,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确保mock目录存在
|
||||||
|
const mockDir = path.join(__dirname, "mock");
|
||||||
|
if (!fs.existsSync(mockDir)) {
|
||||||
|
fs.mkdirSync(mockDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置文件
|
||||||
|
fs.writeFileSync(
|
||||||
|
CONFIG_FILE,
|
||||||
|
JSON.stringify(defaultConfig, null, 2),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
console.log(`[CONFIG] 已创建默认配置文件: ${CONFIG_FILE}`);
|
||||||
|
|
||||||
|
// 加载配置(# 开头的路由视为注释,不参与 mock)
|
||||||
|
const configData: ConfigFile = JSON.parse(
|
||||||
|
fs.readFileSync(CONFIG_FILE, "utf-8"),
|
||||||
|
);
|
||||||
|
RAW_ROUTES = configData.routes || {};
|
||||||
|
MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES);
|
||||||
|
CONFIG = configData.config || CONFIG;
|
||||||
|
} else {
|
||||||
|
const configData: ConfigFile = JSON.parse(
|
||||||
|
fs.readFileSync(CONFIG_FILE, "utf-8"),
|
||||||
|
);
|
||||||
|
RAW_ROUTES = configData.routes || {};
|
||||||
|
MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES);
|
||||||
|
CONFIG = configData.config || CONFIG;
|
||||||
|
console.log(
|
||||||
|
`[CONFIG] 配置文件已加载,共 ${Object.keys(MOCK_ROUTES).length} 个路由${CONFIG.mockEnabled !== false ? "" : "(mock 已关闭,全部走代理)"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证mock文件是否存在
|
||||||
|
validateMockFiles();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CONFIG] 加载配置文件失败:`, error);
|
||||||
|
// 使用默认配置
|
||||||
|
MOCK_ROUTES = {
|
||||||
|
"/api2/user/list": "mock/user_list.txt",
|
||||||
|
};
|
||||||
|
RAW_ROUTES = { ...MOCK_ROUTES };
|
||||||
|
CONFIG = {
|
||||||
|
cacheConfig: true,
|
||||||
|
reloadOnChange: true,
|
||||||
|
defaultContentType: "application/json",
|
||||||
|
proxyPort: 9443,
|
||||||
|
targetHost: "devrmtapp.resmart.cn",
|
||||||
|
targetPort: 443,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCurrentConfigFile(): ConfigFile {
|
||||||
|
return {
|
||||||
|
routes: { ...RAW_ROUTES },
|
||||||
|
config: { ...CONFIG },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfigFile(nextConfig: ConfigFile): void {
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(nextConfig, null, 2), "utf-8");
|
||||||
|
RAW_ROUTES = nextConfig.routes || {};
|
||||||
|
MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES);
|
||||||
|
CONFIG = nextConfig.config || CONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadApiList(): ApiListItem[] {
|
||||||
|
if (!fs.existsSync(API_LIST_FILE)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const text = fs.readFileSync(API_LIST_FILE, "utf-8").trim();
|
||||||
|
if (!text) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const seenRoutes = new Set<string>();
|
||||||
|
const list: ApiListItem[] = [];
|
||||||
|
for (const item of parsed) {
|
||||||
|
const route = String(item?.route || "").trim();
|
||||||
|
const name = String(item?.name || "").trim();
|
||||||
|
if (!route || !route.startsWith("/") || !name || seenRoutes.has(route)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenRoutes.add(route);
|
||||||
|
list.push({ name, route });
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ADMIN] 读取 api-list.json 失败:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMockFilePath(filePath: string): string {
|
||||||
|
const trimmed = filePath.trim().replace(/\\/g, "/");
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new Error("filePath is required");
|
||||||
|
}
|
||||||
|
const relative = trimmed.startsWith("mock/") ? trimmed : `mock/${trimmed}`;
|
||||||
|
if (path.isAbsolute(relative)) {
|
||||||
|
throw new Error("filePath must be a relative path");
|
||||||
|
}
|
||||||
|
return relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMockFullPath(relativePath: string): string {
|
||||||
|
const normalized = normalizeMockFilePath(relativePath);
|
||||||
|
const fullPath = path.resolve(__dirname, normalized);
|
||||||
|
const mockRoot = path.resolve(MOCK_DIR);
|
||||||
|
if (!fullPath.startsWith(mockRoot)) {
|
||||||
|
throw new Error("filePath is invalid");
|
||||||
|
}
|
||||||
|
if (path.basename(fullPath) === "api-list.json") {
|
||||||
|
throw new Error("api-list.json is read-only in this panel");
|
||||||
|
}
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkMockFiles(dir: string, baseDir: string, result: string[]): void {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkMockFiles(fullPath, baseDir, result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile()) continue;
|
||||||
|
if (entry.name === "api-list.json") continue;
|
||||||
|
const relative = path.relative(baseDir, fullPath).replace(/\\/g, "/");
|
||||||
|
result.push(`mock/${relative}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMockFiles(): MockFileItem[] {
|
||||||
|
if (!fs.existsSync(MOCK_DIR)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const files: string[] = [];
|
||||||
|
walkMockFiles(MOCK_DIR, MOCK_DIR, files);
|
||||||
|
files.sort((a, b) => a.localeCompare(b));
|
||||||
|
return files.map((filePath) => {
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
return {
|
||||||
|
filePath,
|
||||||
|
content: fs.readFileSync(fullPath, "utf-8"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBody(req: http.IncomingMessage): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
req.on("data", (chunk: Buffer) => {
|
||||||
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||||
|
});
|
||||||
|
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
||||||
|
req.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证mock文件是否存在
|
||||||
|
function validateMockFiles() {
|
||||||
|
console.log(`[CONFIG] 验证mock文件...`);
|
||||||
|
let missingFiles: Array<{
|
||||||
|
route: string;
|
||||||
|
filePath: string;
|
||||||
|
fullPath: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const [route, filePath] of Object.entries(MOCK_ROUTES)) {
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
missingFiles.push({ route, filePath, fullPath });
|
||||||
|
console.warn(
|
||||||
|
`[CONFIG] 警告: mock文件不存在 - ${filePath} (用于路由: ${route})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingFiles.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`[CONFIG] 缺少 ${missingFiles.length} 个mock文件,请创建这些文件`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(`[CONFIG] 所有mock文件验证通过`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为mock路由的函数
|
||||||
|
function isMockRoute(requestPath: string): boolean {
|
||||||
|
if (CONFIG.mockEnabled === false) return false;
|
||||||
|
return MOCK_ROUTES.hasOwnProperty(requestPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取mock文件路径
|
||||||
|
function getMockFilePath(requestPath: string): string {
|
||||||
|
return MOCK_ROUTES[requestPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeBodyByEncoding(
|
||||||
|
bodyBuffer: Buffer,
|
||||||
|
contentEncoding?: string,
|
||||||
|
): Buffer {
|
||||||
|
const encoding = (contentEncoding || "").toLowerCase().trim();
|
||||||
|
try {
|
||||||
|
if (encoding.includes("gzip")) {
|
||||||
|
return zlib.gunzipSync(bodyBuffer);
|
||||||
|
}
|
||||||
|
if (encoding.includes("br")) {
|
||||||
|
return zlib.brotliDecompressSync(bodyBuffer);
|
||||||
|
}
|
||||||
|
if (encoding.includes("deflate")) {
|
||||||
|
return zlib.inflateSync(bodyBuffer);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[MOCK] 解压上游响应失败,按原始内容写入文件", error);
|
||||||
|
}
|
||||||
|
return bodyBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 代理服务器
|
||||||
|
const proxyServer = http.createServer((clientReq, clientRes) => {
|
||||||
|
// 解析客户端请求的 URL
|
||||||
|
const parsedUrl = new URL(`http://localhost${clientReq.url!}`);
|
||||||
|
const requestPath = parsedUrl.pathname;
|
||||||
|
|
||||||
|
// 管理接口:保留路径,不参与代理转发
|
||||||
|
if (requestPath === "/__reload-config") {
|
||||||
|
if (clientReq.method !== "POST") {
|
||||||
|
clientRes.writeHead(405, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Allow: "POST",
|
||||||
|
});
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Method Not Allowed",
|
||||||
|
allow: ["POST"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const oldCount = Object.keys(MOCK_ROUTES).length;
|
||||||
|
loadConfig();
|
||||||
|
const newCount = Object.keys(MOCK_ROUTES).length;
|
||||||
|
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Configuration reloaded successfully",
|
||||||
|
routesCount: newCount,
|
||||||
|
routesChanged: newCount - oldCount,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[ADMIN] 通过API重新加载配置 (路由数: ${oldCount} -> ${newCount})`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestPath === "/__config") {
|
||||||
|
if (clientReq.method !== "GET" && clientReq.method !== "POST") {
|
||||||
|
clientRes.writeHead(405, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Allow: "GET, POST",
|
||||||
|
});
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Method Not Allowed",
|
||||||
|
allow: ["GET", "POST"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientReq.method === "POST") {
|
||||||
|
readBody(clientReq)
|
||||||
|
.then((bodyText) => {
|
||||||
|
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||||
|
const nextConfig: ConfigFile = {
|
||||||
|
routes: body.routes || {},
|
||||||
|
config: body.config || CONFIG,
|
||||||
|
};
|
||||||
|
saveConfigFile(nextConfig);
|
||||||
|
validateMockFiles();
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Configuration saved successfully",
|
||||||
|
totalRoutes: Object.keys(MOCK_ROUTES).length,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
routes: RAW_ROUTES,
|
||||||
|
config: CONFIG,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
totalRoutes: Object.keys(MOCK_ROUTES).length,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestPath === "/__routes") {
|
||||||
|
if (clientReq.method !== "POST") {
|
||||||
|
clientRes.writeHead(405, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Allow: "POST",
|
||||||
|
});
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Method Not Allowed",
|
||||||
|
allow: ["POST"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
readBody(clientReq)
|
||||||
|
.then((bodyText) => {
|
||||||
|
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||||
|
let route = String(body.route || "").trim();
|
||||||
|
const filePath = String(body.filePath || "").trim();
|
||||||
|
const fileContent = String(body.fileContent || "");
|
||||||
|
const overwrite = body.overwrite === true;
|
||||||
|
const template = String(body.template || "").trim();
|
||||||
|
const apiName = String(body.apiName || "").trim();
|
||||||
|
const selectedApiRoute = String(body.selectedApiRoute || "").trim();
|
||||||
|
const originalRoute = String(body.originalRoute || "").trim();
|
||||||
|
const originalRawRoute = String(body.originalRawRoute || "").trim();
|
||||||
|
const enabled = body.enabled !== false;
|
||||||
|
const useExistingFile =
|
||||||
|
body.useExistingFile === true || template === "basicError";
|
||||||
|
const apiList = loadApiList();
|
||||||
|
const selectedApi = selectedApiRoute
|
||||||
|
? apiList.find((item) => item.route === selectedApiRoute)
|
||||||
|
: undefined;
|
||||||
|
const originalApi = originalRoute
|
||||||
|
? apiList.find((item) => item.route === originalRoute)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// 来自 api-list 的接口路由不可在新增/修改时变更
|
||||||
|
if (selectedApi) {
|
||||||
|
if (route && route !== selectedApi.route) {
|
||||||
|
throw new Error("api-list route cannot be changed");
|
||||||
|
}
|
||||||
|
if (apiName && apiName !== selectedApi.name) {
|
||||||
|
throw new Error("api-list name cannot be changed");
|
||||||
|
}
|
||||||
|
route = selectedApi.route;
|
||||||
|
}
|
||||||
|
if (originalApi && route !== originalApi.route) {
|
||||||
|
throw new Error("api-list route cannot be changed");
|
||||||
|
}
|
||||||
|
if (originalApi && apiName && apiName !== originalApi.name) {
|
||||||
|
throw new Error("api-list name cannot be changed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!route.startsWith("/")) {
|
||||||
|
throw new Error("route must start with '/'");
|
||||||
|
}
|
||||||
|
const normalizedFilePath = normalizeMockFilePath(filePath);
|
||||||
|
const fullPath = resolveMockFullPath(normalizedFilePath);
|
||||||
|
if (useExistingFile) {
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
throw new Error("mock file does not exist");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!overwrite && fs.existsSync(fullPath)) {
|
||||||
|
throw new Error(
|
||||||
|
"mock file already exists, set overwrite=true to replace",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, fileContent, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextConfig = buildCurrentConfigFile();
|
||||||
|
const nextRouteKey = enabled ? route : `#${route}`;
|
||||||
|
const oldRouteCandidates = new Set<string>();
|
||||||
|
if (originalRawRoute) {
|
||||||
|
oldRouteCandidates.add(originalRawRoute);
|
||||||
|
}
|
||||||
|
if (originalRoute) {
|
||||||
|
oldRouteCandidates.add(originalRoute);
|
||||||
|
oldRouteCandidates.add(`#${originalRoute}`);
|
||||||
|
}
|
||||||
|
oldRouteCandidates.forEach((key) => {
|
||||||
|
if (key && key !== nextRouteKey) {
|
||||||
|
delete nextConfig.routes[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nextConfig.routes[nextRouteKey] = normalizedFilePath;
|
||||||
|
saveConfigFile(nextConfig);
|
||||||
|
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Route and mock file created successfully",
|
||||||
|
route,
|
||||||
|
routeKey: nextRouteKey,
|
||||||
|
filePath: normalizedFilePath,
|
||||||
|
enabled,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestPath === "/__api-list") {
|
||||||
|
if (clientReq.method !== "GET") {
|
||||||
|
clientRes.writeHead(405, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Allow: "GET",
|
||||||
|
});
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Method Not Allowed",
|
||||||
|
allow: ["GET"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
list: loadApiList(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestPath === "/__mock-files") {
|
||||||
|
if (
|
||||||
|
clientReq.method !== "GET" &&
|
||||||
|
clientReq.method !== "POST" &&
|
||||||
|
clientReq.method !== "DELETE"
|
||||||
|
) {
|
||||||
|
clientRes.writeHead(405, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Allow: "GET, POST, DELETE",
|
||||||
|
});
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Method Not Allowed",
|
||||||
|
allow: ["GET", "POST", "DELETE"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientReq.method === "GET") {
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
list: loadMockFiles(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
readBody(clientReq)
|
||||||
|
.then((bodyText) => {
|
||||||
|
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||||
|
const filePath = String(body.filePath || "").trim();
|
||||||
|
const fullPath = resolveMockFullPath(filePath);
|
||||||
|
const normalizedPath = normalizeMockFilePath(filePath);
|
||||||
|
|
||||||
|
if (clientReq.method === "POST") {
|
||||||
|
const content = String(body.content || "");
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, content, "utf-8");
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
filePath: normalizedPath,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
throw new Error("mock file does not exist");
|
||||||
|
}
|
||||||
|
fs.unlinkSync(fullPath);
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
filePath: normalizedPath,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestPath === "/__admin") {
|
||||||
|
if (clientReq.method !== "GET") {
|
||||||
|
clientRes.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
|
||||||
|
clientRes.end("Method Not Allowed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const adminHtmlPath = path.join(__dirname, "admin.html");
|
||||||
|
if (!fs.existsSync(adminHtmlPath)) {
|
||||||
|
clientRes.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
||||||
|
clientRes.end("admin.html not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const html = fs.readFileSync(adminHtmlPath, "utf-8");
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
||||||
|
clientRes.end(html);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为需要mock的路由
|
||||||
|
if (isMockRoute(requestPath)) {
|
||||||
|
const mockFile = getMockFilePath(requestPath);
|
||||||
|
console.log(`[MOCK] 拦截路由: ${requestPath} -> 使用文件: ${mockFile}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建完整的文件路径
|
||||||
|
const mockFilePath = path.join(__dirname, mockFile);
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(mockFilePath)) {
|
||||||
|
console.warn(`[MOCK] Mock文件不存在,回源并自动生成: ${mockFilePath}`);
|
||||||
|
|
||||||
|
const targetPort = getTargetPort();
|
||||||
|
const options: http.RequestOptions = {
|
||||||
|
hostname: CONFIG.targetHost,
|
||||||
|
port: targetPort,
|
||||||
|
method: clientReq.method,
|
||||||
|
path: parsedUrl.pathname + parsedUrl.search,
|
||||||
|
headers: {
|
||||||
|
...clientReq.headers,
|
||||||
|
host: CONFIG.targetHost,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyReq = upstreamRequest(options, (proxyRes) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
proxyRes.on("data", (chunk: Buffer) => {
|
||||||
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||||
|
});
|
||||||
|
|
||||||
|
proxyRes.on("end", () => {
|
||||||
|
const bodyBuffer = Buffer.concat(chunks);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decodedBuffer = decodeBodyByEncoding(
|
||||||
|
bodyBuffer,
|
||||||
|
Array.isArray(proxyRes.headers["content-encoding"])
|
||||||
|
? proxyRes.headers["content-encoding"][0]
|
||||||
|
: proxyRes.headers["content-encoding"],
|
||||||
|
);
|
||||||
|
const contentType = String(proxyRes.headers["content-type"] || "");
|
||||||
|
const toWrite =
|
||||||
|
contentType.includes("application/json") ||
|
||||||
|
contentType.includes("text/") ||
|
||||||
|
contentType.includes("application/xml") ||
|
||||||
|
contentType.includes("application/javascript")
|
||||||
|
? decodedBuffer.toString("utf-8")
|
||||||
|
: decodedBuffer;
|
||||||
|
fs.mkdirSync(path.dirname(mockFilePath), { recursive: true });
|
||||||
|
fs.writeFileSync(mockFilePath, toWrite);
|
||||||
|
console.log(`[MOCK] 已自动写入mock文件: ${mockFilePath}`);
|
||||||
|
} catch (writeErr) {
|
||||||
|
console.error(`[MOCK] 自动写入mock文件失败: ${mockFilePath}`, writeErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
clientRes.writeHead(proxyRes.statusCode || 200, {
|
||||||
|
...proxyRes.headers,
|
||||||
|
"X-Mock-Autogenerated": "true",
|
||||||
|
"X-Mock-Source": mockFile,
|
||||||
|
});
|
||||||
|
clientRes.end(bodyBuffer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
proxyReq.on("error", (err) => {
|
||||||
|
console.error("Proxy request error:", err);
|
||||||
|
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
error: "Proxy error",
|
||||||
|
route: requestPath,
|
||||||
|
file: mockFile,
|
||||||
|
message: err.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
clientReq.pipe(proxyReq);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取本地mock文件
|
||||||
|
const mockData = fs.readFileSync(mockFilePath, "utf-8");
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
const contentType = CONFIG.defaultContentType || "application/json";
|
||||||
|
clientRes.writeHead(200, {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type",
|
||||||
|
"X-Mock-Source": mockFile,
|
||||||
|
"X-Mock-Timestamp": new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回mock数据
|
||||||
|
clientRes.end(mockData);
|
||||||
|
console.log(`[MOCK] 成功返回mock数据: ${mockFile}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MOCK] 读取mock文件失败: ${mockFile}`, error);
|
||||||
|
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
error: "Failed to read mock data",
|
||||||
|
route: requestPath,
|
||||||
|
file: mockFile,
|
||||||
|
message: (error as Error).message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return; // 直接返回,不转发到目标服务器
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是mock路由,正常代理转发
|
||||||
|
console.log(`[PROXY] 转发请求: ${requestPath}`);
|
||||||
|
|
||||||
|
const targetPort = getTargetPort();
|
||||||
|
|
||||||
|
// 目标服务器的选项
|
||||||
|
const options: http.RequestOptions = {
|
||||||
|
hostname: CONFIG.targetHost,
|
||||||
|
port: targetPort,
|
||||||
|
method: clientReq.method,
|
||||||
|
path: parsedUrl.pathname + parsedUrl.search,
|
||||||
|
headers: {
|
||||||
|
...clientReq.headers,
|
||||||
|
host: CONFIG.targetHost,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyReq = upstreamRequest(options, (proxyRes) => {
|
||||||
|
// 将目标服务器的响应头复制到客户端响应
|
||||||
|
clientRes.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
||||||
|
// 将目标服务器的响应数据管道传输到客户端
|
||||||
|
proxyRes.pipe(clientRes);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
proxyReq.on("error", (err) => {
|
||||||
|
console.error("Proxy request error:", err);
|
||||||
|
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
error: "Proxy error",
|
||||||
|
message: err.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将客户端请求体管道传输到代理请求
|
||||||
|
clientReq.pipe(proxyReq);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化:第一次加载配置
|
||||||
|
loadConfig();
|
||||||
|
|
||||||
|
// 如果配置了文件监视,则监听配置文件变化
|
||||||
|
if (CONFIG.reloadOnChange) {
|
||||||
|
fs.watchFile(CONFIG_FILE, (curr, prev) => {
|
||||||
|
console.log(`[CONFIG] 配置文件已修改,重新加载...`);
|
||||||
|
try {
|
||||||
|
const oldRoutesCount = Object.keys(MOCK_ROUTES).length;
|
||||||
|
loadConfig();
|
||||||
|
const newRoutesCount = Object.keys(MOCK_ROUTES).length;
|
||||||
|
console.log(
|
||||||
|
`[CONFIG] 配置重载完成 (路由数: ${oldRoutesCount} -> ${newRoutesCount})`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CONFIG] 重新加载配置文件失败:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`[CONFIG] 已启用配置文件监视: ${CONFIG_FILE}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyServer.listen(CONFIG.proxyPort, "0.0.0.0", () => {
|
||||||
|
const targetPort = getTargetPort();
|
||||||
|
const proto = isTargetHttps() ? "https" : "http";
|
||||||
|
const defaultPort = isTargetHttps() ? 443 : 80;
|
||||||
console.log(`========================================`);
|
console.log(`========================================`);
|
||||||
|
console.log(`代理服务器运行在: http://localhost:${CONFIG.proxyPort}`);
|
||||||
console.log(
|
console.log(
|
||||||
`代理服务器运行在: http://localhost:${state.config.proxyPort}`,
|
`目标服务器: ${proto}://${CONFIG.targetHost}${targetPort !== defaultPort ? `:${targetPort}` : ""}`,
|
||||||
);
|
);
|
||||||
|
console.log(`配置文件: ${CONFIG_FILE}`);
|
||||||
console.log(
|
console.log(
|
||||||
`目标服务器: ${proto}://${state.config.targetHost}${targetPort !== defaultPort ? `:${targetPort}` : ""}`,
|
`已配置Mock路由: ${Object.keys(MOCK_ROUTES).length} 个${CONFIG.mockEnabled !== false ? "" : "(当前 mockEnabled=false,未生效)"}`,
|
||||||
);
|
|
||||||
console.log(`数据库: data/mock-mappings.sqlite3`);
|
|
||||||
const activeRoutes = getActiveRoutes();
|
|
||||||
console.log(
|
|
||||||
`已配置Mock路由: ${Object.keys(activeRoutes).length} 个${state.config.mockEnabled !== false ? "" : "(当前 mockEnabled=false,未生效)"}`,
|
|
||||||
);
|
);
|
||||||
console.log(`========================================`);
|
console.log(`========================================`);
|
||||||
console.log(`管理接口:`);
|
console.log(`管理接口:`);
|
||||||
console.log(
|
console.log(
|
||||||
` GET http://localhost:${state.config.proxyPort}/__config 查看当前配置`,
|
` GET http://localhost:${CONFIG.proxyPort}/__config 查看当前配置`,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
` POST http://localhost:${state.config.proxyPort}/__reload-config 重新加载配置`,
|
` POST http://localhost:${CONFIG.proxyPort}/__reload-config 重新加载配置`,
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` GET http://localhost:${state.config.proxyPort}/__admin 管理面板`,
|
|
||||||
);
|
);
|
||||||
console.log(`========================================`);
|
console.log(`========================================`);
|
||||||
console.log(`Mock路由列表:`);
|
console.log(`Mock路由列表:`);
|
||||||
|
|
||||||
for (const [route, file] of Object.entries(activeRoutes)) {
|
for (const [route, file] of Object.entries(MOCK_ROUTES)) {
|
||||||
const filePath = path.join(__dirname, file);
|
const filePath = path.join(__dirname, file);
|
||||||
const exists = fs.existsSync(filePath) ? "✓" : "✗";
|
const exists = fs.existsSync(filePath) ? "✓" : "✗";
|
||||||
console.log(` ${exists} ${route} -> ${file}`);
|
console.log(` ${exists} ${route} -> ${file}`);
|
||||||
}
|
}
|
||||||
console.log(`========================================`);
|
console.log(`========================================`);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function bootstrap(): Promise<void> {
|
|
||||||
await initDatabase();
|
|
||||||
await initMockFilesFromFsIfNeeded();
|
|
||||||
await migrateApiListFromJsonIfNeeded();
|
|
||||||
await loadConfig();
|
|
||||||
startServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrap().catch((error) => {
|
|
||||||
console.error("[BOOTSTRAP] 启动失败:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
});
|
||||||
|
|||||||
60
mock/api-list.json
Normal file
60
mock/api-list.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
[
|
||||||
|
{ "name": "登录/注册发送验证码", "route": "/api1/authentication/sms/send" },
|
||||||
|
{ "name": "绑定手机号发送验证码", "route": "/api1/authentication/bind/send" },
|
||||||
|
{ "name": "账号注销", "route": "/api1/account/revoke" },
|
||||||
|
{ "name": "绑定手机号", "route": "/api1/account/bind" },
|
||||||
|
{ "name": "认证授权刷新接口", "route": "/api1/oauth2/token" },
|
||||||
|
{ "name": "一键登录阿里云授权码", "route": "/api1/authentication/common" },
|
||||||
|
|
||||||
|
{ "name": "更新用户信息", "route": "/api2/user/update" },
|
||||||
|
{ "name": "获取用户信息", "route": "/api2/user" },
|
||||||
|
{ "name": "我的链接", "route": "/api2/link" },
|
||||||
|
{ "name": "获取字典类型", "route": "/api2/dict/types" },
|
||||||
|
{ "name": "获取字典项", "route": "/api2/dict/" },
|
||||||
|
{ "name": "获取设备列表", "route": "/api2/device/list" },
|
||||||
|
{ "name": "根据类型获取设备列表", "route": "/api2/device/type/list" },
|
||||||
|
{ "name": "查询历史设备列表", "route": "/api2/device/history/list" },
|
||||||
|
{ "name": "绑定设备", "route": "/api2/device/save" },
|
||||||
|
{ "name": "切换设备", "route": "/api2/device/switch" },
|
||||||
|
{ "name": "用机人主设备", "route": "/api2/device/master" },
|
||||||
|
{ "name": "解绑设备", "route": "/api2/device/unbind" },
|
||||||
|
{ "name": "获取设备信息", "route": "/api2/device/one" },
|
||||||
|
{ "name": "获取设备店铺", "route": "/api2/device/store" },
|
||||||
|
{ "name": "获取设备图片", "route": "/api2/device/bind/img" },
|
||||||
|
{ "name": "查询设备报告日期", "route": "/api2/device/report/date" },
|
||||||
|
{ "name": "查询是否有报告页", "route": "/api2/device/reports" },
|
||||||
|
|
||||||
|
{ "name": "查询使用教程列表", "route": "/api2/course/list" },
|
||||||
|
{ "name": "查询白脸教程列表", "route": "/api2/course/white/face/list" },
|
||||||
|
{ "name": "获取省份城市列表", "route": "/api2/sys/provinces" },
|
||||||
|
{ "name": "制氧机报告单天", "route": "/api2/oxygenerator/report/day" },
|
||||||
|
{ "name": "制氧机报告多天", "route": "/api2/oxygenerator/report/multi/day" },
|
||||||
|
{ "name": "血氧仪报告单天", "route": "/api2/oximeter/report/day" },
|
||||||
|
{ "name": "血氧仪报告多天", "route": "/api2/oximeter/report/multi/day" },
|
||||||
|
{ "name": "呼吸机报告单天", "route": "/api2/ventilator/report/day" },
|
||||||
|
{ "name": "呼吸机报告多天", "route": "/api2/ventilator/report/multi/day" },
|
||||||
|
{ "name": "血氧仪健康报告单天", "route": "/api2/oximeter/health/report/day" },
|
||||||
|
{ "name": "血氧仪健康报告多天", "route": "/api2/oximeter/health/report/multi/day" },
|
||||||
|
{ "name": "血氧仪报告详情", "route": "/api2/oximeter/report/detail" },
|
||||||
|
{ "name": "首页判断", "route": "/api2/home/flag" },
|
||||||
|
{ "name": "首页Banner", "route": "/api2/home/ad/list" },
|
||||||
|
{ "name": "首页卡片信息", "route": "/api2/home/card/info" },
|
||||||
|
{ "name": "修改首页卡片", "route": "/api2/home/card/update" },
|
||||||
|
{ "name": "协议列表", "route": "/api2/protocol/list" },
|
||||||
|
{ "name": "同意协议", "route": "/api2/protocol/agree" },
|
||||||
|
{ "name": "协议是否更新", "route": "/api2/protocol/has/update" },
|
||||||
|
|
||||||
|
{ "name": "扫码获取报告", "route": "/api2/qr/scan" },
|
||||||
|
{ "name": "查询QR报告详情", "route": "/api2/qr/one" },
|
||||||
|
{ "name": "查询QR报告列表", "route": "/api2/qr/page" },
|
||||||
|
{ "name": "获取报告数量", "route": "/api2/qr/count" },
|
||||||
|
|
||||||
|
{ "name": "检查健康自测人数", "route": "/api2/self/check/head/count" },
|
||||||
|
{ "name": "健康自测结果", "route": "/api2/self/check/save" },
|
||||||
|
|
||||||
|
{ "name": "科普分类列表", "route": "/api2/health/category/list" },
|
||||||
|
{ "name": "科普列表", "route": "/api2/health/service/list" },
|
||||||
|
{ "name": "科普详情", "route": "/api2/health/service/getById" },
|
||||||
|
|
||||||
|
{ "name": "deepSeek流式接口", "route": "/api2/deepSeek/stream" }
|
||||||
|
]
|
||||||
1
mock/basic-error.json
Normal file
1
mock/basic-error.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"code": -1, "success": false, "msg": "失败"}
|
||||||
@ -1 +0,0 @@
|
|||||||
{"code":0,"msg":"ok"}
|
|
||||||
713
package-lock.json
generated
713
package-lock.json
generated
@ -4,9 +4,6 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
|
||||||
"sqlite3": "^6.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
@ -26,18 +23,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/fs-minipass": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"minipass": "^7.0.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
@ -104,16 +89,6 @@
|
|||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/abbrev": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.17.0 || >=22.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.16.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
@ -147,79 +122,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/base64-js": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/bindings": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"file-uri-to-path": "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bl": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"buffer": "^5.5.0",
|
|
||||||
"inherits": "^2.0.4",
|
|
||||||
"readable-stream": "^3.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/buffer": {
|
|
||||||
"version": "5.7.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz",
|
|
||||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-js": "^1.3.1",
|
|
||||||
"ieee754": "^1.1.13"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chownr": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/chownr/-/chownr-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/create-require": {
|
"node_modules/create-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
@ -227,39 +129,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/decompress-response": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mimic-response": "^3.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/deep-extend": {
|
|
||||||
"version": "0.6.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz",
|
|
||||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/detect-libc": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||||
@ -270,126 +139,6 @@
|
|||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/end-of-stream": {
|
|
||||||
"version": "1.4.5",
|
|
||||||
"resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
|
||||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"once": "^1.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/env-paths": {
|
|
||||||
"version": "2.2.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz",
|
|
||||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expand-template": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
|
||||||
"license": "(MIT OR WTFPL)",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/exponential-backoff": {
|
|
||||||
"version": "3.1.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
|
|
||||||
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/fdir": {
|
|
||||||
"version": "6.5.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"picomatch": "^3 || ^4"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"picomatch": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/file-uri-to-path": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/fs-constants": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/github-from-package": {
|
|
||||||
"version": "0.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz",
|
|
||||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/graceful-fs": {
|
|
||||||
"version": "4.2.11",
|
|
||||||
"resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/ieee754": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/inherits": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/ini": {
|
|
||||||
"version": "1.3.8",
|
|
||||||
"resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz",
|
|
||||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/isexe": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/make-error": {
|
"node_modules/make-error": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
@ -397,409 +146,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/mimic-response": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minimist": {
|
|
||||||
"version": "1.2.8",
|
|
||||||
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
|
|
||||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minipass": {
|
|
||||||
"version": "7.1.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz",
|
|
||||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minizlib": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"minipass": "^7.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mkdirp-classic": {
|
|
||||||
"version": "0.5.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
|
||||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/napi-build-utils": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/node-abi": {
|
|
||||||
"version": "3.89.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.89.0.tgz",
|
|
||||||
"integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"semver": "^7.3.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-addon-api": {
|
|
||||||
"version": "8.7.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-8.7.0.tgz",
|
|
||||||
"integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^18 || ^20 || >= 21"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-gyp": {
|
|
||||||
"version": "12.3.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/node-gyp/-/node-gyp-12.3.0.tgz",
|
|
||||||
"integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"env-paths": "^2.2.0",
|
|
||||||
"exponential-backoff": "^3.1.1",
|
|
||||||
"graceful-fs": "^4.2.6",
|
|
||||||
"nopt": "^9.0.0",
|
|
||||||
"proc-log": "^6.0.0",
|
|
||||||
"semver": "^7.3.5",
|
|
||||||
"tar": "^7.5.4",
|
|
||||||
"tinyglobby": "^0.2.12",
|
|
||||||
"undici": "^6.25.0",
|
|
||||||
"which": "^6.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"node-gyp": "bin/node-gyp.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.17.0 || >=22.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nopt": {
|
|
||||||
"version": "9.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/nopt/-/nopt-9.0.0.tgz",
|
|
||||||
"integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==",
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"abbrev": "^4.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"nopt": "bin/nopt.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.17.0 || >=22.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/once": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"wrappy": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/picomatch": {
|
|
||||||
"version": "4.0.4",
|
|
||||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
|
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prebuild-install": {
|
|
||||||
"version": "7.1.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
|
||||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
|
||||||
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"detect-libc": "^2.0.0",
|
|
||||||
"expand-template": "^2.0.3",
|
|
||||||
"github-from-package": "0.0.0",
|
|
||||||
"minimist": "^1.2.3",
|
|
||||||
"mkdirp-classic": "^0.5.3",
|
|
||||||
"napi-build-utils": "^2.0.0",
|
|
||||||
"node-abi": "^3.3.0",
|
|
||||||
"pump": "^3.0.0",
|
|
||||||
"rc": "^1.2.7",
|
|
||||||
"simple-get": "^4.0.0",
|
|
||||||
"tar-fs": "^2.0.0",
|
|
||||||
"tunnel-agent": "^0.6.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"prebuild-install": "bin.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/proc-log": {
|
|
||||||
"version": "6.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/proc-log/-/proc-log-6.1.0.tgz",
|
|
||||||
"integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.17.0 || >=22.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pump": {
|
|
||||||
"version": "3.0.4",
|
|
||||||
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz",
|
|
||||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"end-of-stream": "^1.1.0",
|
|
||||||
"once": "^1.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/rc": {
|
|
||||||
"version": "1.2.8",
|
|
||||||
"resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz",
|
|
||||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
|
||||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
|
||||||
"dependencies": {
|
|
||||||
"deep-extend": "^0.6.0",
|
|
||||||
"ini": "~1.3.0",
|
|
||||||
"minimist": "^1.2.0",
|
|
||||||
"strip-json-comments": "~2.0.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"rc": "cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/readable-stream": {
|
|
||||||
"version": "3.6.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
|
|
||||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"inherits": "^2.0.3",
|
|
||||||
"string_decoder": "^1.1.1",
|
|
||||||
"util-deprecate": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/safe-buffer": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
|
||||||
"version": "7.7.4",
|
|
||||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
|
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/simple-concat": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/simple-get": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"decompress-response": "^6.0.0",
|
|
||||||
"once": "^1.3.1",
|
|
||||||
"simple-concat": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sqlite3": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/sqlite3/-/sqlite3-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-X0czUUMG2tmSqJpEQa3tCuZSHKIx8PwM53vLZzKp/o6Rpy25fiVfjdbnZ988M8+O3ZWR1ih0K255VumCb3MAnQ==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"bindings": "^1.5.0",
|
|
||||||
"node-addon-api": "^8.0.0",
|
|
||||||
"prebuild-install": "^7.1.3",
|
|
||||||
"tar": "^7.5.10"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.17.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"node-gyp": "12.x"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"node-gyp": "12.x"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"node-gyp": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/string_decoder": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "~5.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-json-comments": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tar": {
|
|
||||||
"version": "7.5.13",
|
|
||||||
"resolved": "https://registry.npmmirror.com/tar/-/tar-7.5.13.tgz",
|
|
||||||
"integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@isaacs/fs-minipass": "^4.0.0",
|
|
||||||
"chownr": "^3.0.0",
|
|
||||||
"minipass": "^7.1.2",
|
|
||||||
"minizlib": "^3.1.0",
|
|
||||||
"yallist": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tar-fs": {
|
|
||||||
"version": "2.1.4",
|
|
||||||
"resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz",
|
|
||||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chownr": "^1.1.1",
|
|
||||||
"mkdirp-classic": "^0.5.2",
|
|
||||||
"pump": "^3.0.0",
|
|
||||||
"tar-stream": "^2.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tar-fs/node_modules/chownr": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/tar-stream": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"bl": "^4.0.3",
|
|
||||||
"end-of-stream": "^1.4.1",
|
|
||||||
"fs-constants": "^1.0.0",
|
|
||||||
"inherits": "^2.0.3",
|
|
||||||
"readable-stream": "^3.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tinyglobby": {
|
|
||||||
"version": "0.2.16",
|
|
||||||
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
|
||||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"fdir": "^6.5.0",
|
|
||||||
"picomatch": "^4.0.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ts-node": {
|
"node_modules/ts-node": {
|
||||||
"version": "10.9.2",
|
"version": "10.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
@ -844,18 +190,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tunnel-agent": {
|
|
||||||
"version": "0.6.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
|
||||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "^5.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@ -870,16 +204,6 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
|
||||||
"version": "6.25.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/undici/-/undici-6.25.0.tgz",
|
|
||||||
"integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.18.2",
|
"version": "7.18.2",
|
||||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz",
|
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
@ -887,12 +211,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/util-deprecate": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
@ -900,37 +218,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/which": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/which/-/which-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"isexe": "^4.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"node-which": "bin/which.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.17.0 || >=22.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/wrappy": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/yallist": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yn": {
|
"node_modules/yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
|||||||
@ -8,13 +8,5 @@
|
|||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"sqlite3": "^6.0.1"
|
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"onlyBuiltDependencies": [
|
|
||||||
"sqlite3"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
447
pnpm-lock.yaml
generated
447
pnpm-lock.yaml
generated
@ -7,10 +7,6 @@ settings:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
|
||||||
sqlite3:
|
|
||||||
specifier: ^6.0.1
|
|
||||||
version: 6.0.1
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.5.0
|
specifier: ^25.5.0
|
||||||
@ -28,10 +24,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@isaacs/fs-minipass@4.0.1':
|
|
||||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
|
||||||
engines: {node: '>=18.0.0'}
|
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2':
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@ -57,10 +49,6 @@ packages:
|
|||||||
'@types/node@25.6.0':
|
'@types/node@25.6.0':
|
||||||
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||||
|
|
||||||
abbrev@4.0.0:
|
|
||||||
resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==}
|
|
||||||
engines: {node: ^20.17.0 || >=22.9.0}
|
|
||||||
|
|
||||||
acorn-walk@8.3.5:
|
acorn-walk@8.3.5:
|
||||||
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
|
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@ -73,202 +61,16 @@ packages:
|
|||||||
arg@4.1.3:
|
arg@4.1.3:
|
||||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||||
|
|
||||||
base64-js@1.5.1:
|
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
|
||||||
|
|
||||||
bindings@1.5.0:
|
|
||||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
|
||||||
|
|
||||||
bl@4.1.0:
|
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
|
||||||
|
|
||||||
buffer@5.7.1:
|
|
||||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
|
||||||
|
|
||||||
chownr@1.1.4:
|
|
||||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
|
||||||
|
|
||||||
chownr@3.0.0:
|
|
||||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
create-require@1.1.1:
|
create-require@1.1.1:
|
||||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||||
|
|
||||||
decompress-response@6.0.0:
|
|
||||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
deep-extend@0.6.0:
|
|
||||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
|
||||||
engines: {node: '>=4.0.0'}
|
|
||||||
|
|
||||||
detect-libc@2.1.2:
|
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
diff@4.0.4:
|
diff@4.0.4:
|
||||||
resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
|
resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
|
|
||||||
end-of-stream@1.4.5:
|
|
||||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
|
||||||
|
|
||||||
env-paths@2.2.1:
|
|
||||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
expand-template@2.0.3:
|
|
||||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
exponential-backoff@3.1.3:
|
|
||||||
resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
|
|
||||||
|
|
||||||
fdir@6.5.0:
|
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
picomatch: ^3 || ^4
|
|
||||||
peerDependenciesMeta:
|
|
||||||
picomatch:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
|
||||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
|
||||||
|
|
||||||
fs-constants@1.0.0:
|
|
||||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
|
||||||
|
|
||||||
github-from-package@0.0.0:
|
|
||||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
|
||||||
|
|
||||||
graceful-fs@4.2.11:
|
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
|
||||||
|
|
||||||
ieee754@1.2.1:
|
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
|
||||||
|
|
||||||
inherits@2.0.4:
|
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
|
||||||
|
|
||||||
ini@1.3.8:
|
|
||||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
|
||||||
|
|
||||||
isexe@4.0.0:
|
|
||||||
resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==}
|
|
||||||
engines: {node: '>=20'}
|
|
||||||
|
|
||||||
make-error@1.3.6:
|
make-error@1.3.6:
|
||||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||||
|
|
||||||
mimic-response@3.1.0:
|
|
||||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
minimist@1.2.8:
|
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
|
||||||
|
|
||||||
minipass@7.1.3:
|
|
||||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
|
||||||
|
|
||||||
minizlib@3.1.0:
|
|
||||||
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
|
||||||
engines: {node: '>= 18'}
|
|
||||||
|
|
||||||
mkdirp-classic@0.5.3:
|
|
||||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
|
||||||
|
|
||||||
napi-build-utils@2.0.0:
|
|
||||||
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
|
||||||
|
|
||||||
node-abi@3.89.0:
|
|
||||||
resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
node-addon-api@8.7.0:
|
|
||||||
resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==}
|
|
||||||
engines: {node: ^18 || ^20 || >= 21}
|
|
||||||
|
|
||||||
node-gyp@12.3.0:
|
|
||||||
resolution: {integrity: sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==}
|
|
||||||
engines: {node: ^20.17.0 || >=22.9.0}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
nopt@9.0.0:
|
|
||||||
resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==}
|
|
||||||
engines: {node: ^20.17.0 || >=22.9.0}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
once@1.4.0:
|
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
|
||||||
|
|
||||||
picomatch@4.0.4:
|
|
||||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
prebuild-install@7.1.3:
|
|
||||||
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
proc-log@6.1.0:
|
|
||||||
resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==}
|
|
||||||
engines: {node: ^20.17.0 || >=22.9.0}
|
|
||||||
|
|
||||||
pump@3.0.4:
|
|
||||||
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
|
||||||
|
|
||||||
rc@1.2.8:
|
|
||||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
|
||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
|
|
||||||
safe-buffer@5.2.1:
|
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
|
||||||
|
|
||||||
semver@7.7.4:
|
|
||||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
simple-concat@1.0.1:
|
|
||||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
|
||||||
|
|
||||||
simple-get@4.0.1:
|
|
||||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
|
||||||
|
|
||||||
sqlite3@6.0.1:
|
|
||||||
resolution: {integrity: sha512-X0czUUMG2tmSqJpEQa3tCuZSHKIx8PwM53vLZzKp/o6Rpy25fiVfjdbnZ988M8+O3ZWR1ih0K255VumCb3MAnQ==}
|
|
||||||
engines: {node: '>=20.17.0'}
|
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
|
||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
|
||||||
|
|
||||||
strip-json-comments@2.0.1:
|
|
||||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
tar-fs@2.1.4:
|
|
||||||
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
|
||||||
|
|
||||||
tar-stream@2.2.0:
|
|
||||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
tar@7.5.13:
|
|
||||||
resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
tinyglobby@0.2.16:
|
|
||||||
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
ts-node@10.9.2:
|
ts-node@10.9.2:
|
||||||
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
|
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -283,9 +85,6 @@ packages:
|
|||||||
'@swc/wasm':
|
'@swc/wasm':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
tunnel-agent@0.6.0:
|
|
||||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
|
||||||
|
|
||||||
typescript@5.9.3:
|
typescript@5.9.3:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@ -294,28 +93,9 @@ packages:
|
|||||||
undici-types@7.19.2:
|
undici-types@7.19.2:
|
||||||
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
||||||
|
|
||||||
undici@6.25.0:
|
|
||||||
resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==}
|
|
||||||
engines: {node: '>=18.17'}
|
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
|
||||||
|
|
||||||
v8-compile-cache-lib@3.0.1:
|
v8-compile-cache-lib@3.0.1:
|
||||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||||
|
|
||||||
which@6.0.1:
|
|
||||||
resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==}
|
|
||||||
engines: {node: ^20.17.0 || >=22.9.0}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
wrappy@1.0.2:
|
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
|
||||||
|
|
||||||
yallist@5.0.0:
|
|
||||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
yn@3.1.1:
|
yn@3.1.1:
|
||||||
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -326,10 +106,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.9
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
|
|
||||||
'@isaacs/fs-minipass@4.0.1':
|
|
||||||
dependencies:
|
|
||||||
minipass: 7.1.3
|
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2': {}
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
@ -351,9 +127,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.19.2
|
undici-types: 7.19.2
|
||||||
|
|
||||||
abbrev@4.0.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
acorn-walk@8.3.5:
|
acorn-walk@8.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
@ -362,214 +135,12 @@ snapshots:
|
|||||||
|
|
||||||
arg@4.1.3: {}
|
arg@4.1.3: {}
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
|
||||||
|
|
||||||
bindings@1.5.0:
|
|
||||||
dependencies:
|
|
||||||
file-uri-to-path: 1.0.0
|
|
||||||
|
|
||||||
bl@4.1.0:
|
|
||||||
dependencies:
|
|
||||||
buffer: 5.7.1
|
|
||||||
inherits: 2.0.4
|
|
||||||
readable-stream: 3.6.2
|
|
||||||
|
|
||||||
buffer@5.7.1:
|
|
||||||
dependencies:
|
|
||||||
base64-js: 1.5.1
|
|
||||||
ieee754: 1.2.1
|
|
||||||
|
|
||||||
chownr@1.1.4: {}
|
|
||||||
|
|
||||||
chownr@3.0.0: {}
|
|
||||||
|
|
||||||
create-require@1.1.1: {}
|
create-require@1.1.1: {}
|
||||||
|
|
||||||
decompress-response@6.0.0:
|
|
||||||
dependencies:
|
|
||||||
mimic-response: 3.1.0
|
|
||||||
|
|
||||||
deep-extend@0.6.0: {}
|
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
|
||||||
|
|
||||||
diff@4.0.4: {}
|
diff@4.0.4: {}
|
||||||
|
|
||||||
end-of-stream@1.4.5:
|
|
||||||
dependencies:
|
|
||||||
once: 1.4.0
|
|
||||||
|
|
||||||
env-paths@2.2.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
expand-template@2.0.3: {}
|
|
||||||
|
|
||||||
exponential-backoff@3.1.3:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
|
||||||
optionalDependencies:
|
|
||||||
picomatch: 4.0.4
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
file-uri-to-path@1.0.0: {}
|
|
||||||
|
|
||||||
fs-constants@1.0.0: {}
|
|
||||||
|
|
||||||
github-from-package@0.0.0: {}
|
|
||||||
|
|
||||||
graceful-fs@4.2.11:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
ieee754@1.2.1: {}
|
|
||||||
|
|
||||||
inherits@2.0.4: {}
|
|
||||||
|
|
||||||
ini@1.3.8: {}
|
|
||||||
|
|
||||||
isexe@4.0.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
make-error@1.3.6: {}
|
make-error@1.3.6: {}
|
||||||
|
|
||||||
mimic-response@3.1.0: {}
|
|
||||||
|
|
||||||
minimist@1.2.8: {}
|
|
||||||
|
|
||||||
minipass@7.1.3: {}
|
|
||||||
|
|
||||||
minizlib@3.1.0:
|
|
||||||
dependencies:
|
|
||||||
minipass: 7.1.3
|
|
||||||
|
|
||||||
mkdirp-classic@0.5.3: {}
|
|
||||||
|
|
||||||
napi-build-utils@2.0.0: {}
|
|
||||||
|
|
||||||
node-abi@3.89.0:
|
|
||||||
dependencies:
|
|
||||||
semver: 7.7.4
|
|
||||||
|
|
||||||
node-addon-api@8.7.0: {}
|
|
||||||
|
|
||||||
node-gyp@12.3.0:
|
|
||||||
dependencies:
|
|
||||||
env-paths: 2.2.1
|
|
||||||
exponential-backoff: 3.1.3
|
|
||||||
graceful-fs: 4.2.11
|
|
||||||
nopt: 9.0.0
|
|
||||||
proc-log: 6.1.0
|
|
||||||
semver: 7.7.4
|
|
||||||
tar: 7.5.13
|
|
||||||
tinyglobby: 0.2.16
|
|
||||||
undici: 6.25.0
|
|
||||||
which: 6.0.1
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
nopt@9.0.0:
|
|
||||||
dependencies:
|
|
||||||
abbrev: 4.0.0
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
once@1.4.0:
|
|
||||||
dependencies:
|
|
||||||
wrappy: 1.0.2
|
|
||||||
|
|
||||||
picomatch@4.0.4:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
prebuild-install@7.1.3:
|
|
||||||
dependencies:
|
|
||||||
detect-libc: 2.1.2
|
|
||||||
expand-template: 2.0.3
|
|
||||||
github-from-package: 0.0.0
|
|
||||||
minimist: 1.2.8
|
|
||||||
mkdirp-classic: 0.5.3
|
|
||||||
napi-build-utils: 2.0.0
|
|
||||||
node-abi: 3.89.0
|
|
||||||
pump: 3.0.4
|
|
||||||
rc: 1.2.8
|
|
||||||
simple-get: 4.0.1
|
|
||||||
tar-fs: 2.1.4
|
|
||||||
tunnel-agent: 0.6.0
|
|
||||||
|
|
||||||
proc-log@6.1.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
pump@3.0.4:
|
|
||||||
dependencies:
|
|
||||||
end-of-stream: 1.4.5
|
|
||||||
once: 1.4.0
|
|
||||||
|
|
||||||
rc@1.2.8:
|
|
||||||
dependencies:
|
|
||||||
deep-extend: 0.6.0
|
|
||||||
ini: 1.3.8
|
|
||||||
minimist: 1.2.8
|
|
||||||
strip-json-comments: 2.0.1
|
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
|
||||||
dependencies:
|
|
||||||
inherits: 2.0.4
|
|
||||||
string_decoder: 1.3.0
|
|
||||||
util-deprecate: 1.0.2
|
|
||||||
|
|
||||||
safe-buffer@5.2.1: {}
|
|
||||||
|
|
||||||
semver@7.7.4: {}
|
|
||||||
|
|
||||||
simple-concat@1.0.1: {}
|
|
||||||
|
|
||||||
simple-get@4.0.1:
|
|
||||||
dependencies:
|
|
||||||
decompress-response: 6.0.0
|
|
||||||
once: 1.4.0
|
|
||||||
simple-concat: 1.0.1
|
|
||||||
|
|
||||||
sqlite3@6.0.1:
|
|
||||||
dependencies:
|
|
||||||
bindings: 1.5.0
|
|
||||||
node-addon-api: 8.7.0
|
|
||||||
prebuild-install: 7.1.3
|
|
||||||
tar: 7.5.13
|
|
||||||
optionalDependencies:
|
|
||||||
node-gyp: 12.3.0
|
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
strip-json-comments@2.0.1: {}
|
|
||||||
|
|
||||||
tar-fs@2.1.4:
|
|
||||||
dependencies:
|
|
||||||
chownr: 1.1.4
|
|
||||||
mkdirp-classic: 0.5.3
|
|
||||||
pump: 3.0.4
|
|
||||||
tar-stream: 2.2.0
|
|
||||||
|
|
||||||
tar-stream@2.2.0:
|
|
||||||
dependencies:
|
|
||||||
bl: 4.1.0
|
|
||||||
end-of-stream: 1.4.5
|
|
||||||
fs-constants: 1.0.0
|
|
||||||
inherits: 2.0.4
|
|
||||||
readable-stream: 3.6.2
|
|
||||||
|
|
||||||
tar@7.5.13:
|
|
||||||
dependencies:
|
|
||||||
'@isaacs/fs-minipass': 4.0.1
|
|
||||||
chownr: 3.0.0
|
|
||||||
minipass: 7.1.3
|
|
||||||
minizlib: 3.1.0
|
|
||||||
yallist: 5.0.0
|
|
||||||
|
|
||||||
tinyglobby@0.2.16:
|
|
||||||
dependencies:
|
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
|
||||||
picomatch: 4.0.4
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
ts-node@10.9.2(@types/node@25.6.0)(typescript@5.9.3):
|
ts-node@10.9.2(@types/node@25.6.0)(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cspotcode/source-map-support': 0.8.1
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
@ -588,28 +159,10 @@ snapshots:
|
|||||||
v8-compile-cache-lib: 3.0.1
|
v8-compile-cache-lib: 3.0.1
|
||||||
yn: 3.1.1
|
yn: 3.1.1
|
||||||
|
|
||||||
tunnel-agent@0.6.0:
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
undici-types@7.19.2: {}
|
undici-types@7.19.2: {}
|
||||||
|
|
||||||
undici@6.25.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
|
||||||
|
|
||||||
v8-compile-cache-lib@3.0.1: {}
|
v8-compile-cache-lib@3.0.1: {}
|
||||||
|
|
||||||
which@6.0.1:
|
|
||||||
dependencies:
|
|
||||||
isexe: 4.0.0
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
|
||||||
|
|
||||||
yallist@5.0.0: {}
|
|
||||||
|
|
||||||
yn@3.1.1: {}
|
yn@3.1.1: {}
|
||||||
|
|||||||
@ -1,485 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as http from "http";
|
|
||||||
import * as path from "path";
|
|
||||||
import { loadApiList } from "./api-list";
|
|
||||||
import { reloadConfig, saveConfig, saveRoutes, validateMockFiles } from "./config";
|
|
||||||
import {
|
|
||||||
upsertMockFilePathToDb,
|
|
||||||
removeMockFilePathFromDb,
|
|
||||||
insertApiItemToDb,
|
|
||||||
updateApiItemInDb,
|
|
||||||
deleteApiItemFromDb,
|
|
||||||
} from "./db";
|
|
||||||
import {
|
|
||||||
loadMockFiles,
|
|
||||||
normalizeMockFilePath,
|
|
||||||
resolveMockFullPath,
|
|
||||||
} from "./mock-files";
|
|
||||||
import { normalizeStatusCode, readBody } from "./utils";
|
|
||||||
import { state } from "./state";
|
|
||||||
|
|
||||||
async function handleReloadConfig(
|
|
||||||
clientReq: http.IncomingMessage,
|
|
||||||
clientRes: http.ServerResponse,
|
|
||||||
): Promise<void> {
|
|
||||||
if (clientReq.method !== "POST") {
|
|
||||||
clientRes.writeHead(405, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Allow: "POST",
|
|
||||||
});
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: "Method Not Allowed",
|
|
||||||
allow: ["POST"],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const oldCount = Object.keys(state.rawRoutes).length;
|
|
||||||
await reloadConfig();
|
|
||||||
const newCount = Object.keys(state.rawRoutes).length;
|
|
||||||
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
message: "Configuration reloaded successfully",
|
|
||||||
routesCount: newCount,
|
|
||||||
routesChanged: newCount - oldCount,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[ADMIN] 通过API重新加载配置 (路由数: ${oldCount} -> ${newCount})`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: (error as Error).message,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleConfig(
|
|
||||||
clientReq: http.IncomingMessage,
|
|
||||||
clientRes: http.ServerResponse,
|
|
||||||
): Promise<void> {
|
|
||||||
if (clientReq.method !== "GET" && clientReq.method !== "POST") {
|
|
||||||
clientRes.writeHead(405, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Allow: "GET, POST",
|
|
||||||
});
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: "Method Not Allowed",
|
|
||||||
allow: ["GET", "POST"],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientReq.method === "POST") {
|
|
||||||
readBody(clientReq)
|
|
||||||
.then(async (bodyText) => {
|
|
||||||
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
||||||
// 保存服务器配置
|
|
||||||
if (body.config) {
|
|
||||||
await saveConfig(body.config);
|
|
||||||
}
|
|
||||||
// 保存路由配置
|
|
||||||
if (body.routes || body.routeStatuses) {
|
|
||||||
await saveRoutes(
|
|
||||||
body.routes ?? state.rawRoutes,
|
|
||||||
body.routeStatuses ?? state.rawRouteStatuses,
|
|
||||||
body.routeEnabledMap ?? state.routeEnabledMap,
|
|
||||||
body.routeApiNameMap ?? state.routeApiNameMap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
validateMockFiles();
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
message: "Configuration saved successfully",
|
|
||||||
totalRoutes: Object.keys(state.rawRoutes).length,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: (error as Error).message,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
routes: state.rawRoutes,
|
|
||||||
routeStatuses: state.rawRouteStatuses,
|
|
||||||
routeEnabledMap: state.routeEnabledMap,
|
|
||||||
routeApiNameMap: state.routeApiNameMap,
|
|
||||||
config: state.config,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
totalRoutes: Object.keys(state.rawRoutes).length,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: (error as Error).message,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRoutes(
|
|
||||||
clientReq: http.IncomingMessage,
|
|
||||||
clientRes: http.ServerResponse,
|
|
||||||
): Promise<void> {
|
|
||||||
if (clientReq.method !== "POST") {
|
|
||||||
clientRes.writeHead(405, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Allow: "POST",
|
|
||||||
});
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: "Method Not Allowed",
|
|
||||||
allow: ["POST"],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
readBody(clientReq)
|
|
||||||
.then(async (bodyText) => {
|
|
||||||
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
||||||
let route = String(body.route || "").trim();
|
|
||||||
const filePath = String(body.filePath || "").trim();
|
|
||||||
const fileContent = String(body.fileContent || "");
|
|
||||||
const overwrite = body.overwrite === true;
|
|
||||||
const template = String(body.template || "").trim();
|
|
||||||
const apiName = String(body.apiName || "").trim();
|
|
||||||
const selectedApiRoute = String(body.selectedApiRoute || "").trim();
|
|
||||||
const originalRoute = String(body.originalRoute || "").trim();
|
|
||||||
const statusCode = normalizeStatusCode(body.statusCode, 200);
|
|
||||||
const enabled = body.enabled !== false;
|
|
||||||
const useExistingFile =
|
|
||||||
body.useExistingFile === true || template === "basicError";
|
|
||||||
const apiList = await loadApiList();
|
|
||||||
const selectedApi = selectedApiRoute
|
|
||||||
? apiList.find((item) => item.route === selectedApiRoute)
|
|
||||||
: undefined;
|
|
||||||
const originalApi = originalRoute
|
|
||||||
? apiList.find((item) => item.route === originalRoute)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// 来自 api-list 的接口路由不可在新增/修改时变更
|
|
||||||
if (selectedApi) {
|
|
||||||
if (route && route !== selectedApi.route) {
|
|
||||||
throw new Error("api-list route cannot be changed");
|
|
||||||
}
|
|
||||||
if (apiName && apiName !== selectedApi.name) {
|
|
||||||
throw new Error("api-list name cannot be changed");
|
|
||||||
}
|
|
||||||
route = selectedApi.route;
|
|
||||||
}
|
|
||||||
if (originalApi && route !== originalApi.route) {
|
|
||||||
throw new Error("api-list route cannot be changed");
|
|
||||||
}
|
|
||||||
if (originalApi && apiName && apiName !== originalApi.name) {
|
|
||||||
throw new Error("api-list name cannot be changed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!route.startsWith("/")) {
|
|
||||||
throw new Error("route must start with '/'");
|
|
||||||
}
|
|
||||||
const normalizedFilePath = normalizeMockFilePath(filePath);
|
|
||||||
const fullPath = resolveMockFullPath(normalizedFilePath);
|
|
||||||
if (useExistingFile) {
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
throw new Error("mock file does not exist");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!overwrite && fs.existsSync(fullPath)) {
|
|
||||||
throw new Error(
|
|
||||||
"mock file already exists, set overwrite=true to replace",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
||||||
fs.writeFileSync(fullPath, fileContent, "utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建新的路由映射
|
|
||||||
const nextRoutes = { ...state.rawRoutes };
|
|
||||||
const nextStatuses = { ...state.rawRouteStatuses };
|
|
||||||
const nextEnabledMap = { ...state.routeEnabledMap };
|
|
||||||
const nextApiNameMap = { ...state.routeApiNameMap };
|
|
||||||
const oldRouteCandidates = new Set<string>();
|
|
||||||
if (originalRoute) {
|
|
||||||
oldRouteCandidates.add(originalRoute);
|
|
||||||
}
|
|
||||||
oldRouteCandidates.forEach((key) => {
|
|
||||||
if (key && key !== route) {
|
|
||||||
delete nextRoutes[key];
|
|
||||||
delete nextStatuses[key];
|
|
||||||
delete nextEnabledMap[key];
|
|
||||||
delete nextApiNameMap[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
nextRoutes[route] = normalizedFilePath;
|
|
||||||
nextStatuses[route] = statusCode;
|
|
||||||
nextEnabledMap[route] = enabled;
|
|
||||||
nextApiNameMap[route] = apiName;
|
|
||||||
await saveRoutes(nextRoutes, nextStatuses, nextEnabledMap, nextApiNameMap);
|
|
||||||
await upsertMockFilePathToDb(normalizedFilePath);
|
|
||||||
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
message: "Route and mock file created successfully",
|
|
||||||
route,
|
|
||||||
filePath: normalizedFilePath,
|
|
||||||
statusCode,
|
|
||||||
enabled,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: (error as Error).message,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleApiList(
|
|
||||||
clientReq: http.IncomingMessage,
|
|
||||||
clientRes: http.ServerResponse,
|
|
||||||
): Promise<void> {
|
|
||||||
if (clientReq.method !== "GET" && clientReq.method !== "POST" && clientReq.method !== "DELETE") {
|
|
||||||
clientRes.writeHead(405, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Allow: "GET, POST, DELETE",
|
|
||||||
});
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: "Method Not Allowed",
|
|
||||||
allow: ["GET, POST, DELETE"],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientReq.method === "GET") {
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
list: await loadApiList(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
readBody(clientReq)
|
|
||||||
.then(async (bodyText) => {
|
|
||||||
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
||||||
const route = String(body.route || "").trim();
|
|
||||||
const name = String(body.name || "").trim();
|
|
||||||
const originalRoute = String(body.originalRoute || "").trim();
|
|
||||||
|
|
||||||
if (!route || !route.startsWith("/")) {
|
|
||||||
throw new Error("route must start with '/'");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientReq.method === "POST") {
|
|
||||||
if (!name) {
|
|
||||||
throw new Error("name is required");
|
|
||||||
}
|
|
||||||
if (originalRoute && originalRoute !== route) {
|
|
||||||
await updateApiItemInDb(originalRoute, { route, name });
|
|
||||||
} else if (originalRoute) {
|
|
||||||
await updateApiItemInDb(originalRoute, { route, name });
|
|
||||||
} else {
|
|
||||||
await insertApiItemToDb({ route, name });
|
|
||||||
}
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(JSON.stringify({ success: true, route, name }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE
|
|
||||||
await deleteApiItemFromDb(route);
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(JSON.stringify({ success: true, route }));
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: (error as Error).message,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleMockFiles(
|
|
||||||
clientReq: http.IncomingMessage,
|
|
||||||
clientRes: http.ServerResponse,
|
|
||||||
): Promise<void> {
|
|
||||||
if (
|
|
||||||
clientReq.method !== "GET" &&
|
|
||||||
clientReq.method !== "POST" &&
|
|
||||||
clientReq.method !== "DELETE"
|
|
||||||
) {
|
|
||||||
clientRes.writeHead(405, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Allow: "GET, POST, DELETE",
|
|
||||||
});
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: "Method Not Allowed",
|
|
||||||
allow: ["GET, POST, DELETE"],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientReq.method === "GET") {
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
list: await loadMockFiles(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
readBody(clientReq)
|
|
||||||
.then(async (bodyText) => {
|
|
||||||
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
||||||
const filePath = String(body.filePath || "").trim();
|
|
||||||
const alias = String(body.alias || "");
|
|
||||||
const fullPath = resolveMockFullPath(filePath);
|
|
||||||
const normalizedPath = normalizeMockFilePath(filePath);
|
|
||||||
|
|
||||||
if (clientReq.method === "POST") {
|
|
||||||
const content = String(body.content || "");
|
|
||||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
||||||
fs.writeFileSync(fullPath, content, "utf-8");
|
|
||||||
await upsertMockFilePathToDb(normalizedPath, alias);
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
filePath: normalizedPath,
|
|
||||||
alias,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
throw new Error("mock file does not exist");
|
|
||||||
}
|
|
||||||
fs.unlinkSync(fullPath);
|
|
||||||
await removeMockFilePathFromDb(normalizedPath);
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
filePath: normalizedPath,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: (error as Error).message,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAdminPage(
|
|
||||||
clientReq: http.IncomingMessage,
|
|
||||||
clientRes: http.ServerResponse,
|
|
||||||
): Promise<void> {
|
|
||||||
if (clientReq.method !== "GET") {
|
|
||||||
clientRes.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
|
|
||||||
clientRes.end("Method Not Allowed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const adminHtmlPath = path.join(__dirname, "..", "admin.html");
|
|
||||||
if (!fs.existsSync(adminHtmlPath)) {
|
|
||||||
clientRes.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
||||||
clientRes.end("admin.html not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const html = fs.readFileSync(adminHtmlPath, "utf-8");
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
||||||
clientRes.end(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分发管理接口请求。如果请求路径匹配管理端点则处理并返回 true,否则返回 false。
|
|
||||||
*/
|
|
||||||
export async function dispatchAdmin(
|
|
||||||
requestPath: string,
|
|
||||||
clientReq: http.IncomingMessage,
|
|
||||||
clientRes: http.ServerResponse,
|
|
||||||
): Promise<boolean> {
|
|
||||||
switch (requestPath) {
|
|
||||||
case "/__reload-config":
|
|
||||||
await handleReloadConfig(clientReq, clientRes);
|
|
||||||
return true;
|
|
||||||
case "/__config":
|
|
||||||
await handleConfig(clientReq, clientRes);
|
|
||||||
return true;
|
|
||||||
case "/__routes":
|
|
||||||
await handleRoutes(clientReq, clientRes);
|
|
||||||
return true;
|
|
||||||
case "/__api-list":
|
|
||||||
await handleApiList(clientReq, clientRes);
|
|
||||||
return true;
|
|
||||||
case "/__mock-files":
|
|
||||||
await handleMockFiles(clientReq, clientRes);
|
|
||||||
return true;
|
|
||||||
case "/__admin":
|
|
||||||
await handleAdminPage(clientReq, clientRes);
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import { API_LIST_FILE } from "./constants";
|
|
||||||
import { loadApiListFromDb, upsertApiListToDb } from "./db";
|
|
||||||
import type { ApiListItem } from "./types";
|
|
||||||
|
|
||||||
function parseApiListFromJson(): ApiListItem[] {
|
|
||||||
if (!fs.existsSync(API_LIST_FILE)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const text = fs.readFileSync(API_LIST_FILE, "utf-8").trim();
|
|
||||||
if (!text) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const parsed = JSON.parse(text);
|
|
||||||
if (!Array.isArray(parsed)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const seenRoutes = new Set<string>();
|
|
||||||
const list: ApiListItem[] = [];
|
|
||||||
for (const item of parsed) {
|
|
||||||
const route = String(item?.route || "").trim();
|
|
||||||
const name = String(item?.name || "").trim();
|
|
||||||
if (!route || !route.startsWith("/") || !name || seenRoutes.has(route)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seenRoutes.add(route);
|
|
||||||
list.push({ name, route });
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("[ADMIN] 读取 api-list.json 失败:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 一次性迁移:如果 DB 为空且 api-list.json 存在,导入到 DB
|
|
||||||
export async function migrateApiListFromJsonIfNeeded(): Promise<void> {
|
|
||||||
const dbList = await loadApiListFromDb();
|
|
||||||
if (dbList.length > 0) return;
|
|
||||||
const jsonList = parseApiListFromJson();
|
|
||||||
if (jsonList.length > 0) {
|
|
||||||
await upsertApiListToDb(jsonList);
|
|
||||||
console.log(
|
|
||||||
`[BOOTSTRAP] 已从 api-list.json 迁移 ${jsonList.length} 条接口到数据库`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadApiList(): Promise<ApiListItem[]> {
|
|
||||||
return loadApiListFromDb();
|
|
||||||
}
|
|
||||||
112
src/config.ts
112
src/config.ts
@ -1,112 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
import {
|
|
||||||
saveRoutesToDb,
|
|
||||||
loadRoutesFromDb,
|
|
||||||
loadServerConfig,
|
|
||||||
saveServerConfig,
|
|
||||||
} from "./db";
|
|
||||||
import { getActiveRoutes } from "./route-matching";
|
|
||||||
import { state } from "./state";
|
|
||||||
import type { AppConfig, RouteApiNameMap, RouteEnabledMap } from "./types";
|
|
||||||
|
|
||||||
// 验证mock文件是否存在
|
|
||||||
export function validateMockFiles(): void {
|
|
||||||
console.log(`[CONFIG] 验证mock文件...`);
|
|
||||||
const missingFiles: Array<{
|
|
||||||
route: string;
|
|
||||||
filePath: string;
|
|
||||||
fullPath: string;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
const activeRoutes = getActiveRoutes();
|
|
||||||
for (const [route, filePath] of Object.entries(activeRoutes)) {
|
|
||||||
const fullPath = path.join(__dirname, "..", filePath);
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
missingFiles.push({ route, filePath, fullPath });
|
|
||||||
console.warn(
|
|
||||||
`[CONFIG] 警告: mock文件不存在 - ${filePath} (用于路由: ${route})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingFiles.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`[CONFIG] 缺少 ${missingFiles.length} 个mock文件,请创建这些文件`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(`[CONFIG] 所有mock文件验证通过`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从 SQLite 加载全部配置
|
|
||||||
export async function loadConfig(): Promise<void> {
|
|
||||||
try {
|
|
||||||
state.config = await loadServerConfig();
|
|
||||||
|
|
||||||
const dbMappings = await loadRoutesFromDb();
|
|
||||||
state.rawRoutes = dbMappings.routes;
|
|
||||||
state.rawRouteStatuses = dbMappings.routeStatuses;
|
|
||||||
state.routeEnabledMap = dbMappings.routeEnabledMap;
|
|
||||||
state.routeApiNameMap = dbMappings.routeApiNameMap;
|
|
||||||
|
|
||||||
const activeCount = Object.values(state.routeEnabledMap).filter(
|
|
||||||
(v) => v !== false,
|
|
||||||
).length;
|
|
||||||
console.log(
|
|
||||||
`[CONFIG] 配置已加载(路由 ${activeCount} 条)${state.config.mockEnabled !== false ? "" : "(mock 已关闭,全部走代理)"}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
validateMockFiles();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[CONFIG] 加载配置失败:`, error);
|
|
||||||
state.rawRoutes = {};
|
|
||||||
state.rawRouteStatuses = {};
|
|
||||||
state.routeEnabledMap = {};
|
|
||||||
state.routeApiNameMap = {};
|
|
||||||
state.config = {
|
|
||||||
cacheConfig: true,
|
|
||||||
reloadOnChange: true,
|
|
||||||
defaultContentType: "application/json",
|
|
||||||
proxyPort: 8879,
|
|
||||||
targetHost: "localhost",
|
|
||||||
targetPort: 80,
|
|
||||||
targetHttps: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存服务器配置到 SQLite
|
|
||||||
export async function saveConfig(nextConfig: AppConfig): Promise<void> {
|
|
||||||
await saveServerConfig(nextConfig);
|
|
||||||
state.config = nextConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存路由配置到 SQLite 并刷新内存
|
|
||||||
export async function saveRoutes(
|
|
||||||
routes: Record<string, string>,
|
|
||||||
routeStatuses: Record<string, number>,
|
|
||||||
routeEnabledMap: RouteEnabledMap = {},
|
|
||||||
routeApiNameMap: RouteApiNameMap = {},
|
|
||||||
): Promise<void> {
|
|
||||||
await saveRoutesToDb(routes, routeStatuses, routeEnabledMap, routeApiNameMap);
|
|
||||||
const dbMappings = await loadRoutesFromDb();
|
|
||||||
state.rawRoutes = dbMappings.routes;
|
|
||||||
state.rawRouteStatuses = dbMappings.routeStatuses;
|
|
||||||
state.routeEnabledMap = dbMappings.routeEnabledMap;
|
|
||||||
state.routeApiNameMap = dbMappings.routeApiNameMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重新从 SQLite 加载全部配置
|
|
||||||
export async function reloadConfig(): Promise<void> {
|
|
||||||
const oldCount = Object.values(state.routeEnabledMap).filter(
|
|
||||||
(v) => v !== false,
|
|
||||||
).length;
|
|
||||||
await loadConfig();
|
|
||||||
const newCount = Object.values(state.routeEnabledMap).filter(
|
|
||||||
(v) => v !== false,
|
|
||||||
).length;
|
|
||||||
console.log(
|
|
||||||
`[CONFIG] 配置重载完成 (路由数: ${oldCount} -> ${newCount})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import * as path from "path";
|
|
||||||
|
|
||||||
export const API_LIST_FILE = path.join(__dirname, "..", "mock", "api-list.json");
|
|
||||||
export const MOCK_DIR = path.join(__dirname, "..", "mock");
|
|
||||||
export const DATA_DIR = path.join(__dirname, "..", "data");
|
|
||||||
export const DB_FILE = path.join(DATA_DIR, "mock-mappings.sqlite3");
|
|
||||||
export const LEGACY_DB_FILE = path.join(MOCK_DIR, "mock-mappings.sqlite3");
|
|
||||||
293
src/db.ts
293
src/db.ts
@ -1,293 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import sqlite3 from "sqlite3";
|
|
||||||
import { DB_FILE, LEGACY_DB_FILE, MOCK_DIR, DATA_DIR } from "./constants";
|
|
||||||
import { state } from "./state";
|
|
||||||
import { normalizeStatusCode } from "./utils";
|
|
||||||
import type {
|
|
||||||
ApiListItem,
|
|
||||||
AppConfig,
|
|
||||||
MockFileRow,
|
|
||||||
RouteApiNameMap,
|
|
||||||
RouteConfig,
|
|
||||||
RouteEnabledMap,
|
|
||||||
RouteRow,
|
|
||||||
RouteStatusConfig,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
export function dbRun(sql: string, params: unknown[] = []): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
state.db.run(sql, params, (error) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dbAll<T = unknown>(
|
|
||||||
sql: string,
|
|
||||||
params: unknown[] = [],
|
|
||||||
): Promise<T[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
state.db.all(sql, params, (error, rows) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve((rows as T[]) || []);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openDatabase(): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
state.db = new sqlite3.Database(DB_FILE, (error) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initDatabase(): Promise<void> {
|
|
||||||
fs.mkdirSync(MOCK_DIR, { recursive: true });
|
|
||||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
||||||
if (!fs.existsSync(DB_FILE) && fs.existsSync(LEGACY_DB_FILE)) {
|
|
||||||
fs.copyFileSync(LEGACY_DB_FILE, DB_FILE);
|
|
||||||
}
|
|
||||||
await openDatabase();
|
|
||||||
await dbRun(`
|
|
||||||
CREATE TABLE IF NOT EXISTS route_mappings (
|
|
||||||
route_key TEXT PRIMARY KEY,
|
|
||||||
file_path TEXT NOT NULL,
|
|
||||||
status_code INTEGER NOT NULL DEFAULT 200
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
await dbRun(
|
|
||||||
"ALTER TABLE route_mappings ADD COLUMN status_code INTEGER NOT NULL DEFAULT 200",
|
|
||||||
).catch(() => {
|
|
||||||
// ignore when column already exists
|
|
||||||
});
|
|
||||||
await dbRun(
|
|
||||||
"ALTER TABLE route_mappings ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1",
|
|
||||||
).catch(() => {
|
|
||||||
// ignore when column already exists
|
|
||||||
});
|
|
||||||
await dbRun(
|
|
||||||
"ALTER TABLE route_mappings ADD COLUMN api_name TEXT NOT NULL DEFAULT ''",
|
|
||||||
).catch(() => {
|
|
||||||
// ignore when column already exists
|
|
||||||
});
|
|
||||||
// 迁移:将 # 前缀路由转为 enabled=0 并去掉前缀
|
|
||||||
const hashRoutes = await dbAll<RouteRow>(
|
|
||||||
"SELECT route_key, file_path, status_code FROM route_mappings WHERE route_key LIKE '#%'",
|
|
||||||
);
|
|
||||||
for (const row of hashRoutes) {
|
|
||||||
const cleanKey = row.route_key.replace(/^#+/, "");
|
|
||||||
await dbRun("DELETE FROM route_mappings WHERE route_key = ?", [row.route_key]);
|
|
||||||
await dbRun(
|
|
||||||
"INSERT OR IGNORE INTO route_mappings(route_key, file_path, status_code, enabled) VALUES(?, ?, ?, 0)",
|
|
||||||
[cleanKey, row.file_path, row.status_code],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await dbRun(`
|
|
||||||
CREATE TABLE IF NOT EXISTS api_list (
|
|
||||||
route TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
await dbRun(`
|
|
||||||
CREATE TABLE IF NOT EXISTS mock_files (
|
|
||||||
file_path TEXT PRIMARY KEY,
|
|
||||||
alias TEXT NOT NULL DEFAULT ''
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
await dbRun(
|
|
||||||
"ALTER TABLE mock_files ADD COLUMN alias TEXT NOT NULL DEFAULT ''",
|
|
||||||
).catch(() => {
|
|
||||||
// ignore when column already exists
|
|
||||||
});
|
|
||||||
await dbRun(`
|
|
||||||
CREATE TABLE IF NOT EXISTS server_config (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
// 清理不该出现在 mock 列表中的系统文件
|
|
||||||
await dbRun(
|
|
||||||
"DELETE FROM mock_files WHERE file_path = ? OR file_path LIKE ?",
|
|
||||||
["mock/api-list.json", "%.sqlite3"],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveRoutesToDb(
|
|
||||||
routes: RouteConfig,
|
|
||||||
routeStatuses: RouteStatusConfig = {},
|
|
||||||
routeEnabledMap: RouteEnabledMap = {},
|
|
||||||
routeApiNameMap: RouteApiNameMap = {},
|
|
||||||
): Promise<void> {
|
|
||||||
await dbRun("DELETE FROM route_mappings");
|
|
||||||
for (const [routeKey, filePath] of Object.entries(routes)) {
|
|
||||||
const statusCode = normalizeStatusCode(routeStatuses[routeKey], 200);
|
|
||||||
const enabled = routeEnabledMap[routeKey] !== false ? 1 : 0;
|
|
||||||
const apiName = routeApiNameMap[routeKey] || "";
|
|
||||||
await dbRun(
|
|
||||||
"INSERT INTO route_mappings(route_key, file_path, status_code, enabled, api_name) VALUES(?, ?, ?, ?, ?)",
|
|
||||||
[routeKey, filePath, statusCode, enabled, apiName],
|
|
||||||
);
|
|
||||||
await dbRun("INSERT OR IGNORE INTO mock_files(file_path) VALUES(?)", [
|
|
||||||
filePath,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadRoutesFromDb(): Promise<{
|
|
||||||
routes: RouteConfig;
|
|
||||||
routeStatuses: RouteStatusConfig;
|
|
||||||
routeEnabledMap: RouteEnabledMap;
|
|
||||||
routeApiNameMap: RouteApiNameMap;
|
|
||||||
}> {
|
|
||||||
const rows = await dbAll<RouteRow>(
|
|
||||||
"SELECT route_key, file_path, status_code, enabled, api_name FROM route_mappings ORDER BY route_key ASC",
|
|
||||||
);
|
|
||||||
const routes: RouteConfig = {};
|
|
||||||
const routeStatuses: RouteStatusConfig = {};
|
|
||||||
const routeEnabledMap: RouteEnabledMap = {};
|
|
||||||
const routeApiNameMap: RouteApiNameMap = {};
|
|
||||||
for (const row of rows) {
|
|
||||||
routes[row.route_key] = row.file_path;
|
|
||||||
routeStatuses[row.route_key] = normalizeStatusCode(row.status_code, 200);
|
|
||||||
routeEnabledMap[row.route_key] = row.enabled !== 0;
|
|
||||||
if (row.api_name) {
|
|
||||||
routeApiNameMap[row.route_key] = row.api_name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { routes, routeStatuses, routeEnabledMap, routeApiNameMap };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function upsertApiListToDb(list: ApiListItem[]): Promise<void> {
|
|
||||||
await dbRun("DELETE FROM api_list");
|
|
||||||
for (const item of list) {
|
|
||||||
await dbRun("INSERT INTO api_list(route, name) VALUES(?, ?)", [
|
|
||||||
item.route,
|
|
||||||
item.name,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadApiListFromDb(): Promise<ApiListItem[]> {
|
|
||||||
const rows = await dbAll<{ route: string; name: string }>(
|
|
||||||
"SELECT route, name FROM api_list ORDER BY route ASC",
|
|
||||||
);
|
|
||||||
return rows.map((row) => ({ route: row.route, name: row.name }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function insertApiItemToDb(item: ApiListItem): Promise<void> {
|
|
||||||
await dbRun("INSERT INTO api_list(route, name) VALUES(?, ?)", [
|
|
||||||
item.route,
|
|
||||||
item.name,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateApiItemInDb(
|
|
||||||
oldRoute: string,
|
|
||||||
item: ApiListItem,
|
|
||||||
): Promise<void> {
|
|
||||||
await dbRun("UPDATE api_list SET route = ?, name = ? WHERE route = ?", [
|
|
||||||
item.route,
|
|
||||||
item.name,
|
|
||||||
oldRoute,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteApiItemFromDb(route: string): Promise<void> {
|
|
||||||
await dbRun("DELETE FROM api_list WHERE route = ?", [route]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function upsertMockFilePathToDb(
|
|
||||||
filePath: string,
|
|
||||||
alias?: string,
|
|
||||||
): Promise<void> {
|
|
||||||
if (typeof alias === "string") {
|
|
||||||
await dbRun(
|
|
||||||
"INSERT INTO mock_files(file_path, alias) VALUES(?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias",
|
|
||||||
[filePath, alias],
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbRun("INSERT OR IGNORE INTO mock_files(file_path, alias) VALUES(?, '')", [
|
|
||||||
filePath,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeMockFilePathFromDb(filePath: string): Promise<void> {
|
|
||||||
await dbRun("DELETE FROM mock_files WHERE file_path = ?", [filePath]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadMockFilePathsFromDb(): Promise<MockFileRow[]> {
|
|
||||||
const rows = await dbAll<MockFileRow>(
|
|
||||||
"SELECT file_path, alias FROM mock_files ORDER BY file_path ASC",
|
|
||||||
);
|
|
||||||
return rows.map((row) => ({
|
|
||||||
file_path: row.file_path,
|
|
||||||
alias: String(row.alias || ""),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_SERVER_CONFIG: AppConfig = {
|
|
||||||
cacheConfig: true,
|
|
||||||
reloadOnChange: true,
|
|
||||||
defaultContentType: "application/json",
|
|
||||||
proxyPort: 8879,
|
|
||||||
targetHost: "localhost",
|
|
||||||
targetPort: 80,
|
|
||||||
targetHttps: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function loadServerConfig(): Promise<AppConfig> {
|
|
||||||
const rows = await dbAll<{ key: string; value: string }>(
|
|
||||||
"SELECT key, value FROM server_config",
|
|
||||||
);
|
|
||||||
if (rows.length === 0) {
|
|
||||||
return { ...DEFAULT_SERVER_CONFIG };
|
|
||||||
}
|
|
||||||
const map = new Map(rows.map((r) => [r.key, r.value]));
|
|
||||||
return {
|
|
||||||
mockEnabled: map.has("mockEnabled")
|
|
||||||
? map.get("mockEnabled") === "true"
|
|
||||||
: undefined,
|
|
||||||
cacheConfig: map.get("cacheConfig") !== "false",
|
|
||||||
reloadOnChange: map.get("reloadOnChange") !== "false",
|
|
||||||
defaultContentType:
|
|
||||||
map.get("defaultContentType") ||
|
|
||||||
DEFAULT_SERVER_CONFIG.defaultContentType,
|
|
||||||
proxyPort: Number(map.get("proxyPort")) || DEFAULT_SERVER_CONFIG.proxyPort,
|
|
||||||
targetHost: map.get("targetHost") || DEFAULT_SERVER_CONFIG.targetHost,
|
|
||||||
targetPort: Number(map.get("targetPort")) || DEFAULT_SERVER_CONFIG.targetPort,
|
|
||||||
targetHttps: map.get("targetHttps") === "true",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveServerConfig(config: AppConfig): Promise<void> {
|
|
||||||
const entries: [string, string][] = [
|
|
||||||
["mockEnabled", String(config.mockEnabled !== false)],
|
|
||||||
["cacheConfig", String(config.cacheConfig)],
|
|
||||||
["reloadOnChange", String(config.reloadOnChange)],
|
|
||||||
["defaultContentType", config.defaultContentType],
|
|
||||||
["proxyPort", String(config.proxyPort)],
|
|
||||||
["targetHost", config.targetHost],
|
|
||||||
["targetPort", String(config.targetPort ?? "")],
|
|
||||||
["targetHttps", String(config.targetHttps !== false)],
|
|
||||||
];
|
|
||||||
await dbRun("DELETE FROM server_config");
|
|
||||||
for (const [key, value] of entries) {
|
|
||||||
await dbRun("INSERT INTO server_config(key, value) VALUES(?, ?)", [
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
import { MOCK_DIR } from "./constants";
|
|
||||||
import { loadMockFilePathsFromDb, upsertMockFilePathToDb } from "./db";
|
|
||||||
import type { MockFileItem } from "./types";
|
|
||||||
|
|
||||||
export function normalizeMockFilePath(filePath: string): string {
|
|
||||||
const trimmed = filePath.trim().replace(/\\/g, "/");
|
|
||||||
if (!trimmed) {
|
|
||||||
throw new Error("filePath is required");
|
|
||||||
}
|
|
||||||
const relative = trimmed.startsWith("mock/") ? trimmed : `mock/${trimmed}`;
|
|
||||||
if (path.isAbsolute(relative)) {
|
|
||||||
throw new Error("filePath must be a relative path");
|
|
||||||
}
|
|
||||||
return relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveMockFullPath(relativePath: string): string {
|
|
||||||
const normalized = normalizeMockFilePath(relativePath);
|
|
||||||
const fullPath = path.resolve(__dirname, "..", normalized);
|
|
||||||
const mockRoot = path.resolve(MOCK_DIR);
|
|
||||||
if (!fullPath.startsWith(mockRoot)) {
|
|
||||||
throw new Error("filePath is invalid");
|
|
||||||
}
|
|
||||||
if (path.basename(fullPath) === "api-list.json") {
|
|
||||||
throw new Error("api-list.json is read-only in this panel");
|
|
||||||
}
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function walkMockFiles(
|
|
||||||
dir: string,
|
|
||||||
baseDir: string,
|
|
||||||
result: string[],
|
|
||||||
): void {
|
|
||||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
walkMockFiles(fullPath, baseDir, result);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!entry.isFile()) continue;
|
|
||||||
if (entry.name === "api-list.json") continue;
|
|
||||||
if (entry.name.endsWith(".sqlite3")) continue;
|
|
||||||
const relative = path.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
||||||
result.push(`mock/${relative}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadMockFiles(): Promise<MockFileItem[]> {
|
|
||||||
let files = await loadMockFilePathsFromDb();
|
|
||||||
files = files.filter(
|
|
||||||
(item) =>
|
|
||||||
item.file_path !== "mock/api-list.json" &&
|
|
||||||
!item.file_path.endsWith(".sqlite3"),
|
|
||||||
);
|
|
||||||
if (files.length === 0 && fs.existsSync(MOCK_DIR)) {
|
|
||||||
const fsFiles: string[] = [];
|
|
||||||
walkMockFiles(MOCK_DIR, MOCK_DIR, fsFiles);
|
|
||||||
fsFiles.sort((a, b) => a.localeCompare(b));
|
|
||||||
for (const filePath of fsFiles) {
|
|
||||||
await upsertMockFilePathToDb(filePath);
|
|
||||||
}
|
|
||||||
files = fsFiles.map((filePath) => ({ file_path: filePath, alias: "" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: MockFileItem[] = [];
|
|
||||||
for (const item of files) {
|
|
||||||
const filePath = item.file_path;
|
|
||||||
const fullPath = path.join(__dirname, "..", filePath);
|
|
||||||
if (!fs.existsSync(fullPath)) continue;
|
|
||||||
result.push({
|
|
||||||
filePath,
|
|
||||||
alias: String(item.alias || ""),
|
|
||||||
content: fs.readFileSync(fullPath, "utf-8"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initMockFilesFromFsIfNeeded(): Promise<void> {
|
|
||||||
const filesInDb = await loadMockFilePathsFromDb();
|
|
||||||
if (filesInDb.length > 0 || !fs.existsSync(MOCK_DIR)) return;
|
|
||||||
if (!fs.existsSync(MOCK_DIR)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const files: string[] = [];
|
|
||||||
walkMockFiles(MOCK_DIR, MOCK_DIR, files);
|
|
||||||
files.sort((a, b) => a.localeCompare(b));
|
|
||||||
for (const filePath of files) {
|
|
||||||
await upsertMockFilePathToDb(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
211
src/proxy.ts
211
src/proxy.ts
@ -1,211 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as http from "http";
|
|
||||||
import * as https from "https";
|
|
||||||
import * as path from "path";
|
|
||||||
import { dispatchAdmin } from "./admin-handlers";
|
|
||||||
import {
|
|
||||||
getMockFilePath,
|
|
||||||
getMockStatusCode,
|
|
||||||
isMockRoute,
|
|
||||||
} from "./route-matching";
|
|
||||||
import { state } from "./state";
|
|
||||||
import { decodeBodyByEncoding } from "./utils";
|
|
||||||
|
|
||||||
function isTargetHttps(): boolean {
|
|
||||||
return state.config.targetHttps !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTargetPort(): number {
|
|
||||||
if (state.config.targetPort != null) return state.config.targetPort;
|
|
||||||
return isTargetHttps() ? 443 : 80;
|
|
||||||
}
|
|
||||||
|
|
||||||
function upstreamRequest(
|
|
||||||
options: http.RequestOptions,
|
|
||||||
callback: (proxyRes: http.IncomingMessage) => void,
|
|
||||||
): http.ClientRequest {
|
|
||||||
return isTargetHttps()
|
|
||||||
? https.request(options, callback)
|
|
||||||
: http.request(options, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createProxyServer(): http.Server {
|
|
||||||
return http.createServer(async (clientReq, clientRes) => {
|
|
||||||
const parsedUrl = new URL(`http://localhost${clientReq.url!}`);
|
|
||||||
const requestPath = parsedUrl.pathname;
|
|
||||||
|
|
||||||
// 管理接口:保留路径,不参与代理转发
|
|
||||||
const handled = await dispatchAdmin(requestPath, clientReq, clientRes);
|
|
||||||
if (handled) return;
|
|
||||||
|
|
||||||
// 检查是否为需要mock的路由
|
|
||||||
if (isMockRoute(requestPath)) {
|
|
||||||
const mockFile = getMockFilePath(requestPath);
|
|
||||||
const mockStatusCode = getMockStatusCode(requestPath);
|
|
||||||
console.log(`[MOCK] 拦截路由: ${requestPath} -> 使用文件: ${mockFile}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const mockFilePath = path.join(__dirname, "..", mockFile);
|
|
||||||
|
|
||||||
// 检查文件是否存在
|
|
||||||
if (!fs.existsSync(mockFilePath)) {
|
|
||||||
console.warn(
|
|
||||||
`[MOCK] Mock文件不存在,回源并自动生成: ${mockFilePath}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const targetPort = getTargetPort();
|
|
||||||
const options: http.RequestOptions = {
|
|
||||||
hostname: state.config.targetHost,
|
|
||||||
port: targetPort,
|
|
||||||
method: clientReq.method,
|
|
||||||
path: parsedUrl.pathname + parsedUrl.search,
|
|
||||||
headers: {
|
|
||||||
...clientReq.headers,
|
|
||||||
host: state.config.targetHost,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const proxyReq = upstreamRequest(options, (proxyRes) => {
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
proxyRes.on("data", (chunk: Buffer) => {
|
|
||||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
||||||
});
|
|
||||||
|
|
||||||
proxyRes.on("end", () => {
|
|
||||||
const bodyBuffer = Buffer.concat(chunks);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decodedBuffer = decodeBodyByEncoding(
|
|
||||||
bodyBuffer,
|
|
||||||
Array.isArray(proxyRes.headers["content-encoding"])
|
|
||||||
? proxyRes.headers["content-encoding"][0]
|
|
||||||
: proxyRes.headers["content-encoding"],
|
|
||||||
);
|
|
||||||
const contentType = String(
|
|
||||||
proxyRes.headers["content-type"] || "",
|
|
||||||
);
|
|
||||||
const toWrite =
|
|
||||||
contentType.includes("application/json") ||
|
|
||||||
contentType.includes("text/") ||
|
|
||||||
contentType.includes("application/xml") ||
|
|
||||||
contentType.includes("application/javascript")
|
|
||||||
? decodedBuffer.toString("utf-8")
|
|
||||||
: decodedBuffer;
|
|
||||||
fs.mkdirSync(path.dirname(mockFilePath), { recursive: true });
|
|
||||||
fs.writeFileSync(mockFilePath, toWrite);
|
|
||||||
console.log(`[MOCK] 已自动写入mock文件: ${mockFilePath}`);
|
|
||||||
} catch (writeErr) {
|
|
||||||
console.error(
|
|
||||||
`[MOCK] 自动写入mock文件失败: ${mockFilePath}`,
|
|
||||||
writeErr,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
clientRes.writeHead(mockStatusCode, {
|
|
||||||
...proxyRes.headers,
|
|
||||||
"X-Mock-Autogenerated": "true",
|
|
||||||
"X-Mock-Source": mockFile,
|
|
||||||
"X-Mock-Status-Code": String(mockStatusCode),
|
|
||||||
});
|
|
||||||
clientRes.end(bodyBuffer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
proxyReq.on("error", (err) => {
|
|
||||||
console.error("Proxy request error:", err);
|
|
||||||
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
error: "Proxy error",
|
|
||||||
route: requestPath,
|
|
||||||
file: mockFile,
|
|
||||||
message: err.message,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
clientReq.pipe(proxyReq);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取本地mock文件
|
|
||||||
const mockData = fs.readFileSync(mockFilePath, "utf-8");
|
|
||||||
|
|
||||||
// 设置响应头
|
|
||||||
const contentType =
|
|
||||||
state.config.defaultContentType || "application/json";
|
|
||||||
clientRes.writeHead(mockStatusCode, {
|
|
||||||
"Content-Type": contentType,
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
||||||
"Access-Control-Allow-Headers": "Content-Type",
|
|
||||||
"X-Mock-Source": mockFile,
|
|
||||||
"X-Mock-Status-Code": String(mockStatusCode),
|
|
||||||
"X-Mock-Timestamp": new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 返回mock数据
|
|
||||||
clientRes.end(mockData);
|
|
||||||
console.log(`[MOCK] 成功返回mock数据: ${mockFile}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[MOCK] 读取mock文件失败: ${mockFile}`, error);
|
|
||||||
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
error: "Failed to read mock data",
|
|
||||||
route: requestPath,
|
|
||||||
file: mockFile,
|
|
||||||
message: (error as Error).message,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果不是mock路由,正常代理转发
|
|
||||||
console.log(`[PROXY] 转发请求: ${requestPath}`);
|
|
||||||
|
|
||||||
const targetPort = getTargetPort();
|
|
||||||
|
|
||||||
const options: http.RequestOptions = {
|
|
||||||
hostname: state.config.targetHost,
|
|
||||||
port: targetPort,
|
|
||||||
method: clientReq.method,
|
|
||||||
path: parsedUrl.pathname + parsedUrl.search,
|
|
||||||
headers: {
|
|
||||||
...clientReq.headers,
|
|
||||||
host: state.config.targetHost,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const proxyReq = upstreamRequest(options, (proxyRes) => {
|
|
||||||
clientRes.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
|
||||||
proxyRes.pipe(clientRes);
|
|
||||||
});
|
|
||||||
|
|
||||||
proxyReq.on("error", (err) => {
|
|
||||||
console.error("Proxy request error:", err);
|
|
||||||
clientRes.writeHead(500, { "Content-Type": "application/json" });
|
|
||||||
clientRes.end(
|
|
||||||
JSON.stringify({
|
|
||||||
error: "Proxy error",
|
|
||||||
message: err.message,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
clientReq.pipe(proxyReq);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEffectiveTargetPort(): number {
|
|
||||||
if (state.config.targetPort != null) return state.config.targetPort;
|
|
||||||
return isTargetHttps() ? 443 : 80;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEffectiveTargetHttps(): boolean {
|
|
||||||
return isTargetHttps();
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { state } from "./state";
|
|
||||||
import { normalizeStatusCode } from "./utils";
|
|
||||||
|
|
||||||
// 检查是否为mock路由的函数
|
|
||||||
export function isMockRoute(requestPath: string): boolean {
|
|
||||||
if (state.config.mockEnabled === false) return false;
|
|
||||||
if (!state.rawRoutes.hasOwnProperty(requestPath)) return false;
|
|
||||||
return state.routeEnabledMap[requestPath] !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取mock文件路径
|
|
||||||
export function getMockFilePath(requestPath: string): string {
|
|
||||||
return state.rawRoutes[requestPath];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMockStatusCode(requestPath: string): number {
|
|
||||||
return normalizeStatusCode(state.rawRouteStatuses[requestPath], 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取仅启用的路由映射(用于展示)
|
|
||||||
export function getActiveRoutes(): Record<string, string> {
|
|
||||||
const result: Record<string, string> = {};
|
|
||||||
for (const [route, filePath] of Object.entries(state.rawRoutes)) {
|
|
||||||
if (state.routeEnabledMap[route] !== false) {
|
|
||||||
result[route] = filePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
18
src/state.ts
18
src/state.ts
@ -1,18 +0,0 @@
|
|||||||
import type sqlite3 from "sqlite3";
|
|
||||||
import type { AppConfig, RouteApiNameMap, RouteConfig, RouteEnabledMap, RouteStatusConfig } from "./types";
|
|
||||||
|
|
||||||
export const state = {
|
|
||||||
rawRoutes: {} as RouteConfig,
|
|
||||||
rawRouteStatuses: {} as RouteStatusConfig,
|
|
||||||
routeEnabledMap: {} as RouteEnabledMap,
|
|
||||||
routeApiNameMap: {} as RouteApiNameMap,
|
|
||||||
config: {
|
|
||||||
cacheConfig: true,
|
|
||||||
reloadOnChange: true,
|
|
||||||
defaultContentType: "application/json",
|
|
||||||
proxyPort: 443,
|
|
||||||
targetHost: "localhost",
|
|
||||||
targetPort: 443,
|
|
||||||
} as AppConfig,
|
|
||||||
db: null as unknown as sqlite3.Database,
|
|
||||||
};
|
|
||||||
59
src/types.ts
59
src/types.ts
@ -1,59 +0,0 @@
|
|||||||
export interface RouteConfig {
|
|
||||||
[route: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouteStatusConfig {
|
|
||||||
[route: string]: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppConfig {
|
|
||||||
/** 为 false 时所有请求走代理,不命中 mock 路由;缺省为 true */
|
|
||||||
mockEnabled?: boolean;
|
|
||||||
cacheConfig: boolean;
|
|
||||||
reloadOnChange: boolean;
|
|
||||||
defaultContentType: string;
|
|
||||||
proxyPort: number;
|
|
||||||
targetHost: string;
|
|
||||||
/** 代理转发的目标端口;缺省 HTTPS 为 443,HTTP 为 80 */
|
|
||||||
targetPort?: number;
|
|
||||||
/** 为 false 时用 HTTP 连接上游;缺省 true(HTTPS) */
|
|
||||||
targetHttps?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigFile {
|
|
||||||
routes: RouteConfig;
|
|
||||||
routeStatuses?: RouteStatusConfig;
|
|
||||||
config: AppConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiListItem {
|
|
||||||
name: string;
|
|
||||||
route: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MockFileItem {
|
|
||||||
filePath: string;
|
|
||||||
alias: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouteRow {
|
|
||||||
route_key: string;
|
|
||||||
file_path: string;
|
|
||||||
status_code: number;
|
|
||||||
enabled: number;
|
|
||||||
api_name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouteEnabledMap {
|
|
||||||
[route: string]: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouteApiNameMap {
|
|
||||||
[route: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MockFileRow {
|
|
||||||
file_path: string;
|
|
||||||
alias: string;
|
|
||||||
}
|
|
||||||
43
src/utils.ts
43
src/utils.ts
@ -1,43 +0,0 @@
|
|||||||
import * as http from "http";
|
|
||||||
import * as zlib from "zlib";
|
|
||||||
|
|
||||||
export function normalizeStatusCode(input: unknown, fallback = 200): number {
|
|
||||||
const numeric =
|
|
||||||
typeof input === "number" ? input : Number.parseInt(String(input ?? ""), 10);
|
|
||||||
if (Number.isInteger(numeric) && numeric >= 100 && numeric <= 599) {
|
|
||||||
return numeric;
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readBody(req: http.IncomingMessage): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
req.on("data", (chunk: Buffer) => {
|
|
||||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
||||||
});
|
|
||||||
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
||||||
req.on("error", reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeBodyByEncoding(
|
|
||||||
bodyBuffer: Buffer,
|
|
||||||
contentEncoding?: string,
|
|
||||||
): Buffer {
|
|
||||||
const encoding = (contentEncoding || "").toLowerCase().trim();
|
|
||||||
try {
|
|
||||||
if (encoding.includes("gzip")) {
|
|
||||||
return zlib.gunzipSync(bodyBuffer);
|
|
||||||
}
|
|
||||||
if (encoding.includes("br")) {
|
|
||||||
return zlib.brotliDecompressSync(bodyBuffer);
|
|
||||||
}
|
|
||||||
if (encoding.includes("deflate")) {
|
|
||||||
return zlib.inflateSync(bodyBuffer);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("[MOCK] 解压上游响应失败,按原始内容写入文件", error);
|
|
||||||
}
|
|
||||||
return bodyBuffer;
|
|
||||||
}
|
|
||||||
@ -9,5 +9,5 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noEmit": true
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": ["*.ts", "src/**/*.ts"]
|
"include": ["*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user