Compare commits

...

8 Commits

Author SHA1 Message Date
xiaojunnuo 83a6aed625 chore: 优化腾讯云删除证书提示 2026-07-30 19:08:57 +08:00
xiaojunnuo f1b67049d1 perf: 证书成功通知增加有效期显示 2026-07-28 19:22:12 +08:00
xiaojunnuo 8cbca5761e chore(pipeline): adjust npm install log truncation logic
change log output to take last 2000 chars instead of first 2000 for both stdout/stderr, and add consistent empty log placeholder text
2026-07-27 00:35:00 +08:00
xiaojunnuo 967846bef5 perf(tencent-clb): 为腾讯云CLB部署插件支持远程选择输入
- 重构插件输入配置,使用createRemoteSelectInputDefine统一封装远程选择组件
- 新增onGetCLBList、onGetListenerList、onGetDomainList方法实现数据拉取和格式转换
- 补全完整的单元测试用例,覆盖输入配置校验和数据映射逻辑
2026-07-26 19:01:18 +08:00
xiaojunnuo 18b2d3ac20 perf: 优化第三方依赖安装失败日志输出 2026-07-23 18:35:04 +08:00
xiaojunnuo 7d22fe3d7d perf: 优化SqliteError: disk I/O error报错修复提示 2026-07-23 10:49:00 +08:00
xiaojunnuo ee67b6c042 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-21 14:13:16 +08:00
xiaojunnuo 1cfa76683b perf: 优化宝塔报错提示 2026-07-21 14:13:07 +08:00
11 changed files with 281 additions and 55 deletions
@@ -568,11 +568,18 @@ export class RuntimeDepsService {
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
if (result.code === 0) {
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
log.info(`${result.stdout?.slice(-2000) || "无npm安装日志输出"}`);
log.info("第三方依赖安装完成");
return { registryUrl: tryUrl, packageJsonPath };
}
lastError = result.stderr || result.stdout || "unknown error";
log.warn?.(`镜像 ${tryUrl || "默认"} 安装失败${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
const errOutput = (result.stderr || "").trim();
const outOutput = (result.stdout || "").trim();
lastError = errOutput || outOutput || "unknown error";
log.info(`镜像 ${tryUrl || "默认"} 安装失败,退出码: ${result.code}${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
log.info(` pnpm stderr: ${(errOutput || "无npm安装日志输出").slice(-2000)}`);
if (outOutput) {
log.info(` pnpm stdout: ${outOutput.slice(-2000)}`);
}
}
this.writeInstallState(statePath, {
...currentState,
+8 -5
View File
@@ -93,19 +93,21 @@ export class Flyway {
await queryRunner.commitTransaction();
} catch (err) {
this.logger.error(err);
this.errorTip(err);
await this.storeSqlExecLog(file.script, filepath, false, queryRunner);
await queryRunner.rollbackTransaction();
if (err.code === "SQLITE_IOERR_WRITE") {
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)", file.script);
}
throw err;
}
}
this.logger.info("[ midfly ] end-------------");
}
private errorTip(err: any) {
if (err.code === "SQLITE_IOERR_WRITE") {
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)");
}
}
private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) {
const hash = await this.getFileHash(filepath);
//先删除
@@ -265,6 +267,7 @@ export class Flyway {
await queryRunner.query(sql);
} catch (err: any) {
this.logger.error("exec sql error ", err.message, err);
this.errorTip(err);
throw err;
}
}
@@ -64,6 +64,7 @@ export default {
projectUserManager: "Project User Management",
myProjectManager: "My Projects",
myProjectDetail: "Project Detail",
projectDetail: "Project Detail",
projectJoin: "Join Project",
currentProject: "Current Project",
projectMemberManager: "Project Member",
@@ -64,6 +64,7 @@ export default {
enterpriseSetting: "企业设置",
myProjectManager: "我的项目",
myProjectDetail: "项目详情",
projectDetail: "项目详情",
projectJoin: "加入项目",
currentProject: "当前项目",
projectMemberManager: "项目成员管理",
@@ -17,7 +17,7 @@
</template>
<script lang="ts">
import { defineComponent, reactive, ref, watch, inject, onMounted } from "vue";
import { defineComponent, reactive, ref, watch, inject, onMounted, Ref } from "vue";
import CertAccessModal from "./access/index.vue";
import { createAccessApi } from "../api";
import { message } from "ant-design-vue";
@@ -64,9 +64,9 @@ export default defineComponent({
setup(props, ctx) {
const api = createAccessApi(props.from);
const target = ref({});
const target:Ref<any> = ref({});
const selectedId = ref();
async function refreshTarget(value) {
async function refreshTarget(value:any) {
selectedId.value = value;
if (value > 0) {
target.value = await api.GetSimpleInfo(value);
@@ -83,7 +83,7 @@ export default defineComponent({
const userStore = useUserStore();
const projectStore = useProjectStore();
async function emitValue(value) {
async function emitValue(value:any) {
const userId = userStore.userInfo.id;
const isEnterprice = projectStore.isEnterprise;
if (pipeline?.value) {
@@ -132,7 +132,7 @@ export default defineComponent({
const providerDefine = ref({});
async function refreshProviderDefine(type) {
async function refreshProviderDefine(type:any) {
providerDefine.value = await api.GetProviderDefine(type);
}
watch(
@@ -76,7 +76,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
this.clearLastStatus();
if (this.successNotify) {
await this.sendSuccessNotify();
await this.sendSuccessNotify(cert);
}
} else {
throw new Error("申请证书失败");
@@ -165,12 +165,14 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
nextUpdateDays: leftDays - maxDays,
};
}
async sendSuccessNotify() {
async sendSuccessNotify(certReader: CertReader) {
this.logger.info("发送证书申请成功通知");
const url = await this.ctx.urlService.getPipelineDetailUrl(this.pipeline.id, this.ctx.runtime.id);
const body: NotificationBody = {
title: `证书申请成功【${this.pipeline.title}`,
content: `域名:${this.domains.join(",")}`,
content: `域名:${this.domains.join(",")}\n
证书有效期:${dayjs(certReader.expires).format("YYYY-MM-DD HH:mm:ss")}\n
`,
url: url,
notificationType: "certApplySuccess",
};
@@ -1,15 +1,17 @@
import crypto from "node:crypto";
import { BaotaAccess } from "../access.js";
import { HttpClient, HttpRequestConfig } from "@certd/basic";
import { HttpClient, HttpRequestConfig, ILogger } from "@certd/basic";
import * as querystring from "node:querystring";
export class BaotaClient {
access: BaotaAccess;
http: HttpClient;
logger: ILogger
constructor(access: BaotaAccess, http: HttpClient) {
this.access = access;
this.http = http;
this.logger = access.ctx.logger;
}
//将以上 java代码 翻译成nodejs 代码
@@ -88,7 +88,7 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
})
siteName!: string | string[];
async onInstance() {}
async onInstance() { }
async execute(): Promise<void> {
const { cert, accessId } = this;
const access = await this.getAccess(accessId);
@@ -104,26 +104,37 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
}
const lockKey = `baota-lock-${accessId}`;
if (this.isDockerSite) {
this.logger.info(`当前已勾选docker站点(如果部署失败,请确认站点:${siteNames} 是否全部为docker站点)`);
}
for (const site of siteNames) {
// 加锁,防止并发部署证书, 宝塔并发部署会导致nginx的conf错乱
await this.ctx.utils.locker.execute(lockKey, async () => {
this.logger.info(`为站点:${site}设置证书,目前支持宝塔网站站点、docker站点`);
if (this.isDockerSite) {
const res = await client.doRequest("/mod/docker/com/set_ssl", "", {
site_name: site,
key: cert.key,
csr: cert.crt,
});
this.logger.info(res?.msg);
} else {
const res = await client.doRequest("/site", "SetSSL", {
type: 0,
siteName: site,
key: cert.key,
csr: cert.crt,
});
this.logger.info(res?.msg);
try {
if (this.isDockerSite) {
this.logger.info(`为Docker站点:${site} 设置证书`);
const res = await client.doRequest("/mod/docker/com/set_ssl", "", {
site_name: site,
key: cert.key,
csr: cert.crt,
});
this.logger.info(res?.msg);
} else {
this.logger.info(`为非Docker站点:${site} 设置证书`);
const res = await client.doRequest("/site", "SetSSL", {
type: 0,
siteName: site,
key: cert.key,
csr: cert.crt,
});
this.logger.info(res?.msg);
}
} catch (e: any) {
if (e?.message?.includes("没有服务器配置文件")) {
this.logger.error(e.message);
this.logger.warn(`首先请确认站点 ${site} 是否存在,如果存在,请到宝塔上手动保存一下该站点证书试试`);
}
throw e
}
});
}
@@ -9,7 +9,7 @@ import { TencentAccess, TencentSslClient } from "../../../plugin-lib/tencent/ind
title: "腾讯云-删除即将过期证书",
icon: "svg:icon-tencentcloud",
group: pluginGroups.tencent.key,
desc: "仅删除未使用的证书",
desc: "仅删除即将过期且未使用的证书",
dependPlugins: {
"access:tencent": "*",
},
@@ -0,0 +1,111 @@
/// <reference types="mocha" />
import assert from "node:assert/strict";
import { DeployCertToTencentCLB } from "./index.js";
describe("DeployCertToTencentCLB", () => {
it("uses remote single-select inputs for CLB and HTTPS listener", () => {
const input = (DeployCertToTencentCLB as any).define.input;
assert.equal(input.loadBalancerId.component.name, "remote-select");
assert.equal(input.loadBalancerId.component.single, true);
assert.equal(input.loadBalancerId.component.action, "onGetCLBList");
assert.equal(input.loadBalancerId.required, true);
assert.equal(input.listenerId.component.name, "remote-select");
assert.equal(input.listenerId.component.single, true);
assert.equal(input.listenerId.component.action, "onGetListenerList");
assert.deepEqual(input.listenerId.component.watches, ["certDomains", "accessId", "region", "loadBalancerId"]);
assert.equal(input.listenerId.required, true);
assert.equal(input.domain.component.name, "remote-select");
assert.equal(input.domain.component.single, false);
assert.equal(input.domain.component.action, "onGetDomainList");
assert.deepEqual(input.domain.component.watches, ["certDomains", "accessId", "region", "loadBalancerId", "listenerId"]);
assert.equal(input.domain.required, false);
});
it("maps CLB API results to remote-select options", async () => {
const plugin = new DeployCertToTencentCLB();
plugin.accessId = "access-1";
plugin.logger = { info: () => undefined } as any;
(plugin as any).getClient = async () => ({});
(plugin as any).getCLBList = async () => [
{
LoadBalancerId: "lb-1",
LoadBalancerName: "业务负载均衡",
},
];
const options = await plugin.onGetCLBList({});
assert.deepEqual(options, [
{
value: "lb-1",
label: "业务负载均衡<lb-1>",
},
]);
});
it("maps HTTPS listener API results after selecting a CLB", async () => {
const plugin = new DeployCertToTencentCLB();
plugin.accessId = "access-1";
plugin.loadBalancerId = "lb-1";
plugin.logger = { info: () => undefined } as any;
(plugin as any).getClient = async () => ({});
(plugin as any).getListenerList = async (_client: any, loadBalancerId: string, listenerIds: any) => {
assert.equal(loadBalancerId, "lb-1");
assert.equal(listenerIds, null);
return [
{
ListenerId: "listener-1",
ListenerName: "HTTPS监听器",
Port: 443,
},
];
};
const options = await plugin.onGetListenerList({});
assert.deepEqual(options, [
{
value: "listener-1",
label: "HTTPS监听器:443<listener-1>",
},
]);
});
it("maps SNI domains from the selected listener rules", async () => {
const plugin = new DeployCertToTencentCLB();
plugin.accessId = "access-1";
plugin.loadBalancerId = "lb-1";
plugin.listenerId = "listener-1";
plugin.logger = { info: () => undefined } as any;
(plugin as any).getClient = async () => ({});
(plugin as any).getListenerList = async (_client: any, loadBalancerId: string, listenerIds: string[]) => {
assert.equal(loadBalancerId, "lb-1");
assert.deepEqual(listenerIds, ["listener-1"]);
return [
{
ListenerId: "listener-1",
Rules: [{ Domain: "www.example.com" }, { Domain: "api.example.com" }],
},
];
};
const options = await plugin.onGetDomainList({});
assert.deepEqual(options, [
{
value: "www.example.com",
label: "www.example.com",
domain: "www.example.com",
},
{
value: "api.example.com",
label: "api.example.com",
domain: "api.example.com",
},
]);
});
});
@@ -1,7 +1,8 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import dayjs from "dayjs";
import { TencentAccess } from "../../../plugin-lib/tencent/index.js";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
@IsTaskPlugin({
name: "DeployCertToTencentCLB",
title: "腾讯云-部署到CLB",
@@ -73,29 +74,44 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
})
region!: string;
@TaskInput({
title: "负载均衡ID",
required: true,
})
@TaskInput(
createRemoteSelectInputDefine({
title: "负载均衡ID",
helper: "请选择要部署证书的负载均衡",
action: DeployCertToTencentCLB.prototype.onGetCLBList.name,
watches: ["region"],
single: true,
pager: false,
search: false,
})
)
loadBalancerId!: string;
@TaskInput({
title: "监听器ID",
required: true,
})
@TaskInput(
createRemoteSelectInputDefine({
title: "监听器ID",
helper: "请选择要部署证书的HTTPS监听器",
action: DeployCertToTencentCLB.prototype.onGetListenerList.name,
watches: ["region", "loadBalancerId"],
single: true,
pager: false,
search: false,
})
)
listenerId!: string;
@TaskInput({
title: "域名",
required: false,
component: {
name: "a-select",
vModel: "value",
open: false,
mode: "tags",
},
helper: "如果开启了sni,则此项必须填写,未开启,则不要填写",
})
@TaskInput(
createRemoteSelectInputDefine({
title: "域名",
helper: "如果开启了SNI,请选择要部署证书的域名;未开启SNI时可以留空",
action: DeployCertToTencentCLB.prototype.onGetDomainList.name,
watches: ["region", "loadBalancerId", "listenerId"],
required: false,
single: false,
pager: false,
search: false,
})
)
domain!: string | string[];
@TaskInput({
@@ -272,6 +288,27 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
return ret.LoadBalancerSet;
}
async onGetCLBList(data: PageSearch) {
if (!this.accessId) {
throw new Error("请选择Access提供者");
}
const client = await this.getClient();
const list = await this.getCLBList(client);
if (!list || list.length === 0) {
return [];
}
return list.map((item: any) => {
const loadBalancerId = item.LoadBalancerId;
const loadBalancerName = item.LoadBalancerName || loadBalancerId;
return {
value: loadBalancerId,
label: `${loadBalancerName}<${loadBalancerId}>`,
};
});
}
async getListenerList(client: any, balancerId: any, listenerIds: any) {
// HTTPS
const params = {
@@ -284,6 +321,57 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
return ret.Listeners;
}
async onGetListenerList(data: PageSearch) {
if (!this.accessId) {
throw new Error("请选择Access提供者");
}
if (!this.loadBalancerId) {
throw new Error("请先选择负载均衡");
}
const client = await this.getClient();
const list = await this.getListenerList(client, this.loadBalancerId, null);
if (!list || list.length === 0) {
return [];
}
return list.map((item: any) => {
const listenerId = item.ListenerId;
const listenerName = item.ListenerName || "HTTPS监听器";
const port = item.Port ? `:${item.Port}` : "";
return {
value: listenerId,
label: `${listenerName}${port}<${listenerId}>`,
};
});
}
async onGetDomainList(data: PageSearch) {
if (!this.accessId) {
throw new Error("请选择Access提供者");
}
if (!this.loadBalancerId) {
throw new Error("请先选择负载均衡");
}
if (!this.listenerId) {
throw new Error("请先选择监听器");
}
const client = await this.getClient();
const listeners = await this.getListenerList(client, this.loadBalancerId, [this.listenerId]);
const listener = listeners?.[0];
const domains = listener?.Rules?.map((rule: any) => rule.Domain).filter(Boolean) || [];
const uniqueDomains = [...new Set(domains)];
return uniqueDomains.map(domain => {
return {
value: domain,
label: domain,
domain,
};
});
}
checkRet(ret: any) {
if (!ret || ret.Error) {
throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message);