mirror of
https://github.com/certd/certd.git
synced 2026-07-07 04:47:34 +08:00
Compare commits
4 Commits
3e304c497e
...
635f069012
| Author | SHA1 | Date | |
|---|---|---|---|
| 635f069012 | |||
| eeb83f9024 | |||
| b48831e60b | |||
| 095791cdc2 |
@@ -211,6 +211,9 @@ export class DemoTest extends AbstractTaskPlugin {
|
||||
//当以下参数变化时,触发获取选项
|
||||
watches: ['certDomains', 'accessId'],
|
||||
required: true,
|
||||
single: false, // 是否单选
|
||||
pager: true, // 是否卡其分页查询
|
||||
search: true, // 是否开启搜索
|
||||
})
|
||||
)
|
||||
siteName!: string | string[];
|
||||
@@ -260,9 +263,12 @@ export class DemoTest extends AbstractTaskPlugin {
|
||||
throw new Error('请选择Access授权');
|
||||
}
|
||||
|
||||
const pager = new Pager(req);
|
||||
|
||||
// @ts-ignore
|
||||
const access = await this.getAccess(this.accessId);
|
||||
|
||||
//
|
||||
// 根据接口情况是否支持翻页查询,和关键字查询, 传递对应的参数,pager.pageNo,pager.pageSize, req.searchKey
|
||||
// const siteRes = await access.GetDomainList(req);
|
||||
//以下是模拟数据
|
||||
const siteRes = [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Certd 开发 Agent 上下文
|
||||
|
||||
进入仓库后先读本文。本文同时包含常驻规则、仓库地图、常用入口和验证命令;不要依赖分散规则文件。
|
||||
思考时也要使用中文。
|
||||
|
||||
## 项目定位
|
||||
|
||||
@@ -60,7 +61,6 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
||||
|
||||
- 后端聚焦单测:`corepack pnpm --dir packages\ui\certd-server test:unit`。
|
||||
- 后端完整测试:`corepack pnpm --dir packages\ui\certd-server test`。
|
||||
- 前端构建:`corepack pnpm --dir packages\ui\certd-client build`。
|
||||
- 前端改动文件格式化:`packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`。
|
||||
- 前端改动文件 ESLint 修复:`packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`。
|
||||
- 后端改动文件 lint fix:`corepack pnpm --dir packages\ui\certd-server run lint`。
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"esmock": "^2.7.5",
|
||||
"jsdoc-to-markdown": "^8.0.1",
|
||||
"mocha": "^10.6.0",
|
||||
"nock": "^13.5.4",
|
||||
"prettier": "3.3.3",
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"@certd/pipeline": "^1.41.4",
|
||||
"dayjs": "^1.11.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"psl": "^1.15.0"
|
||||
"psl": "^1.15.0",
|
||||
"punycode.js": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^5.0.5",
|
||||
|
||||
@@ -280,9 +280,14 @@ export class CertReader {
|
||||
return `${prefix}_${domain}_${timeStr}.${suffix}`;
|
||||
}
|
||||
|
||||
buildCertName(prefix: string = "") {
|
||||
buildCertName(prefix: string = "", useHash: boolean = false) {
|
||||
let domain = this.getMainDomain();
|
||||
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
|
||||
if (useHash) {
|
||||
const domains = JSON.stringify(this.getAllDomains());
|
||||
const hash = cryptoLib.createHash("md5").update(domains).digest("hex").slice(0, 16);
|
||||
return `${prefix}_${domain}_${hash}`;
|
||||
}
|
||||
return `${prefix}_${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`;
|
||||
}
|
||||
|
||||
@@ -293,7 +298,7 @@ export class CertReader {
|
||||
return name + "_" + dayjs().format("YYYYMMDDHHmmssSSS");
|
||||
}
|
||||
|
||||
static buildCertName(cert: CertInfo) {
|
||||
return new CertReader(cert).buildCertName();
|
||||
static buildCertName(cert: CertInfo, useHash: boolean = false) {
|
||||
return new CertReader(cert).buildCertName("", useHash);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="kv-input">
|
||||
<div v-for="(item, index) of items" :key="index" class="kv-row">
|
||||
<a-input :value="item.key" placeholder="名称" class="kv-key" @input="onKeyChange(index, $event)" @blur="emitValue" />
|
||||
<span class="kv-sep">:</span>
|
||||
<a-input :value="item.value" placeholder="版本" class="kv-value" @input="onValueChange(index, $event)" @blur="emitValue" />
|
||||
<a-button type="link" danger @click="removeItem(index)">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
<a-button type="dashed" block @click="addItem">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons-vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Record<string, string>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: Record<string, string>): void;
|
||||
}>();
|
||||
|
||||
interface KvItem {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const items = ref<KvItem[]>([]);
|
||||
|
||||
function initItems(val: Record<string, string>) {
|
||||
if (val && typeof val === "object" && !Array.isArray(val)) {
|
||||
items.value = Object.entries(val).map(([k, v]) => ({
|
||||
key: k,
|
||||
value: v ?? "",
|
||||
}));
|
||||
} else {
|
||||
items.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
val => {
|
||||
initItems(val);
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
function rebuildRecord(): Record<string, string> {
|
||||
const record: Record<string, string> = {};
|
||||
for (const item of items.value) {
|
||||
const k = item.key?.trim();
|
||||
if (k) {
|
||||
record[k] = item.value?.trim() ?? "";
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
function emitValue() {
|
||||
emit("update:modelValue", rebuildRecord());
|
||||
}
|
||||
|
||||
function onKeyChange(index: number, event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
items.value[index].key = target.value;
|
||||
}
|
||||
|
||||
function onValueChange(index: number, event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
items.value[index].value = target.value;
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
items.value.push({ key: "", value: "" });
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
items.value.splice(index, 1);
|
||||
emitValue();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.kv-input {
|
||||
.kv-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
gap: 4px;
|
||||
.kv-key {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.kv-sep {
|
||||
flex-shrink: 0;
|
||||
padding: 0 2px;
|
||||
color: #999;
|
||||
}
|
||||
.kv-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -26,7 +26,9 @@ export default {
|
||||
store: "Store",
|
||||
version: "Version",
|
||||
pluginDependencies: "Plugin Dependencies",
|
||||
pluginDependenciesHelper: "Dependencies to install first in format: [author/]pluginName[:version]",
|
||||
pluginDependenciesHelper: "Format: [author/]pluginName[:version]. Required plugins must be installed first",
|
||||
thirdPartyDependencies: "Third-party Dependencies",
|
||||
thirdPartyDependenciesHelper: "Format: npmPackageName: version. Auto-installed at runtime",
|
||||
editableRunStrategy: "Editable Run Strategy",
|
||||
editable: "Editable",
|
||||
notEditable: "Not Editable",
|
||||
|
||||
@@ -27,6 +27,8 @@ export default {
|
||||
version: "版本",
|
||||
pluginDependencies: "插件依赖",
|
||||
pluginDependenciesHelper: "格式: [作者/]插件名[:版本],需先安装依赖插件",
|
||||
thirdPartyDependencies: "第三方依赖",
|
||||
thirdPartyDependenciesHelper: "格式: npm包名: 版本号,运行时自动安装",
|
||||
editableRunStrategy: "可编辑运行策略",
|
||||
editable: "可编辑",
|
||||
notEditable: "不可编辑",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Modal, message } from "ant-design-vue";
|
||||
//@ts-ignore
|
||||
import yaml from "js-yaml";
|
||||
import { usePluginImport } from "./use-import";
|
||||
import KvInput from "/@/components/plugins/common/kv-input.vue";
|
||||
import { usePluginConfig } from "./use-config";
|
||||
import { useSettingStore } from "/src/store/settings/index";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
@@ -37,6 +38,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
return res;
|
||||
};
|
||||
|
||||
// const infoRequest = async ({ row }: AddReq) => {
|
||||
// return await api.GetObj(row.id);
|
||||
// };
|
||||
|
||||
const selectedRowKeys: Ref<any[]> = ref([]);
|
||||
context.selectedRowKeys = selectedRowKeys;
|
||||
|
||||
@@ -66,6 +71,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest,
|
||||
// infoRequest,
|
||||
},
|
||||
actionbar: {
|
||||
buttons: {
|
||||
@@ -187,6 +193,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
id: opts.res.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
crudExpose.doRefresh();
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -363,10 +371,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
type: "text",
|
||||
form: {
|
||||
component: {
|
||||
name: "a-select",
|
||||
mode: "tags",
|
||||
open: false,
|
||||
allowClear: true,
|
||||
name: KvInput,
|
||||
vModel: "modelValue",
|
||||
},
|
||||
helper: t("certd.pluginDependenciesHelper"),
|
||||
},
|
||||
@@ -374,6 +380,20 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
"extra.dependPackages": {
|
||||
title: t("certd.thirdPartyDependencies"),
|
||||
type: "text",
|
||||
form: {
|
||||
component: {
|
||||
name: KvInput,
|
||||
vModel: "modelValue",
|
||||
},
|
||||
helper: t("certd.thirdPartyDependenciesHelper"),
|
||||
},
|
||||
column: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
"extra.showRunStrategy": {
|
||||
title: t("certd.editableRunStrategy"),
|
||||
type: "dict-switch",
|
||||
@@ -419,12 +439,12 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
show: false,
|
||||
},
|
||||
valueBuilder({ row }) {
|
||||
if (row.extra) {
|
||||
if (typeof row.extra === "string") {
|
||||
row.extra = yaml.load(row.extra);
|
||||
}
|
||||
},
|
||||
valueResolve({ row }) {
|
||||
if (row.extra) {
|
||||
if (row.extra && typeof row.extra === "object") {
|
||||
row.extra = yaml.dump(row.extra);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -99,6 +99,19 @@ input:
|
||||
value: play
|
||||
required: true
|
||||
order: 0
|
||||
sourceStationType:
|
||||
title: 源站类型
|
||||
helper: 选择源站类型
|
||||
component:
|
||||
name: a-select
|
||||
vModel: value
|
||||
options:
|
||||
- value: 1
|
||||
label: 点播源站
|
||||
- value: 2
|
||||
label: 自定义源站
|
||||
required: false
|
||||
order: 0
|
||||
domainList:
|
||||
title: 域名
|
||||
component:
|
||||
@@ -117,6 +130,7 @@ input:
|
||||
- accessId
|
||||
- spaceName
|
||||
- domainType
|
||||
- sourceStationType
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
|
||||
@@ -115,9 +115,32 @@
|
||||
"uuid": "^10.0.0",
|
||||
"wechatpay-node-v3": "^2.2.1",
|
||||
"whoiser": "2.0.0-beta.10",
|
||||
"xml2js": "^0.6.2"
|
||||
"xml2js": "^0.6.2",
|
||||
"mwtsc": "^1.15.1"
|
||||
},
|
||||
"lazyDependencies": {
|
||||
"devDependencies": {
|
||||
"mwts": "^1.3.0",
|
||||
"@midwayjs/mock": "3.20.11",
|
||||
"@types/ali-oss": "^6.16.11",
|
||||
"@types/cache-manager": "^4.0.6",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/koa": "2.15.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "^18",
|
||||
"@types/nodemailer": "^6.4.8",
|
||||
"c8": "^10.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.6.0",
|
||||
"prettier": "3.3.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2",
|
||||
"why-is-node-running": "^3.2.2"
|
||||
},
|
||||
"lazyDependencies": {
|
||||
"@alicloud/fc20230330": "^4.1.7",
|
||||
"@alicloud/tea-typescript": "^1.8.0",
|
||||
"@alicloud/openapi-client": "^0.4.12",
|
||||
@@ -147,30 +170,7 @@
|
||||
"@google-cloud/publicca": "^1.3.0",
|
||||
"basic-ftp": "^5.0.5",
|
||||
"esdk-obs-nodejs": "^3.25.6",
|
||||
"qiniu": "^7.12.0",
|
||||
"mwtsc": "^1.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mwts": "^1.3.0",
|
||||
"@midwayjs/mock": "3.20.11",
|
||||
"@types/ali-oss": "^6.16.11",
|
||||
"@types/cache-manager": "^4.0.6",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/koa": "2.15.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "^18",
|
||||
"@types/nodemailer": "^6.4.8",
|
||||
"c8": "^10.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.6.0",
|
||||
"prettier": "3.3.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2",
|
||||
"why-is-node-running": "^3.2.2"
|
||||
"qiniu": "^7.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
||||
@@ -30,8 +30,9 @@
|
||||
// export * from './plugin-wangsu/index.js'
|
||||
// export * from './plugin-admin/index.js'
|
||||
// export * from './plugin-ksyun/index.js'
|
||||
// export * from './plugin-apisix/index.js'
|
||||
// export * from './plugin-dokploy/index.js'
|
||||
// export * from './plugin-apisix/index.js';
|
||||
// export * from './plugin-asiaisp/index.js';
|
||||
// export * from './plugin-dokploy/index.js';
|
||||
// export * from './plugin-godaddy/index.js'
|
||||
// export * from './plugin-captcha/index.js'
|
||||
// export * from './plugin-xinnet/index.js'
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import { AsiaIspClient } from "./client.js";
|
||||
|
||||
@IsAccess({
|
||||
name: "asiaisp",
|
||||
title: "橙域网络(asia-isp)授权",
|
||||
desc: "橙域网络CDN API授权,用于部署证书到橙域CDN",
|
||||
icon: "clarity:plugin-line",
|
||||
})
|
||||
export class AsiaIspAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "AccessKeyId",
|
||||
component: {
|
||||
placeholder: "请输入 AccessKeyId",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessKeyId = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "AccessKeySecret",
|
||||
component: {
|
||||
placeholder: "请输入 AccessKeySecret",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
accessKeySecret = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "测试连接",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "TestRequest",
|
||||
},
|
||||
helper: "点击测试接口是否正常",
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
async onTestRequest() {
|
||||
const client = await this.getClient();
|
||||
const list = await client.getCertList();
|
||||
return `连接成功,共 ${list.length} 个证书`;
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
return new AsiaIspClient({
|
||||
accessKeyId: this.accessKeyId,
|
||||
accessKeySecret: this.accessKeySecret,
|
||||
http: this.ctx.http,
|
||||
logger: this.ctx.logger,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new AsiaIspAccess();
|
||||
@@ -0,0 +1,259 @@
|
||||
import { HttpClient, ILogger } from "@certd/basic";
|
||||
import { CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
const BASE_URL = "https://api.asia-isp.com";
|
||||
const URI = "/openapi/v3/stat";
|
||||
|
||||
export interface AsiaIspConfig {
|
||||
accessKeyId: string;
|
||||
accessKeySecret: string;
|
||||
http: HttpClient;
|
||||
logger: ILogger;
|
||||
}
|
||||
|
||||
export interface AsiaIspDomain {
|
||||
domain: string;
|
||||
serviceType: string;
|
||||
scope: string;
|
||||
protocol: string;
|
||||
certId: number;
|
||||
cname: string;
|
||||
originHost: string;
|
||||
originAddr: string;
|
||||
originProtocol: string;
|
||||
originType: string;
|
||||
domainStatus: number;
|
||||
operatingStatus: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 橙域网络(asia-isp) CDN API 客户端
|
||||
* 签名算法参照 Python 参考实现:
|
||||
* message = accessKeySecret={sk}&[body={json}]&method={method}&nonce={nonce}&queryString={qs}×tamp={ts}&uri={uri}
|
||||
* signature = URL-safe-Base64(HMAC-SHA1(sk, message))
|
||||
*/
|
||||
export class AsiaIspClient {
|
||||
private config: AsiaIspConfig;
|
||||
private http: HttpClient;
|
||||
private logger: ILogger;
|
||||
|
||||
constructor(config: AsiaIspConfig) {
|
||||
this.config = config;
|
||||
this.http = config.http;
|
||||
this.logger = config.logger;
|
||||
|
||||
if (!this.config.accessKeyId) {
|
||||
throw new Error("accessKeyId 不能为空");
|
||||
}
|
||||
if (!this.config.accessKeySecret) {
|
||||
throw new Error("accessKeySecret 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 HMAC-SHA1 签名,结果做 URL-safe Base64(替换 + → -,/ → _)
|
||||
*/
|
||||
private buildSignature(opts: {
|
||||
body?: any;
|
||||
method: string;
|
||||
nonce: string;
|
||||
queryString: string;
|
||||
timestamp: string;
|
||||
}): string {
|
||||
const { body, method, nonce, queryString, timestamp } = opts;
|
||||
const sk = this.config.accessKeySecret;
|
||||
|
||||
const pieces: string[] = [`accessKeySecret=${sk}`];
|
||||
|
||||
// body 仅在 POST/PUT 时存在,对应 Python 的 body is not None
|
||||
if (body !== undefined && body !== null) {
|
||||
pieces.push(`body=${JSON.stringify(body)}`);
|
||||
}
|
||||
|
||||
pieces.push(
|
||||
`method=${method}`,
|
||||
`nonce=${nonce}`,
|
||||
`queryString=${queryString}`,
|
||||
`timestamp=${timestamp}`,
|
||||
`uri=${URI}`
|
||||
);
|
||||
|
||||
const message = pieces.join("&");
|
||||
const hmac = crypto.createHmac("sha1", sk).update(message).digest("base64");
|
||||
// URL-safe Base64
|
||||
return hmac.replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 API 请求(完全对齐 Python 实现)
|
||||
*/
|
||||
async doRequest(req: {
|
||||
method: string;
|
||||
action: string;
|
||||
data?: any;
|
||||
}): Promise<any> {
|
||||
const { method, action, data } = req;
|
||||
const nonce = String(Math.floor(Math.random() * 90000000) + 10000000);
|
||||
const timestamp = Date.now().toString();
|
||||
const queryString = `action=${action}`;
|
||||
|
||||
const signature = this.buildSignature({
|
||||
body: method === "POST" || method === "PUT" ? data : undefined,
|
||||
method,
|
||||
nonce,
|
||||
queryString,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
accessKeyId: this.config.accessKeyId,
|
||||
nonce,
|
||||
timestamp,
|
||||
signature,
|
||||
};
|
||||
|
||||
const url = `${BASE_URL}${URI}?${queryString}`;
|
||||
|
||||
try {
|
||||
const response = await this.http.request({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
data: method === "POST" || method === "PUT" ? data : undefined,
|
||||
skipSslVerify: true,
|
||||
logParams: false,
|
||||
logRes: false,
|
||||
logData: false,
|
||||
});
|
||||
|
||||
if (response.code !== "0") {
|
||||
this.logger.error(`接口请求失败: code=${response.code}, msg=${response.msg}`);
|
||||
throw new Error(response.msg || "接口请求失败");
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
if (error.message && !error.message.includes("接口请求失败")) {
|
||||
this.logger.error(`接口请求异常: ${error.message}`);
|
||||
throw new Error(`接口请求异常: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域名列表
|
||||
* GET /openapi/v3/stat?action=domainQueryList
|
||||
*/
|
||||
async getDomainList(): Promise<AsiaIspDomain[]> {
|
||||
const res = await this.doRequest({
|
||||
method: "GET",
|
||||
action: "domainQueryList",
|
||||
});
|
||||
const list = res.data || [];
|
||||
this.logger.info(`获取域名列表成功,共 ${list.length} 个域名`);
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取证书列表
|
||||
* GET /openapi/v3/stat?action=certificateQueryList
|
||||
*/
|
||||
async getCertList(): Promise<Array<{ certId: string; name: string }>> {
|
||||
const res = await this.doRequest({
|
||||
method: "GET",
|
||||
action: "certificateQueryList",
|
||||
});
|
||||
return res.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询证书详情
|
||||
* GET /openapi/v3/stat?action=certificateQuery&certId=xxx
|
||||
*/
|
||||
async getCertDetail(certId: number): Promise<any> {
|
||||
const res = await this.doRequest({
|
||||
method: "GET",
|
||||
action: `certificateQuery&certId=${certId}`,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传证书
|
||||
* POST /openapi/v3/stat?action=certificateUpload
|
||||
* 返回证书ID
|
||||
*/
|
||||
async uploadCert(req: { cert: CertInfo; name?: string }): Promise<number> {
|
||||
const certReader = new CertReader(req.cert);
|
||||
const certName = req.name || certReader.buildCertName("", true);
|
||||
|
||||
try {
|
||||
const res = await this.doRequest({
|
||||
method: "POST",
|
||||
action: "certificateUpload",
|
||||
data: {
|
||||
name: certName,
|
||||
publicKey: req.cert.crt,
|
||||
privateKey: req.cert.key,
|
||||
},
|
||||
});
|
||||
const certId = res.data;
|
||||
this.logger.info(`上传证书成功,证书ID: ${certId}`);
|
||||
return certId;
|
||||
} catch (e: any) {
|
||||
const msg = e.message || "";
|
||||
const isExists = msg.includes("Certificate already exists") || e.code ==='80003' ||
|
||||
msg.includes("Certificate note name already exists") || e.code ==='80010'
|
||||
//返回数据: {"code":"80010","msg":"Certificate note name already exists","data":null}
|
||||
if (!isExists) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.logger.info(`证书已存在,按名称查找: ${certName}`);
|
||||
const list = await this.getCertList();
|
||||
const found = list.find((item: any) => item.name === certName);
|
||||
if (!found) {
|
||||
throw new Error(`证书已存在但无法查询到: ${certName}`);
|
||||
}
|
||||
const certId = Number(found.certId);
|
||||
this.logger.info(`复用已有证书,证书ID: ${certId}`);
|
||||
return certId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除证书
|
||||
* DELETE /openapi/v3/stat?action=certificateDelete&certId=xxx
|
||||
*/
|
||||
async deleteCert(certId: number): Promise<void> {
|
||||
await this.doRequest({
|
||||
method: "DELETE",
|
||||
action: `certificateDelete&certId=${certId}`,
|
||||
});
|
||||
this.logger.info(`删除证书成功,证书ID: ${certId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 部署证书到 CDN 域名(修改域名配置,绑定证书)
|
||||
* PUT /openapi/v3/stat?action=domainModify
|
||||
*/
|
||||
async deployCertToDomain(req: {
|
||||
domain: string;
|
||||
certId: number;
|
||||
protocol: string;
|
||||
}): Promise<void> {
|
||||
await this.doRequest({
|
||||
method: "PUT",
|
||||
action: "domainModify",
|
||||
data: {
|
||||
domain: req.domain,
|
||||
certId: `${req.certId}`,
|
||||
protocol: req.protocol || "https",
|
||||
},
|
||||
});
|
||||
this.logger.info(`部署证书到域名成功: ${req.domain}, certId=${req.certId}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./access.js";
|
||||
export * from "./plugin-deploy-to-cdn.js";
|
||||
@@ -0,0 +1,122 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { AsiaIspAccess } from "./access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "AsiaIspDeployToCDN",
|
||||
title: "橙域网络-部署证书到CDN",
|
||||
desc: "部署证书到橙域网络(asia-isp) CDN加速域名",
|
||||
icon: "clarity:plugin-line",
|
||||
group: pluginGroups.cdn.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class AsiaIspDeployToCDN extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
|
||||
certDomains!: string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "橙域网络授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "asiaisp",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "加速域名",
|
||||
helper: "选择要部署证书的橙域CDN加速域名",
|
||||
action: AsiaIspDeployToCDN.prototype.onGetDomainList.name,
|
||||
pager: true,
|
||||
search: true,
|
||||
pageSize: 10,
|
||||
single: false,
|
||||
watches: ["certDomains", "accessId"],
|
||||
required: true,
|
||||
})
|
||||
)
|
||||
domainList!: string[];
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<AsiaIspAccess>(this.accessId);
|
||||
const client = await access.getClient();
|
||||
|
||||
this.logger.info("开始部署证书到橙域网络CDN");
|
||||
|
||||
// 1. 上传证书到橙域平台
|
||||
this.logger.info("上传证书到橙域网络...");
|
||||
const certId = await client.uploadCert({
|
||||
cert: this.cert,
|
||||
});
|
||||
|
||||
// 2. 为每个选中的域名绑定证书
|
||||
for (const domain of this.domainList) {
|
||||
this.logger.info(`部署证书到域名: ${domain}`);
|
||||
await client.deployCertToDomain({
|
||||
domain,
|
||||
certId,
|
||||
protocol: "https",
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.info(`证书部署完成,共处理 ${this.domainList.length} 个域名`);
|
||||
}
|
||||
|
||||
async onGetDomainList(data: PageSearch = {}) {
|
||||
const access = await this.getAccess<AsiaIspAccess>(this.accessId);
|
||||
const client = await access.getClient();
|
||||
const list = await client.getDomainList();
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("没有找到加速域名");
|
||||
}
|
||||
|
||||
// 客户端过滤:按搜索关键词匹配域名
|
||||
let filtered = list;
|
||||
const keyword = data.searchKey?.trim().toLowerCase();
|
||||
if (keyword) {
|
||||
filtered = list.filter((item: any) => item.domain?.toLowerCase().includes(keyword));
|
||||
}
|
||||
|
||||
// 客户端分页
|
||||
const pageNo = data.pageNo || 1;
|
||||
const pageSize = data.pageSize || 10;
|
||||
const start = (pageNo - 1) * pageSize;
|
||||
const paged = filtered.slice(start, start + pageSize);
|
||||
|
||||
const options = paged.map((item: any) => {
|
||||
return {
|
||||
label: `${item.domain}${item.protocol === "https" ? " (HTTPS)" : ""}`,
|
||||
value: item.domain,
|
||||
domain: item.domain,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains),
|
||||
total: filtered.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
new AsiaIspDeployToCDN();
|
||||
+28
-6
@@ -46,7 +46,7 @@ export class VolcengineDeployToVOD extends AbstractTaskPlugin {
|
||||
title: "区域",
|
||||
helper: "选择火山引擎区域",
|
||||
component: {
|
||||
name: "select",
|
||||
name: "a-select",
|
||||
options: [
|
||||
{ value: "cn-north-1", label: "华北1(北京)" },
|
||||
{ value: "ap-southeast-1", label: "东南亚1(新加坡)" },
|
||||
@@ -85,12 +85,29 @@ export class VolcengineDeployToVOD extends AbstractTaskPlugin {
|
||||
})
|
||||
domainType!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "源站类型",
|
||||
helper: "选择源站类型",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ value: 1, label: "点播源站" },
|
||||
{ value: 2, label: "自定义源站" },
|
||||
],
|
||||
value: 1,
|
||||
helper: "注意:封面加速域名不支持自定义源站",
|
||||
},
|
||||
required: false,
|
||||
})
|
||||
sourceStationType!: number | undefined;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "域名",
|
||||
helper: "选择要部署证书的域名\n需要先在域名管理页面进行证书中心访问授权(即点击去配置SSL证书)",
|
||||
action: VolcengineDeployToVOD.prototype.onGetDomainList.name,
|
||||
watches: ["certDomains", "accessId", "spaceName", "domainType"],
|
||||
watches: ["certDomains", "accessId", "spaceName", "domainType", "sourceStationType"],
|
||||
required: true,
|
||||
})
|
||||
)
|
||||
@@ -206,12 +223,17 @@ export class VolcengineDeployToVOD extends AbstractTaskPlugin {
|
||||
}
|
||||
const service = await this.getVodService({ version: "2023-01-01", region: this.regionId });
|
||||
|
||||
const query: Record<string, any> = {
|
||||
SpaceName: this.spaceName,
|
||||
DomainType: this.domainType,
|
||||
};
|
||||
if (this.sourceStationType !== undefined) {
|
||||
query.SourceStationType = this.sourceStationType;
|
||||
}
|
||||
|
||||
const res = await service.request({
|
||||
action: "ListDomain",
|
||||
query: {
|
||||
SpaceName: this.spaceName,
|
||||
DomainType: this.domainType,
|
||||
},
|
||||
query,
|
||||
});
|
||||
|
||||
const instances = res.Result?.PlayInstanceInfo?.ByteInstances;
|
||||
|
||||
Reference in New Issue
Block a user