diff --git a/.gitignore b/.gitignore index 00c104f1f..b9c2e515e 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ test.js .history /logs .pnpm-lock.yaml -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml +.studio/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 8414615d6..6ef8e544e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,5 +20,6 @@ "scm.repositories.visible": 9, "scm.repositories.explorer": false, "scm.repositories.selectionMode": "multiple", - "scm.repositories.sortOrder": "discovery time" + "scm.repositories.sortOrder": "discovery time", + "git.ignoreLimitWarning": true } \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 3a008f4ec..6d053e2eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# Certd 开发 Agent 上下文 +# Certd 开发 Agent 上下文 这个文件是给在本仓库工作的开发 agent 看的常驻项目说明。后续会话进入仓库后,应先读取它,再按任务需要查看具体代码,避免每次都重新全量扫描项目。 @@ -218,3 +218,6 @@ Get-ChildItem packages\ui\certd-client\src\views\certd - 单个 monorepo 包运行单元测试时,优先使用 `corepack pnpm --dir <包目录> test:unit`,例如 `corepack pnpm --dir packages\ui\certd-server test:unit`、`corepack pnpm --dir packages\core\basic test:unit`、`corepack pnpm --dir packages\plugins\plugin-lib test:unit`;也可以用包名过滤,例如 `corepack pnpm --filter @certd/ui-server test:unit`。前端 `packages\ui\certd-client` 暂时不跑单元测试。 - 前端 TS/Vue/locale 等文件改动后,优先只对本次改动文件运行项目现有自动格式化/修复;Windows/PowerShell 下 Prettier 已验证可用命令为 `packages\ui\certd-client\node_modules\.bin\prettier.cmd --write `,ESLint 可用命令为 `packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix `;不要运行 `vue-tsc` / `pnpm tsc`;不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier。 - 优先对改动包运行聚焦的测试;后端可按包运行单元测试,前端优先使用 Prettier/ESLint 做改动文件验证。只有跨包影响明显时再考虑全 monorepo 构建。 + +- 不要主动运行 `pnpm install` 安装依赖:用户会事先准备好 `node_modules`。如果 `pnpm install` 或 `test:unit` 因缺少依赖、TTY 或网络问题失败,立即停止尝试,告知用户解决环境问题。 + diff --git a/packages/plugins/plugin-lib/src/cert/cert-reader.ts b/packages/plugins/plugin-lib/src/cert/cert-reader.ts index 277514df6..0bf366c5b 100644 --- a/packages/plugins/plugin-lib/src/cert/cert-reader.ts +++ b/packages/plugins/plugin-lib/src/cert/cert-reader.ts @@ -13,7 +13,7 @@ export interface ICertInfoGetter { export type CertInfo = { crt: string; //fullchain证书 key: string; //私钥 - csr: string; //csr + csr?: string; //csr oc?: string; //仅证书,非fullchain证书 ic?: string; //中间证书 pfx?: string; diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-live/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-live/index.ts new file mode 100644 index 000000000..8e5f1bbf4 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-live/index.ts @@ -0,0 +1,159 @@ +import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline'; +import { CertApplyPluginNames } from '@certd/plugin-cert'; +import { CertInfo, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib'; +import { AliyunAccess } from '../../../plugin-lib/aliyun/access/index.js'; +import { AliyunSslClient, CasCertId } from '../../../plugin-lib/aliyun/lib/index.js'; + +@IsTaskPlugin({ + name: 'DeployCertToAliyunLive', + title: '阿里云-部署至直播(Live)', + icon: 'svg:icon-aliyun', + group: pluginGroups.aliyun.key, + desc: '部署证书到阿里云视频直播(Live)域名', + needPlus: false, + default: { + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed, + }, + }, +}) +export class DeployCertToAliyunLive extends AbstractTaskPlugin { + + + @TaskInput({ + title: '域名证书', + helper: '请选择前置任务输出的域名证书', + component: { + name: 'output-selector', + from: [...CertApplyPluginNames, 'uploadCertToAliyun'], + }, + template: false, + required: true, + }) + cert!: CertInfo | CasCertId; + + @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) + certDomains!: string[]; + + + @TaskInput({ + title: 'Access授权', + helper: '阿里云授权AccessKeyId、AccessKeySecret', + component: { + name: 'access-selector', + type: 'aliyun', + }, + required: true, + }) + accessId!: string; + + @TaskInput({ + title: '证书服务接入点', + helper: '不会选就按默认', + value: 'cas.aliyuncs.com', + component: { + name: 'a-select', + options: [ + { value: 'cas.aliyuncs.com', label: '中国大陆' }, + { value: 'cas.ap-southeast-1.aliyuncs.com', label: '新加坡' }, + { value: 'cas.eu-central-1.aliyuncs.com', label: '德国(法兰克福)' }, + ], + }, + required: true, + }) + endpoint!: string; + + @TaskInput( + createRemoteSelectInputDefine({ + title: '直播域名', + helper: '请选择要部署证书的直播域名', + typeName: 'DeployCertToAliyunLive', + action: DeployCertToAliyunLive.prototype.onGetDomainList.name, + watches: ['certDomains', 'accessId'], + pager: true, + search: true, + }) + ) + domainList!: string[]; + + async onInstance() {} + + async execute(): Promise { + this.logger.info('开始部署证书到阿里云直播'); + const access = await this.getAccess(this.accessId); + + if (this.cert == null) { + throw new Error('域名证书参数为空,请检查前置任务'); + } + + const client = await this.getClient(access); + const sslClient = new AliyunSslClient({ + access, + logger: this.logger, + endpoint: this.endpoint || 'cas.aliyuncs.com', + }); + + // 确保证书已上传到 CAS,统一使用 cas 方式部署 + const casCert = await sslClient.uploadCertOrGet(this.cert); + // const certName = this.appendTimeSuffix(this.certName || casCert.certName); + for (const domain of this.domainList) { + const res = await client.doRequest({ + action: 'SetLiveDomainCertificate', + version: '2016-11-01', + protocol: 'HTTPS', + data: { + query: { + DomainName: domain, + CertName: casCert.certName, + CertType: 'cas', + SSLProtocol: 'on', + CertId: casCert.certId, + }, + }, + }); + this.logger.info('部署直播域名[' + domain + ']证书成功:' + JSON.stringify(res)); + } + } + + async getClient(access: AliyunAccess) { + const endpoint = 'live.aliyuncs.com'; + return access.getClient(endpoint); + } + + async onGetDomainList(data: PageSearch) { + if (!this.accessId) { + throw new Error('请选择Access授权'); + } + const access = await this.getAccess(this.accessId); + const client = await this.getClient(access); + + const res = await client.doRequest({ + action: 'DescribeLiveUserDomains', + version: '2016-11-01', + protocol: 'HTTPS', + data: { + query: { + DomainName: data.searchKey || undefined, + PageNumber: data.pageNo || 1, + PageSize: data.pageSize || 50, + }, + }, + }); + + const list = res?.Domains?.PageData; + if (!list || list.length === 0) { + throw new Error('没有找到直播域名,请先在阿里云添加直播域名'); + } + + const options = list.map((item: any) => { + return { + label: item.DomainName, + value: item.DomainName, + domain: item.DomainName, + }; + }); + return this.ctx.utils.options.buildGroupOptions(options, this.certDomains); + } +} + +new DeployCertToAliyunLive(); diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/index.ts index 8080cb96d..81ba9bb05 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/index.ts @@ -1,4 +1,4 @@ -export * from './deploy-to-cdn/index.js'; +export * from './deploy-to-cdn/index.js'; export * from './deploy-to-dcdn/index.js'; export * from './deploy-to-oss/index.js'; export * from './upload-to-aliyun/index.js'; @@ -10,8 +10,9 @@ export * from './deploy-to-fc/index.js'; export * from './deploy-to-esa/index.js'; export * from './deploy-to-ga/index.js'; export * from './deploy-to-vod/index.js'; +export * from './deploy-to-live/index.js'; export * from './deploy-to-apigateway/index.js'; export * from './deploy-to-apig/index.js'; export * from './deploy-to-ack/index.js'; export * from './deploy-to-all/index.js'; -export * from './delete-expiring-cert/index.js'; \ No newline at end of file +export * from './delete-expiring-cert/index.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts index eb1f52af9..4e3e51139 100644 --- a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts +++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts @@ -38,7 +38,7 @@ export type Providers = { export type CertInfo = { crt: string; //fullchain证书 key: string; //私钥 - csr: string; //csr + csr?: string; //csr oc?: string; //仅证书,非fullchain证书 ic?: string; //中间证书 pfx?: string; diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts index d4df9f8af..b493ea8fe 100644 --- a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts +++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts @@ -194,7 +194,7 @@ cert.jks:jks格式证书文件,java服务器使用 return pem; } - formatCerts(cert: { crt: string; key: string; csr: string }) { + formatCerts(cert: { crt: string; key: string; csr?: string }) { const newCert: CertInfo = { crt: this.formatCert(cert.crt), key: this.formatCert(cert.key),