Commit 2674df7a by inrgihc

代码优化与测试修复

parent 48304bfa
......@@ -3,24 +3,24 @@
![SQLREST](docs/images/SQLREST.PNG#pic_center)
> 将 SQL 查询转化为 RESTful API的便捷工具
> 将 SQL 操作转化为 RESTful API的便捷工具
## 一、工具介绍
SQLREST 是一个开源项目,旨在提供一种简单而强大的方式来将 SQL 查询转化为 RESTful API。它支持多种数据库,允许用户通过配置 SQL 语句来创建 API,无
SQLREST 是一个开源项目,旨在提供一种简单而强大的方式来将 SQL 操作转化为 RESTful API。它支持多种数据库,允许用户通过配置 SQL 语句来创建 API,无
需编写复杂的后端逻辑,用户只需选择数据源、输入SQL或脚本、简单path配置即可快速生成API接口。
### 1、功能介绍
SQLREST的功能包括:
- **SQL直接构建API**:通过配置SQL和参数即可生成 RESTful API。
- **SQL直接构建API**:通过配置增删改查SQL和参数即可生成 RESTful API。
- **多数据库支持**:支持常见的20+种数据库,其中包含多款国产数据库。
- **MyBatis语法支持**:支持MyBatis的动态SQL语法。
- **Groovy脚本支持**:支持groovy语法构建复杂场景下的接口。
- **参数类型支持**:支持整型/浮点型/时间/日期/布尔/字符串/对象等多种类型。
- **ContentType支持**:支持application/x-www-form-urlencoded及application/json等多种请求格式。
- **身份认证**:提供基于 Token 的认证机制,保护 API 安全。
- **身份认证支持**:提供基于 Token 的认证机制,保护 API 安全。
- **Swagger在线文档**:支持自动生成swagger-ui的在线接口文档。
- **缓存配置支持**:支持 Hazelcast 和 Redis 缓存,提升 API 访问性能。
- **流控配置管理**:通过 Sentinel 支持流量控制,防止系统过载。
......
......@@ -45,6 +45,8 @@ public abstract class Constants {
public static final String SYS_PARAM_KEY_SWAGGER_INFO_DESCRIPTION = "apiDocInfoDescription";
public static final String SYS_PARAM_KEY_MCP_TOOL_LIST_PAGE_SIZE = "mcpToolListPageSize";
public static final String DEFAULT_SSE_TOKEN_PRAM_NAME = "token";
public static final String DEFAULT_SSE_ENDPOINT = "/mcp/sse";
public static final String MESSAGE_ENDPOINT = "/mcp/message";
......
......@@ -79,6 +79,7 @@ public class ApiExecuteService {
}
public ResultEntity<Object> execute(ApiAssignmentEntity config, HttpServletRequest request) {
String resourceName = Constants.getResourceName(config.getMethod().name(), config.getPath());
try {
List<ItemParam> invalidArgs = new ArrayList<>();
Map<String, Object> paramValues = mergeParameters(request, config.getParams(), invalidArgs);
......@@ -87,10 +88,10 @@ public class ApiExecuteService {
}
return execute(config, paramValues);
} catch (CommonException e) {
log.warn("<SR出错>参数校验错误,错误消息:" + e.getMessage(), e);
log.warn("Failed to valid parameters for {}, error:{}", resourceName, e.getMessage());
return ResultEntity.failed(e.getCode(), e.getMessage());
} catch (Throwable t) {
log.warn("<SR出错>方法执行出错,错误消息:" + t.getMessage(), t);
log.warn("Failed to execute for {}, error:{}", resourceName, t.getMessage());
return ResultEntity.failed(ResponseErrorCode.ERROR_INTERNAL_ERROR, ExceptionUtil.getMessage(t));
}
}
......
......@@ -78,7 +78,7 @@ public class AuthenticationFilter implements Filter {
String message = String.format("/%s/%s[%s]", Constants.API_PATH_PREFIX, path, method.name());
ResultEntity result = ResultEntity.failed(ResponseErrorCode.ERROR_PATH_NOT_EXISTS, message);
response.getWriter().append(JSONUtil.toJsonStr(result));
log.warn("<SR出错>404路径不存在:{}", message);
log.warn("Request path not exists: {}", message);
return;
}
......
......@@ -9,13 +9,16 @@
/////////////////////////////////////////////////////////////
package com.gitee.sqlrest.core.service;
import com.gitee.sqlrest.common.enums.ParamTypeEnum;
import com.gitee.sqlrest.common.exception.CommonException;
import com.gitee.sqlrest.common.exception.ResponseErrorCode;
import com.gitee.sqlrest.persistence.dao.SystemParamDao;
import com.gitee.sqlrest.persistence.entity.SystemParamEntity;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SystemParamService {
......@@ -44,4 +47,20 @@ public class SystemParamService {
}
systemParamDao.updateByParamKey(key, String.valueOf(paramValue));
}
public Integer getIntByParamKey(String key, int defaultValue) {
SystemParamEntity entity = systemParamDao.getByParamKey(key);
if (null == entity) {
return defaultValue;
}
if (ParamTypeEnum.LONG.equals(entity.getParamType())) {
try {
return Integer.parseInt(entity.getParamValue());
} catch (Exception e) {
log.warn("Read system param integer value by key={} failed,use default value={},error:{} ",
key, defaultValue, e.getMessage());
}
}
return defaultValue;
}
}
......@@ -12,19 +12,16 @@ package com.gitee.sqlrest.core.servlet;
import com.gitee.sqlrest.common.consts.Constants;
import com.gitee.sqlrest.common.dto.ResultEntity;
import com.gitee.sqlrest.common.enums.HttpMethodEnum;
import com.gitee.sqlrest.common.exception.ResponseErrorCode;
import com.gitee.sqlrest.core.exec.ApiExecuteService;
import com.gitee.sqlrest.core.util.JacksonUtils;
import com.gitee.sqlrest.persistence.dao.ApiAssignmentDao;
import com.gitee.sqlrest.persistence.entity.ApiAssignmentEntity;
import com.google.common.base.Charsets;
import java.io.IOException;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
@Slf4j
......@@ -39,16 +36,8 @@ public class ApiServletService {
public void process(HttpMethodEnum method, HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getRequestURI().substring(Constants.API_PATH_PREFIX.length() + 2);
ResultEntity result = ResultEntity.success();
ApiAssignmentEntity apiConfigEntity = apiAssignmentDao.getByUk(method, path);
if (null == apiConfigEntity || !apiConfigEntity.getStatus()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
String message = String.format("/%s/%s[%s]", Constants.API_PATH_PREFIX, path, method.name());
result = ResultEntity.failed(ResponseErrorCode.ERROR_PATH_NOT_EXISTS, message);
log.warn("<SR出错>404路径不存在:{}", message);
} else {
result = apiExecuteService.execute(apiConfigEntity, request);
}
ResultEntity result = apiExecuteService.execute(apiConfigEntity, request);
if (0 != result.getCode()) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
......
......@@ -40,6 +40,14 @@
<p>
执行器基于sentinel支持接口的流量控制功能。
</p>
<li>支持接口的缓存配置功能</li>
<p>
执行器支持哈希及SpEL表达式方式配置缓存功能。
</p>
<li>支持将接口转换为MCP工具功能</li>
<p>
Manager支持作为MCP服务端提供Tool功能。
</p>
</ul>
</div>
</el-card>
......@@ -65,9 +73,8 @@
<li>MariaDB
</li>
<li>微软的Microsoft SQLServer
<li>PostgreSQL
</li>
<li>Greenplum(需使用PostgreSQL类型)
<li>PostgreSQL/Greenplum
</li>
<li>IBM的DB2
</li>
......@@ -97,6 +104,8 @@
</li>
<li>OceanBase
</li>
<li>TDengine
</li>
</ul>
</div>
</el-card>
......@@ -118,6 +127,7 @@
<pre>
└── sqlrest
├── sqlrest-common // sqlrest通用定义模块
├── sqlrest-mcp // sqlrest的MCP协议模块
├── sqlrest-template // sqlrest的SQL内容模板模块
├── sqlrest-persistence // sqlrest的数据库持久化模块
├── sqlrest-core // sqlrest-core的接口实现模块
......
......@@ -203,7 +203,7 @@
:key="index"
:label="item.name"
:value="item.value"
v-if="shouldShowOption(item,scope.row)"></el-option>
v-if="shouldInputShowOption(item,scope.row)"></el-option>
</el-select>
</template>
</el-table-column>
......@@ -285,7 +285,8 @@
<el-option v-for="(item,index) in paramTypeList"
:key="index"
:label="item.name"
:value="item.value"></el-option>
:value="item.value"
v-if="shouldOutputShowOption(item,scope.row)"></el-option>
</el-select>
</template>
</el-table-column>
......@@ -301,8 +302,12 @@
v-if="!isOnlyShowDetail"
min-width="25%">
<template slot-scope="scope">
<el-link icon="el-icon-plus"
v-if="scope.row.type=='OBJECT'"
@click="addOutputSubParamsItem(scope.row)"></el-link>
&nbsp;&nbsp;&nbsp;&nbsp;
<el-link icon="el-icon-delete"
@click="deleteOutputParamsItem(scope.$index)"></el-link>
@click="deleteOutputParamsItem(scope.$index,scope.row)"></el-link>
</template>
</el-table-column>
</el-table>
......@@ -1352,13 +1357,13 @@ export default {
);
}
},
shouldShowOption: function (item, row) {
if (this.getParentRow(row)) {
shouldInputShowOption: function (item, row) {
if (this.getInputParamsParentRow(row)) {
return item.value != 'OBJECT';
}
return true;
},
getParentRow: function (childRow) {
getInputParamsParentRow: function (childRow) {
for (const row of this.inputParams) {
if (row.children && row.children.includes(childRow)) {
return row;
......@@ -1376,7 +1381,7 @@ export default {
},
deleteInputSubParamsItem: function (index, childRow) {
// 通过 childRow 访问父级行数据
const parentRow = this.getParentRow(childRow);
const parentRow = this.getInputParamsParentRow(childRow);
if (parentRow) {
const childIndex = parentRow.children.indexOf(childRow);
if (childIndex !== -1) {
......@@ -1707,10 +1712,72 @@ export default {
},
)
},
deleteOutputParamsItem: function (index) {
this.outputParams.splice(index, 1);
shouldOutputShowOption: function (item, row) {
if (this.getOutputParamsParentRow(row)) {
return item.value != 'OBJECT';
}
return true;
},
getOutputParamsParentRow: function (childRow) {
for (const row of this.outputParams) {
if (row.children && row.children.includes(childRow)) {
return row;
}
}
return null;
},
addOutputSubParamsItem: function (row) {
const index = this.outputParams.findIndex(item => row == item)
if (index !== -1) {
if (!this.outputParams[index].children) {
// 如果还没有 children 数组,则创建它
// 使用 Vue.set 来确保响应性
Vue.set(this.outputParams[index], 'children', []);
}
this.outputParams[index].location = 'REQUEST_BODY';
this.outputParams[index].type = 'OBJECT';
this.outputParams[index].children.push(
{
id: this.uuid(),
name: "",
type: "STRING",
location: 'REQUEST_BODY',
isArray: false,
required: true,
defaultValue: "",
remark: ""
},
);
} else {
row.type = 'STRING';
this.$alert('只允许嵌套一层,类型被还原为字符串类型', "操作提示",
{
confirmButtonText: "确定",
type: "info"
}
);
}
},
deleteOutputParamsItem: function (idx, row) {
const index = this.outputParams.indexOf(row);
if (index !== -1) {
this.outputParams.splice(index, 1);
} else {
this.deleteOutputSubParamsItem(idx, row);
}
},
deleteOutputSubParamsItem: function (index, childRow) {
// 通过 childRow 访问父级行数据
const parentRow = this.getOutputParamsParentRow(childRow);
if (parentRow) {
const childIndex = parentRow.children.indexOf(childRow);
if (childIndex !== -1) {
parentRow.children.splice(childIndex, 1);
} else {
console.warn('Child not found');
}
}
},
},
created () {
this.loadAssignmentDetail();
......
......@@ -14,10 +14,13 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.gitee.sqlrest.common.consts.Constants;
import com.gitee.sqlrest.common.dto.BaseParam;
import com.gitee.sqlrest.common.dto.ItemParam;
import com.gitee.sqlrest.common.dto.ResultEntity;
import com.gitee.sqlrest.common.enums.ParamTypeEnum;
import com.gitee.sqlrest.core.exec.ApiExecuteService;
import com.gitee.sqlrest.core.service.SystemParamService;
import com.gitee.sqlrest.core.util.JacksonUtils;
import com.gitee.sqlrest.persistence.entity.ApiAssignmentEntity;
import com.google.common.collect.Lists;
......@@ -48,13 +51,15 @@ public class McpToolCallHandler {
private final String toolDescription;
private final ApiAssignmentEntity config;
private final ApiExecuteService apiExecuteService;
private final int defaultPageSize;
public McpToolCallHandler(String toolName, String toolDescription,
ApiAssignmentEntity config) {
public McpToolCallHandler(String toolName, String description, ApiAssignmentEntity config) {
this.toolName = toolName;
this.toolDescription = toolDescription;
this.toolDescription = description;
this.config = config;
this.apiExecuteService = SpringUtil.getBean(ApiExecuteService.class);
this.defaultPageSize = SpringUtil.getBean(SystemParamService.class)
.getIntByParamKey(Constants.SYS_PARAM_KEY_MCP_TOOL_LIST_PAGE_SIZE, 1000);
}
public McpSchema.Tool getMcpToolSchema() {
......@@ -110,15 +115,35 @@ public class McpToolCallHandler {
}
public CallToolResult executeTool(McpSyncServerExchange exchange, Map<String, Object> arguments) {
prepareArgumentsPageSizeParameter(arguments);
ResultEntity<Object> resultEntity = apiExecuteService.execute(config, arguments);
if (0 == resultEntity.getCode()) {
String json = JacksonUtils.toJsonStr(resultEntity.getData(), config.getResponseFormat());
McpSchema.TextContent content = new McpSchema.TextContent("获取JSON格式的数据为:\n " + json);
McpSchema.TextContent content = new McpSchema.TextContent("操作成功,JSON格式的响应数据为:\n " + json);
return new McpSchema.CallToolResult(Lists.newArrayList(content), false);
} else {
String message = resultEntity.getMessage();
McpSchema.TextContent content = new McpSchema.TextContent("获取数据异常:\n " + message);
McpSchema.TextContent content = new McpSchema.TextContent("操作异常,JSON格式的响应数据为:\n " + message);
return new McpSchema.CallToolResult(Lists.newArrayList(content), true);
}
}
private void prepareArgumentsPageSizeParameter(Map<String, Object> arguments) {
if (CollectionUtils.isNotEmpty(config.getParams())) {
for (ItemParam param : config.getParams()) {
String name = param.getName();
ParamTypeEnum type = param.getType();
Boolean required = param.getRequired();
String defaultValue = param.getDefaultValue();
if (!required && !arguments.containsKey(param.getName())) {
if (!type.isObject()) {
arguments.put(name, type.getConverter().apply(defaultValue));
}
}
}
}
if (!arguments.containsKey(Constants.PARAM_PAGE_SIZE)) {
arguments.put(Constants.PARAM_PAGE_SIZE, defaultPageSize);
}
}
}
......@@ -18,6 +18,7 @@ import com.gitee.sqlrest.common.util.TokenUtils;
import com.gitee.sqlrest.core.dto.EntitySearchRequest;
import com.gitee.sqlrest.core.dto.McpToolResponse;
import com.gitee.sqlrest.core.dto.McpToolSaveRequest;
import com.gitee.sqlrest.core.util.ApiPathUtils;
import com.gitee.sqlrest.manager.model.McpToolCallHandler;
import com.gitee.sqlrest.persistence.dao.ApiAssignmentDao;
import com.gitee.sqlrest.persistence.dao.ApiModuleDao;
......@@ -205,7 +206,7 @@ public class McpManageService {
.apiId(toolEntity.getApiId())
.apiName(config.getName())
.apiMethod(config.getMethod().name())
.apiPath(config.getPath())
.apiPath(ApiPathUtils.getFullPath(config.getPath()))
.createTime(toolEntity.getCreateTime())
.updateTime(toolEntity.getUpdateTime())
.build();
......
......@@ -10,4 +10,6 @@ databaseChangeLog:
- include:
file: classpath:db/changelog/log-v1.2.1.yaml
- include:
file: classpath:db/changelog/log-v1.2.2.yaml
\ No newline at end of file
file: classpath:db/changelog/log-v1.2.2.yaml
- include:
file: classpath:db/changelog/log-v1.3.1.yaml
\ No newline at end of file
databaseChangeLog:
- changeSet:
id: 1.3.1
author: sqlrest
runOnChange: false
changes:
- sqlFile:
encoding: UTF-8
path: db/migration/V1_3_1__system-ddl.sql
INSERT INTO `SQLREST_SYSTEM_PARAM` (`param_key`, `param_type`, `param_value`) VALUES ('mcpToolListPageSize', 'LONG', '1000');
\ No newline at end of file
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>SQLREST工具</title><link href=/static/css/app.a8c22f6b9b014005bd869232cbdaa4b4.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=/static/js/manifest.d9be689ba09af216a8ce.js></script><script type=text/javascript src=/static/js/vendor.a6ebeac16c85a178c5e7.js></script><script type=text/javascript src=/static/js/app.e892e4af016c82ce3f40.js></script></body></html>
\ No newline at end of file
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>SQLREST工具</title><link href=/static/css/app.a0d428dcff1407fb55c58a847b2839b3.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=/static/js/manifest.52df910f8eccdd2a9197.js></script><script type=text/javascript src=/static/js/vendor.a6ebeac16c85a178c5e7.js></script><script type=text/javascript src=/static/js/app.e892e4af016c82ce3f40.js></script></body></html>
\ No newline at end of file
......@@ -10,4 +10,6 @@ databaseChangeLog:
- include:
file: classpath:pg/changelog/log-v1.2.1.yaml
- include:
file: classpath:pg/changelog/log-v1.2.2.yaml
\ No newline at end of file
file: classpath:pg/changelog/log-v1.2.2.yaml
- include:
file: classpath:pg/changelog/log-v1.3.1.yaml
\ No newline at end of file
databaseChangeLog:
- changeSet:
id: 1.3.1
author: sqlrest
runOnChange: false
changes:
- sqlFile:
encoding: UTF-8
path: pg/migration/V1_3_1__system-ddl.sql
INSERT INTO SQLREST_SYSTEM_PARAM ("param_key", "param_type", "param_value") VALUES ('mcpToolListPageSize', 'LONG', '1000');
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
!function(e){var c=window.webpackJsonp;window.webpackJsonp=function(a,t,f){for(var o,d,i,u=0,b=[];u<a.length;u++)d=a[u],n[d]&&b.push(n[d][0]),n[d]=0;for(o in t)Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);for(c&&c(a,t,f);b.length;)b.shift()();if(f)for(u=0;u<f.length;u++)i=r(r.s=f[u]);return i};var a={},n={28:0};function r(c){if(a[c])return a[c].exports;var n=a[c]={i:c,l:!1,exports:{}};return e[c].call(n.exports,n,n.exports,r),n.l=!0,n.exports}r.e=function(e){var c=n[e];if(0===c)return new Promise(function(e){e()});if(c)return c[2];var a=new Promise(function(a,r){c=n[e]=[a,r]});c[2]=a;var t=document.getElementsByTagName("head")[0],f=document.createElement("script");f.type="text/javascript",f.charset="utf-8",f.async=!0,f.timeout=12e4,r.nc&&f.setAttribute("nonce",r.nc),f.src=r.p+"static/js/"+e+"."+{0:"067f6a1af8c2636542e3",1:"7acdf4fa190624b33517",2:"140338f6a5528feea1a3",3:"776d791724a8de12ff9e",4:"f8494b8dd039413f79c8",5:"404ff5302fc6ee181ee9",6:"8f85de06573e2a5f9562",7:"061807fe4716131f26f8",8:"c4a2e9952c298efc080c",9:"313072ac394fc9349d2f",10:"0591dbe3e75f89e4c00e",11:"9ccf6e8ba19ce146e66b",12:"d26d5fe93a45bd5c6eaa",13:"bfc06db76836c228f491",14:"b74db4e8e5c2f13b4c43",15:"d80f5cf2fd51d72b22ea",16:"2ca69ffb53da98b61da4",17:"64a6906a4b77392d43c7",18:"bb8da82a2138ed7b18a8",19:"a2c24b3aee6af674aa30",20:"1272deb68d764581e1ab",21:"5952cc009e9eb6c25dfc",22:"0a5f684edcb8df816a5d",23:"93251b045354cc966a59",24:"61c786230b6da7b9ddc7",25:"e85f7ab22c459c102670"}[e]+".js";var o=setTimeout(d,12e4);function d(){f.onerror=f.onload=null,clearTimeout(o);var c=n[e];0!==c&&(c&&c[1](new Error("Loading chunk "+e+" failed.")),n[e]=void 0)}return f.onerror=f.onload=d,t.appendChild(f),a},r.m=e,r.c=a,r.d=function(e,c,a){r.o(e,c)||Object.defineProperty(e,c,{configurable:!1,enumerable:!0,get:a})},r.n=function(e){var c=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(c,"a",c),c},r.o=function(e,c){return Object.prototype.hasOwnProperty.call(e,c)},r.p="/",r.oe=function(e){throw console.error(e),e}}([]);
//# sourceMappingURL=manifest.52df910f8eccdd2a9197.js.map
\ No newline at end of file
!function(e){var c=window.webpackJsonp;window.webpackJsonp=function(n,a,f){for(var o,d,b,i=0,u=[];i<n.length;i++)d=n[i],r[d]&&u.push(r[d][0]),r[d]=0;for(o in a)Object.prototype.hasOwnProperty.call(a,o)&&(e[o]=a[o]);for(c&&c(n,a,f);u.length;)u.shift()();if(f)for(i=0;i<f.length;i++)b=t(t.s=f[i]);return b};var n={},r={28:0};function t(c){if(n[c])return n[c].exports;var r=n[c]={i:c,l:!1,exports:{}};return e[c].call(r.exports,r,r.exports,t),r.l=!0,r.exports}t.e=function(e){var c=r[e];if(0===c)return new Promise(function(e){e()});if(c)return c[2];var n=new Promise(function(n,t){c=r[e]=[n,t]});c[2]=n;var a=document.getElementsByTagName("head")[0],f=document.createElement("script");f.type="text/javascript",f.charset="utf-8",f.async=!0,f.timeout=12e4,t.nc&&f.setAttribute("nonce",t.nc),f.src=t.p+"static/js/"+e+"."+{0:"f72dbb9d754b88a9e6e3",1:"b17200cccd46e216dcb3",2:"140338f6a5528feea1a3",3:"776d791724a8de12ff9e",4:"f8494b8dd039413f79c8",5:"404ff5302fc6ee181ee9",6:"8f85de06573e2a5f9562",7:"061807fe4716131f26f8",8:"c4a2e9952c298efc080c",9:"313072ac394fc9349d2f",10:"0591dbe3e75f89e4c00e",11:"9ccf6e8ba19ce146e66b",12:"d26d5fe93a45bd5c6eaa",13:"bfc06db76836c228f491",14:"b74db4e8e5c2f13b4c43",15:"d80f5cf2fd51d72b22ea",16:"2ca69ffb53da98b61da4",17:"64a6906a4b77392d43c7",18:"bb8da82a2138ed7b18a8",19:"a2c24b3aee6af674aa30",20:"1272deb68d764581e1ab",21:"5952cc009e9eb6c25dfc",22:"0a5f684edcb8df816a5d",23:"93251b045354cc966a59",24:"61c786230b6da7b9ddc7",25:"e85f7ab22c459c102670"}[e]+".js";var o=setTimeout(d,12e4);function d(){f.onerror=f.onload=null,clearTimeout(o);var c=r[e];0!==c&&(c&&c[1](new Error("Loading chunk "+e+" failed.")),r[e]=void 0)}return f.onerror=f.onload=d,a.appendChild(f),n},t.m=e,t.c=n,t.d=function(e,c,n){t.o(e,c)||Object.defineProperty(e,c,{configurable:!1,enumerable:!0,get:n})},t.n=function(e){var c=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(c,"a",c),c},t.o=function(e,c){return Object.prototype.hasOwnProperty.call(e,c)},t.p="/",t.oe=function(e){throw console.error(e),e}}([]);
//# sourceMappingURL=manifest.d9be689ba09af216a8ce.js.map
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment