refactor: 1

This commit is contained in:
xiaojunnuo
2022-11-07 23:31:20 +08:00
parent f710c00c0d
commit d66bc33761
97 changed files with 1384 additions and 3562 deletions
@@ -1,5 +0,0 @@
import { AbstractAccess } from "./abstract-access";
export interface IAccessService {
getById(id: any): Promise<AbstractAccess>;
}
+5
View File
@@ -1,6 +1,7 @@
import { Registrable } from "../registry";
import { accessRegistry } from "./registry";
import { FormItemProps } from "../d.ts";
import { AbstractAccess } from "./abstract-access";
export type AccessInput = FormItemProps & {
title: string;
@@ -17,3 +18,7 @@ export function IsAccess(define: AccessDefine) {
accessRegistry.install(target);
};
}
export interface IAccessService {
getById(id: any): Promise<AbstractAccess>;
}
@@ -1,28 +0,0 @@
import { IsAccess } from "../api";
import { AbstractAccess } from "../abstract-access";
@IsAccess({
name: "aliyun",
title: "阿里云授权",
desc: "",
input: {
accessKeyId: {
title: "accessKeyId",
component: {
placeholder: "accessKeyId",
},
required: true,
},
accessKeySecret: {
title: "accessKeySecret",
component: {
placeholder: "accessKeySecret",
},
required: true,
},
},
})
export class AliyunAccess extends AbstractAccess {
accessKeyId = "";
accessKeySecret = "";
}
@@ -1 +0,0 @@
export * from "./aliyun-access";
+1 -1
View File
@@ -1,3 +1,3 @@
export * from "./api";
export * from "./impl";
export * from "./abstract-access";
export * from "./registry";
+3 -2
View File
@@ -2,12 +2,12 @@ import { ConcurrencyStrategy, Pipeline, ResultType, Runnable, RunStrategy, Stage
import _ from "lodash";
import { RunHistory } from "./run-history";
import { pluginRegistry, TaskPlugin } from "../plugin";
import { IAccessService } from "../access/access-service";
import { ContextFactory, IContext } from "./context";
import { IStorage } from "./storage";
import { logger } from "../utils/util.log";
import { Logger } from "log4js";
import { request } from "../utils/util.request";
import { IAccessService } from "../access";
export class Executor {
userId: any;
pipeline: Pipeline;
@@ -166,6 +166,7 @@ export class Executor {
pipelineContext: this.pipelineContext,
userContext: this.contextFactory.getContext("user", this.userId),
logger,
http: request,
});
return plugin;
}
+2
View File
@@ -1,2 +1,4 @@
export * from "./executor";
export * from "./run-history";
export * from "./context";
export * from "./storage";
@@ -2,12 +2,15 @@ import { AbstractRegistrable } from "../registry";
import { CreateRecordOptions, IDnsProvider, DnsProviderDefine, RemoveRecordOptions } from "./api";
import { AbstractAccess } from "../access";
import { Logger } from "log4js";
import { AxiosInstance } from "axios";
export abstract class AbstractDnsProvider extends AbstractRegistrable<DnsProviderDefine> implements IDnsProvider {
access!: AbstractAccess;
logger!: Logger;
doInit(options: { access: AbstractAccess; logger: Logger }) {
http!: AxiosInstance;
doInit(options: { access: AbstractAccess; logger: Logger; http: AxiosInstance }) {
this.access = options.access;
this.logger = options.logger;
this.http = options.http;
this.onInit();
}
@@ -1,3 +1,3 @@
import "./providers";
export * from "./api";
export * from "./registry";
export * from "./abstract-dns-provider";
@@ -1,120 +0,0 @@
import { AbstractDnsProvider } from "../abstract-dns-provider";
import Core from "@alicloud/pop-core";
import _ from "lodash";
import { CreateRecordOptions, IDnsProvider, IsDnsProvider, RemoveRecordOptions } from "../api";
@IsDnsProvider({
name: "aliyun",
title: "阿里云",
desc: "阿里云DNS解析提供商",
accessType: "aliyun",
})
export class AliyunDnsProvider extends AbstractDnsProvider implements IDnsProvider {
client: any;
constructor() {
super();
}
async onInit() {
const access: any = this.access;
this.client = new Core({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
endpoint: "https://alidns.aliyuncs.com",
apiVersion: "2015-01-09",
});
}
async getDomainList() {
const params = {
RegionId: "cn-hangzhou",
};
const requestOption = {
method: "POST",
};
const ret = await this.client.request("DescribeDomains", params, requestOption);
return ret.Domains.Domain;
}
async matchDomain(dnsRecord: string) {
const list = await this.getDomainList();
let domain = null;
for (const item of list) {
if (_.endsWith(dnsRecord, item.DomainName)) {
domain = item.DomainName;
break;
}
}
if (!domain) {
throw new Error("can not find Domain ," + dnsRecord);
}
return domain;
}
async getRecords(domain: string, rr: string, value: string) {
const params: any = {
RegionId: "cn-hangzhou",
DomainName: domain,
RRKeyWord: rr,
ValueKeyWord: undefined,
};
if (value) {
params.ValueKeyWord = value;
}
const requestOption = {
method: "POST",
};
const ret = await this.client.request("DescribeDomainRecords", params, requestOption);
return ret.DomainRecords.Record;
}
async createRecord(options: CreateRecordOptions): Promise<any> {
const { fullRecord, value, type } = options;
this.logger.info("添加域名解析:", fullRecord, value);
const domain = await this.matchDomain(fullRecord);
const rr = fullRecord.replace("." + domain, "");
const params = {
RegionId: "cn-hangzhou",
DomainName: domain,
RR: rr,
Type: type,
Value: value,
// Line: 'oversea' // 海外
};
const requestOption = {
method: "POST",
};
try {
const ret = await this.client.request("AddDomainRecord", params, requestOption);
this.logger.info("添加域名解析成功:", value, value, ret.RecordId);
return ret.RecordId;
} catch (e: any) {
if (e.code === "DomainRecordDuplicate") {
return;
}
this.logger.info("添加域名解析出错", e);
throw e;
}
}
async removeRecord(options: RemoveRecordOptions): Promise<any> {
const { fullRecord, value, record } = options;
const params = {
RegionId: "cn-hangzhou",
RecordId: record,
};
const requestOption = {
method: "POST",
};
const ret = await this.client.request("DeleteDomainRecord", params, requestOption);
this.logger.info("删除域名解析成功:", fullRecord, value, ret.RecordId);
return ret.RecordId;
}
}
@@ -1 +0,0 @@
import "./aliyun-dns-provider";
+1
View File
@@ -4,3 +4,4 @@ export * from "./access";
export * from "./registry";
export * from "./dns-provider";
export * from "./plugin";
export * from "./utils";
@@ -1,8 +1,9 @@
import { AbstractRegistrable } from "../registry";
import { Logger } from "log4js";
import { IAccessService } from "../access/access-service";
import { IContext } from "../core/context";
import { PluginDefine, TaskInput, TaskOutput, TaskPlugin } from "./api";
import { IAccessService } from "../access";
import { AxiosInstance } from "axios";
export abstract class AbstractPlugin extends AbstractRegistrable<PluginDefine> implements TaskPlugin {
logger!: Logger;
@@ -12,12 +13,14 @@ export abstract class AbstractPlugin extends AbstractRegistrable<PluginDefine> i
pipelineContext: IContext;
// @ts-ignore
userContext: IContext;
http!: AxiosInstance;
async doInit(options: { accessService: IAccessService; pipelineContext: IContext; userContext: IContext; logger: Logger }) {
async doInit(options: { accessService: IAccessService; pipelineContext: IContext; userContext: IContext; logger: Logger; http: AxiosInstance }) {
this.accessService = options.accessService;
this.pipelineContext = options.pipelineContext;
this.userContext = options.userContext;
this.logger = options.logger;
this.http = options.http;
await this.onInit();
}
+1 -1
View File
@@ -1,3 +1,3 @@
import "./plugins";
export * from "./api";
export * from "./registry";
export * from "./abstract-plugin";
@@ -1,199 +0,0 @@
// @ts-ignore
import * as acme from "@certd/acme-client";
import _ from "lodash";
import { AbstractDnsProvider } from "../../../dns-provider/abstract-dns-provider";
import { IContext } from "../../../core/context";
import { IDnsProvider } from "../../../dns-provider";
import { Challenge } from "@certd/acme-client/types/rfc8555";
import { Logger } from "log4js";
export class AcmeService {
userContext: IContext;
logger: Logger;
constructor(options: { userContext: IContext; logger: Logger }) {
this.userContext = options.userContext;
this.logger = options.logger;
acme.setLogger((text: string) => {
this.logger.info(text);
});
}
async getAccountConfig(email: string) {
return (await this.userContext.get(this.buildAccountKey(email))) || {};
}
buildAccountKey(email: string) {
return "acme.config." + email;
}
async saveAccountConfig(email: string, conf: any) {
await this.userContext.set(this.buildAccountKey(email), conf);
}
async getAcmeClient(email: string, isTest = false): Promise<acme.Client> {
const conf = await this.getAccountConfig(email);
if (conf.key == null) {
conf.key = await this.createNewKey();
await this.saveAccountConfig(email, conf);
}
if (isTest == null) {
isTest = process.env.CERTD_MODE === "test";
}
const client = new acme.Client({
directoryUrl: isTest ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
accountKey: conf.key,
accountUrl: conf.accountUrl,
backoffAttempts: 20,
backoffMin: 5000,
backoffMax: 10000,
});
if (conf.accountUrl == null) {
const accountPayload = {
termsOfServiceAgreed: true,
contact: [`mailto:${email}`],
};
await client.createAccount(accountPayload);
conf.accountUrl = client.getAccountUrl();
await this.saveAccountConfig(email, conf);
}
return client;
}
async createNewKey() {
const key = await acme.forge.createPrivateKey();
return key.toString();
}
async challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider) {
this.logger.info("Triggered challengeCreateFn()");
/* http-01 */
if (challenge.type === "http-01") {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
const fileContents = keyAuthorization;
this.logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`);
/* Replace this */
this.logger.info(`Would write "${fileContents}" to path "${filePath}"`);
// await fs.writeFileAsync(filePath, fileContents);
} else if (challenge.type === "dns-01") {
/* dns-01 */
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
const recordValue = keyAuthorization;
this.logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`);
/* Replace this */
this.logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`);
return await dnsProvider.createRecord({
fullRecord: dnsRecord,
type: "TXT",
value: recordValue,
});
}
}
/**
* Function used to remove an ACME challenge response
*
* @param {object} authz Authorization object
* @param {object} challenge Selected challenge
* @param {string} keyAuthorization Authorization key
* @param recordItem challengeCreateFn create record item
* @param dnsProvider dnsProvider
* @returns {Promise}
*/
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordItem: any, dnsProvider: IDnsProvider) {
this.logger.info("Triggered challengeRemoveFn()");
/* http-01 */
if (challenge.type === "http-01") {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
this.logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`);
/* Replace this */
this.logger.info(`Would remove file on path "${filePath}"`);
// await fs.unlinkAsync(filePath);
} else if (challenge.type === "dns-01") {
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
const recordValue = keyAuthorization;
this.logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`);
/* Replace this */
this.logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
await dnsProvider.removeRecord({
fullRecord: dnsRecord,
type: "TXT",
value: keyAuthorization,
record: recordItem,
});
}
}
async order(options: { email: string; domains: string | string[]; dnsProvider: AbstractDnsProvider; csrInfo: any; isTest?: boolean }) {
const { email, isTest, domains, csrInfo, dnsProvider } = options;
const client: acme.Client = await this.getAcmeClient(email, isTest);
/* Create CSR */
const { commonName, altNames } = this.buildCommonNameByDomains(domains);
const [key, csr] = await acme.forge.createCsr({
commonName,
...csrInfo,
altNames,
});
if (dnsProvider == null) {
throw new Error("dnsProvider 不能为空");
}
/* 自动申请证书 */
const crt = await client.auto({
csr,
email: email,
termsOfServiceAgreed: true,
challengePriority: ["dns-01"],
challengeCreateFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string): Promise<any> => {
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider);
},
challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordItem: any): Promise<any> => {
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider);
},
});
const cert = {
crt: crt.toString(),
key: key.toString(),
csr: csr.toString(),
};
/* Done */
this.logger.debug(`CSR:\n${cert.csr}`);
this.logger.debug(`Certificate:\n${cert.crt}`);
this.logger.info("证书申请成功");
return cert;
}
buildCommonNameByDomains(domains: string | string[]): {
commonName: string;
altNames: string[] | undefined;
} {
if (typeof domains === "string") {
domains = domains.split(",");
}
if (domains.length === 0) {
throw new Error("domain can not be empty");
}
const commonName = domains[0];
let altNames: undefined | string[] = undefined;
if (domains.length > 1) {
altNames = _.slice(domains, 1);
}
return {
commonName,
altNames,
};
}
}
@@ -1,236 +0,0 @@
import { AbstractPlugin } from "../../abstract-plugin";
import forge from "node-forge";
import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../../api";
import dayjs from "dayjs";
import { dnsProviderRegistry } from "../../../dns-provider";
import { AbstractDnsProvider } from "../../../dns-provider/abstract-dns-provider";
import { AcmeService } from "./acme";
import _ from "lodash";
export type CertInfo = {
crt: string;
key: string;
csr: string;
};
@IsTask(() => {
return {
name: "CertApply",
title: "证书申请",
desc: "免费通配符域名证书申请,支持多个域名打到同一个证书上",
input: {
domains: {
title: "域名",
component: {
name: "a-select",
vModel: "value",
mode: "tags",
open: false,
},
required: true,
col: {
span: 24,
},
helper:
"支持通配符域名,例如: *.foo.com 、 *.test.handsfree.work\n" +
"支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" +
"多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com\n" +
"输入一个回车之后,再输入下一个",
},
email: {
title: "邮箱",
component: {
name: "a-input",
vModel: "value",
},
required: true,
helper: "请输入邮箱",
},
dnsProviderType: {
title: "DNS提供商",
component: {
name: "pi-dns-provider-selector",
},
required: true,
helper: "请选择dns解析提供商",
},
dnsProviderAccess: {
title: "DNS解析授权",
component: {
name: "pi-access-selector",
},
required: true,
helper: "请选择dns解析提供商授权",
},
renewDays: {
title: "更新天数",
component: {
name: "a-input-number",
vModel: "value",
},
required: true,
helper: "到期前多少天后更新证书",
},
forceUpdate: {
title: "强制更新",
component: {
name: "a-switch",
vModel: "checked",
},
helper: "是否强制重新申请证书",
},
},
default: {
input: {
renewDays: 20,
forceUpdate: false,
},
},
output: {
cert: {
key: "cert",
type: "CertInfo",
title: "域名证书",
},
},
};
})
export class CertApplyPlugin extends AbstractPlugin implements TaskPlugin {
// @ts-ignore
acme: AcmeService;
protected async onInit() {
this.acme = new AcmeService({ userContext: this.userContext, logger: this.logger });
}
async execute(input: TaskInput): Promise<TaskOutput> {
const oldCert = await this.condition(input);
if (oldCert != null) {
return {
cert: oldCert,
};
}
const cert = await this.doCertApply(input);
return { cert };
}
/**
* 是否更新证书
* @param input
*/
async condition(input: TaskInput) {
if (input.forceUpdate) {
return null;
}
let oldCert;
try {
oldCert = await this.readCurrentCert();
} catch (e) {
this.logger.warn("读取cert失败:", e);
}
if (oldCert == null) {
this.logger.info("还未申请过,准备申请新证书");
return null;
}
const ret = this.isWillExpire(oldCert.expires, input.renewDays);
if (!ret.isWillExpire) {
this.logger.info(`证书还未过期:过期时间${dayjs(oldCert.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${ret.leftDays}`);
return oldCert;
}
this.logger.info("即将过期,开始更新证书");
return null;
}
async doCertApply(input: TaskInput) {
const email = input["email"];
const domains = input["domains"];
const dnsProviderType = input["dnsProviderType"];
const dnsProviderAccessId = input["dnsProviderAccess"];
const csrInfo = _.merge(
{
country: "CN",
state: "GuangDong",
locality: "ShengZhen",
organization: "CertD Org.",
organizationUnit: "IT Department",
emailAddress: email,
},
input["csrInfo"]
);
this.logger.info("开始申请证书,", email, domains);
const dnsProviderClass = dnsProviderRegistry.get(dnsProviderType);
const access = await this.accessService.getById(dnsProviderAccessId);
// @ts-ignore
const dnsProvider: AbstractDnsProvider = new dnsProviderClass();
dnsProvider.doInit({ access, logger: this.logger });
const cert = await this.acme.order({
email,
domains,
dnsProvider,
csrInfo,
isTest: false,
});
await this.writeCert(cert);
const ret = await this.readCurrentCert();
return {
...ret,
isNew: true,
};
}
formatCert(pem: string) {
pem = pem.replace(/\r/g, "");
pem = pem.replace(/\n\n/g, "\n");
pem = pem.replace(/\n$/g, "");
return pem;
}
async writeCert(cert: { crt: string; key: string; csr: string }) {
const newCert = {
crt: this.formatCert(cert.crt),
key: this.formatCert(cert.key),
csr: this.formatCert(cert.csr),
};
await this.pipelineContext.set("cert", newCert);
}
async readCurrentCert() {
const cert: CertInfo = await this.pipelineContext.get("cert");
if (cert == null) {
return undefined;
}
const { detail, expires } = this.getCrtDetail(cert.crt);
return {
...cert,
detail,
expires: expires.getTime(),
};
}
getCrtDetail(crt: string) {
const pki = forge.pki;
const detail = pki.certificateFromPem(crt.toString());
const expires = detail.validity.notAfter;
return { detail, expires };
}
/**
* 检查是否过期,默认提前20天
* @param expires
* @param maxDays
* @returns {boolean}
*/
isWillExpire(expires: number, maxDays = 20) {
if (expires == null) {
throw new Error("过期时间不能为空");
}
// 检查有效期
const leftDays = dayjs(expires).diff(dayjs(), "day");
return {
isWillExpire: leftDays < maxDays,
leftDays,
};
}
}
@@ -1,100 +0,0 @@
import { AbstractPlugin } from "../../abstract-plugin";
import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../../api";
import dayjs from "dayjs";
import Core from "@alicloud/pop-core";
import RPCClient from "@alicloud/pop-core";
import { AliyunAccess } from "../../../access";
import { CertInfo } from "../cert-plugin";
import { RunStrategy } from "../../../d.ts";
@IsTask(() => {
return {
name: "DeployCertToAliyunCDN",
title: "部署证书至阿里云CDN",
desc: "依赖证书申请前置任务,自动部署域名证书至阿里云CDN",
input: {
domainName: {
title: "CDN加速域名",
helper: "你在阿里云上配置的CDN加速域名,比如certd.docmirror.cn",
required: true,
},
certName: {
title: "证书名称",
helper: "上传后将以此名称作为前缀备注",
},
cert: {
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "pi-output-selector",
},
required: true,
},
accessId: {
title: "Access授权",
helper: "阿里云授权AccessKeyId、AccessKeySecret",
component: {
name: "pi-access-selector",
type: "aliyun",
},
required: true,
},
},
output: {},
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
};
})
export class DeployCertToAliyunCDN extends AbstractPlugin implements TaskPlugin {
async execute(input: TaskInput): Promise<TaskOutput> {
console.log("开始部署证书到阿里云cdn");
const access = (await this.accessService.getById(input.accessId)) as AliyunAccess;
const client = this.getClient(access);
const params = await this.buildParams(input);
await this.doRequest(client, params);
console.log("部署完成");
return {};
}
getClient(access: AliyunAccess) {
return new Core({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
endpoint: "https://cdn.aliyuncs.com",
apiVersion: "2018-05-10",
});
}
async buildParams(input: TaskInput) {
const { certName, domainName } = input;
const CertName = (certName ?? "certd") + "-" + dayjs().format("YYYYMMDDHHmmss");
const cert = input.cert as CertInfo;
return {
RegionId: "cn-hangzhou",
DomainName: domainName,
ServerCertificateStatus: "on",
CertName: CertName,
CertType: "upload",
ServerCertificate: cert.crt,
PrivateKey: cert.key,
};
}
async doRequest(client: RPCClient, params: any) {
const requestOption = {
method: "POST",
};
const ret: any = await client.request("SetDomainServerCertificate", params, requestOption);
this.checkRet(ret);
this.logger.info("设置cdn证书成功:", ret.RequestId);
}
checkRet(ret: any) {
if (ret.code != null) {
throw new Error("执行失败:" + ret.Message);
}
}
}
@@ -1,27 +0,0 @@
import { AbstractPlugin } from "../abstract-plugin";
import { IsTask, TaskInput, TaskOutput, TaskPlugin } from "../api";
@IsTask(() => {
return {
name: "EchoPlugin",
title: "测试插件【echo】",
input: {
cert: {
title: "cert",
component: {
name: "pi-output-selector",
},
helper: "输出选择",
},
},
output: {},
};
})
export class EchoPlugin extends AbstractPlugin implements TaskPlugin {
async execute(input: TaskInput): Promise<TaskOutput> {
for (const key in input) {
this.logger.info("input :", key, input[key]);
}
return input;
}
}
@@ -1,3 +0,0 @@
export * from "./cert-plugin/index";
export * from "./echo-plugin";
export * from "./deploy-to-cdn/index";
@@ -0,0 +1,7 @@
import sleep from "./util.sleep";
import { request } from "./util.request";
export * from "./util.log";
export const utils = {
sleep,
http: request,
};
@@ -0,0 +1,58 @@
import axios from "axios";
// @ts-ignore
import qs from "qs";
import { logger } from "./util.log";
/**
* @description 创建请求实例
*/
function createService() {
// 创建一个 axios 实例
const service = axios.create();
// 请求拦截
service.interceptors.request.use(
(config: any) => {
if (config.formData) {
config.data = qs.stringify(config.formData, {
arrayFormat: "indices",
allowDots: true,
}); // 序列化请求参数
delete config.formData;
}
return config;
},
(error: Error) => {
// 发送失败
logger.error(error);
return Promise.reject(error);
}
);
// 响应拦截
service.interceptors.response.use(
(response: any) => {
logger.info("http response:", JSON.stringify(response.data));
return response.data;
},
(error: any) => {
// const status = _.get(error, 'response.status')
// switch (status) {
// case 400: error.message = '请求错误'; break
// case 401: error.message = '未授权,请登录'; break
// case 403: error.message = '拒绝访问'; break
// case 404: error.message = `请求地址出错: ${error.response.config.url}`; break
// case 408: error.message = '请求超时'; break
// case 500: error.message = '服务器内部错误'; break
// case 501: error.message = '服务未实现'; break
// case 502: error.message = '网关错误'; break
// case 503: error.message = '服务不可用'; break
// case 504: error.message = '网关超时'; break
// case 505: error.message = 'HTTP版本不受支持'; break
// default: break
// }
logger.error("请求出错:", error.response.config.url, error);
return Promise.reject(error);
}
);
return service;
}
export const request = createService();
@@ -0,0 +1,7 @@
export default function (timeout: number) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({});
}, timeout);
});
}