diff --git a/C b/C new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/C @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index eb638d909..e9a5b57a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +### Bug Fixes + +* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167)) +* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b)) + +### Performance Improvements + +* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3)) +* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461)) + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) ### Bug Fixes diff --git a/docs/guide/changelogs/CHANGELOG.md b/docs/guide/changelogs/CHANGELOG.md index eb638d909..e9a5b57a3 100644 --- a/docs/guide/changelogs/CHANGELOG.md +++ b/docs/guide/changelogs/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +### Bug Fixes + +* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167)) +* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b)) + +### Performance Improvements + +* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3)) +* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461)) + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) ### Bug Fixes diff --git a/lerna.json b/lerna.json index 12ec730e2..99c74acca 100644 --- a/lerna.json +++ b/lerna.json @@ -9,5 +9,5 @@ } }, "npmClient": "pnpm", - "version": "1.42.4" + "version": "1.42.5" } diff --git a/package.json b/package.json index ed0169e9a..28f818c71 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit", "pub": "echo 1", "dev": "pnpm run -r --parallel compile ", + "lint_all": "pnpm run -r --parallel lint ", "pub_all": "node ./scripts/pub-all.js", "release": "time /t >trigger/release.trigger && git add trigger/release.trigger && git commit -m \"build: release\" && git push", "publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js", diff --git a/packages/core/acme-client/CHANGELOG.md b/packages/core/acme-client/CHANGELOG.md index 8b01a4871..0583e7eb8 100644 --- a/packages/core/acme-client/CHANGELOG.md +++ b/packages/core/acme-client/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/publishlab/node-acme-client/compare/v1.42.4...v1.42.5) (2026-07-15) + +**Note:** Version bump only for package @certd/acme-client + ## [1.42.4](https://github.com/publishlab/node-acme-client/compare/v1.42.3...v1.42.4) (2026-07-11) **Note:** Version bump only for package @certd/acme-client diff --git a/packages/core/acme-client/package.json b/packages/core/acme-client/package.json index c1fa8a21a..a974def44 100644 --- a/packages/core/acme-client/package.json +++ b/packages/core/acme-client/package.json @@ -3,7 +3,7 @@ "description": "Simple and unopinionated ACME client", "private": false, "author": "nmorsman", - "version": "1.42.4", + "version": "1.42.5", "type": "module", "module": "./dist/index.js", "main": "./dist/index.js", @@ -18,7 +18,7 @@ "types" ], "dependencies": { - "@certd/basic": "^1.42.4", + "@certd/basic": "^1.42.5", "@peculiar/x509": "^1.11.0", "asn1js": "^3.0.5", "axios": "^1.9.0", @@ -75,5 +75,5 @@ "bugs": { "url": "https://github.com/publishlab/node-acme-client/issues" }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/core/basic/CHANGELOG.md b/packages/core/basic/CHANGELOG.md index 802c41db0..7e26ded27 100644 --- a/packages/core/basic/CHANGELOG.md +++ b/packages/core/basic/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +**Note:** Version bump only for package @certd/basic + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) **Note:** Version bump only for package @certd/basic diff --git a/packages/core/basic/build.md b/packages/core/basic/build.md index 376926ba0..e2c91fe7d 100644 --- a/packages/core/basic/build.md +++ b/packages/core/basic/build.md @@ -1 +1 @@ -02:45 +23:31 diff --git a/packages/core/basic/package.json b/packages/core/basic/package.json index 8c7e91941..8a0adb355 100644 --- a/packages/core/basic/package.json +++ b/packages/core/basic/package.json @@ -1,7 +1,7 @@ { "name": "@certd/basic", "private": false, - "version": "1.42.4", + "version": "1.42.5", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", @@ -54,5 +54,5 @@ "tslib": "^2.8.1", "typescript": "^5.4.2" }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/core/pipeline/CHANGELOG.md b/packages/core/pipeline/CHANGELOG.md index 09aa981e2..023e40e68 100644 --- a/packages/core/pipeline/CHANGELOG.md +++ b/packages/core/pipeline/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +### Bug Fixes + +* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167)) + +### Performance Improvements + +* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461)) + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) ### Bug Fixes diff --git a/packages/core/pipeline/package.json b/packages/core/pipeline/package.json index 218b20c3e..6eaf3f489 100644 --- a/packages/core/pipeline/package.json +++ b/packages/core/pipeline/package.json @@ -1,7 +1,7 @@ { "name": "@certd/pipeline", "private": false, - "version": "1.42.4", + "version": "1.42.5", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", @@ -21,8 +21,8 @@ "lint": "eslint --fix" }, "dependencies": { - "@certd/basic": "^1.42.4", - "@certd/plus-core": "^1.42.4", + "@certd/basic": "^1.42.5", + "@certd/plus-core": "^1.42.5", "dayjs": "^1.11.7", "lodash-es": "^4.17.21", "reflect-metadata": "^0.2.2" @@ -51,5 +51,5 @@ "tslib": "^2.8.1", "typescript": "^5.4.2" }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/core/pipeline/src/access/api.ts b/packages/core/pipeline/src/access/api.ts index d72c71cc5..92ee6c371 100644 --- a/packages/core/pipeline/src/access/api.ts +++ b/packages/core/pipeline/src/access/api.ts @@ -3,7 +3,7 @@ import { FormItemProps } from "../dt/index.js"; import { HttpClient, ILogger, utils } from "@certd/basic"; import * as _ from "lodash-es"; import { PluginRequestHandleReq } from "../plugin/index.js"; -import { IRuntimeDepsService, IServiceGetter } from "../service/index.js"; +import { IServiceGetter, getRuntimeDepsService } from "../service/index.js"; // export type AccessRequestHandleReqInput = { // id?: number; @@ -48,20 +48,13 @@ export type AccessContext = { export abstract class BaseAccess implements IAccess { ctx!: AccessContext; - runtimeDepsService?: IRuntimeDepsService; async importRuntime(specifier: string) { - if (!this.runtimeDepsService) { - throw new Error("runtimeDepsService 未初始化"); - } - return await this.runtimeDepsService.importRuntime(specifier, this.ctx.logger); + return await getRuntimeDepsService().importRuntime(specifier, this.ctx.logger); } async setCtx(ctx: AccessContext) { this.ctx = ctx; - if (!this.runtimeDepsService && this.ctx.serviceGetter) { - this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService"); - } } async onRequest(req: AccessRequestHandleReq) { diff --git a/packages/core/pipeline/src/notification/api.ts b/packages/core/pipeline/src/notification/api.ts index c083fe42c..c79d309a4 100644 --- a/packages/core/pipeline/src/notification/api.ts +++ b/packages/core/pipeline/src/notification/api.ts @@ -3,7 +3,7 @@ import { Registrable } from "../registry/index.js"; import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js"; import { HttpClient, ILogger, utils } from "@certd/basic"; import * as _ from "lodash-es"; -import { IEmailService, IRuntimeDepsService, IServiceGetter } from "../service/index.js"; +import { IEmailService, IServiceGetter, getRuntimeDepsService } from "../service/index.js"; export type NotificationBody = { userId?: number; @@ -89,16 +89,16 @@ export abstract class BaseNotification implements INotification { ctx!: NotificationContext; http!: HttpClient; logger!: ILogger; - runtimeDepsService?: IRuntimeDepsService; async importRuntime(specifier: string) { - if (!this.runtimeDepsService) { - return await import(specifier); - } - return await this.runtimeDepsService.importRuntime(specifier, this.logger); + return await getRuntimeDepsService().importRuntime(specifier, this.logger); } async doSend(body: NotificationBody) { + if (body.content) { + const content = body.content?.replace(/\n/g, " \n"); + body.content = content; + } return await this.send(body); } abstract send(body: NotificationBody): Promise; @@ -109,9 +109,6 @@ export abstract class BaseNotification implements INotification { this.ctx = ctx; this.http = ctx.http; this.logger = ctx.logger; - if (!this.runtimeDepsService && this.ctx.serviceGetter) { - this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService"); - } } setDefine = (define: NotificationDefine) => { this.define = define; diff --git a/packages/core/pipeline/src/plugin/api.ts b/packages/core/pipeline/src/plugin/api.ts index c01ae3795..544ad2c01 100644 --- a/packages/core/pipeline/src/plugin/api.ts +++ b/packages/core/pipeline/src/plugin/api.ts @@ -10,7 +10,7 @@ import { INotificationService } from "../notification/index.js"; import { Registrable } from "../registry/index.js"; import { IPluginConfigService } from "../service/config.js"; import { TaskEmitter } from "../service/emit.js"; -import { ICnameProxyService, IEmailService, IRuntimeDepsService, IServiceGetter, IUrlService } from "../service/index.js"; +import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService, getRuntimeDepsService } from "../service/index.js"; export type PluginRequestHandleReq = { typeName: string; @@ -76,7 +76,7 @@ export type ITaskPlugin = { execute(): Promise; onRequest(req: PluginRequestHandleReq): Promise; setCtx(ctx: TaskInstanceContext): Promise; - importRuntime?(specifier: string): Promise; + importRuntime(specifier: string): Promise; [key: string]: any; }; @@ -150,13 +150,9 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin { logger!: ILogger; http!: HttpClient; accessService!: IAccessService; - runtimeDepsService!: IRuntimeDepsService; async importRuntime(specifier: string) { - if (!this.runtimeDepsService) { - throw new Error("runtimeDepsService 未初始化"); - } - return await this.runtimeDepsService.importRuntime(specifier, this.logger); + return await getRuntimeDepsService().importRuntime(specifier, this.logger); } clearLastStatus() { @@ -178,9 +174,6 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin { this.logger = ctx.logger; this.accessService = ctx.accessService; this.http = ctx.http; - if (!this.runtimeDepsService && this.ctx.serviceGetter) { - this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService"); - } // 将证书加入secret // @ts-ignore if (this.cert && this.cert.crt && this.cert.key) { diff --git a/packages/ui/certd-server/src/modules/runtime-deps/runtime-deps-service.test.ts b/packages/core/pipeline/src/service/runtime.test.ts similarity index 61% rename from packages/ui/certd-server/src/modules/runtime-deps/runtime-deps-service.test.ts rename to packages/core/pipeline/src/service/runtime.test.ts index 02cbd6f73..1947840a3 100644 --- a/packages/ui/certd-server/src/modules/runtime-deps/runtime-deps-service.test.ts +++ b/packages/core/pipeline/src/service/runtime.test.ts @@ -1,38 +1,35 @@ -import assert from "assert"; +import assert from "assert"; import fs from "fs"; import path from "path"; import os from "os"; -import { RuntimeDepsService, type RuntimeDependencyPluginDefine } from "./runtime-deps-service.js"; -import { accessRegistry, pluginRegistry } from "@certd/pipeline"; -import { addonRegistry } from "@certd/lib-server"; +import { RuntimeDepsService, NpmRegistryResolver, type RuntimeDependencyPluginDefine } from "./runtime.js"; +import { accessRegistry } from "../access/registry.js"; +import { pluginRegistry } from "../plugin/registry.js"; describe("RuntimeDepsService", () => { it("detects conflicting dependency ranges across plugins", () => { - const service = new RuntimeDepsService(); + const service = new RuntimeDepsService({}, null); const merged = service.collectDependencies([ { name: "a", dependPackages: { foo: "^1.0.0" } }, { name: "b", dependPackages: { foo: "^1.2.0" } }, ]); - assert.deepEqual(merged.dependencies, { foo: "^1.0.0" }); assert.equal(merged.conflicts.length, 0); }); it("reports incompatible dependency ranges", () => { - const service = new RuntimeDepsService(); + const service = new RuntimeDepsService({}, null); const merged = service.collectDependencies([ { name: "a", dependPackages: { foo: "^1.0.0" } }, { name: "b", dependPackages: { foo: "^2.0.0" } }, ]); - assert.equal(merged.conflicts.length, 1); assert.equal(merged.conflicts[0].packageName, "foo"); }); it("builds a runtime package manifest in the target directory", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; + const service = new RuntimeDepsService({ rootDir }, null); service.registryResolver = { async resolve() { return "https://registry.npmmirror.com"; @@ -50,18 +47,15 @@ describe("RuntimeDepsService", () => { return { stdout: "", stderr: "", code: 0 }; }, } as any; - const plugins: RuntimeDependencyPluginDefine[] = [{ name: "a", dependPackages: { foo: "^1.0.0" } }]; const result = await service.ensureInstalled({ plugins }); - assert.equal(result.registryUrl, "https://registry.npmmirror.com"); assert.ok(fs.existsSync(path.join(rootDir, "package.json"))); }); it("installs direct dependency maps without plugin metadata", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-direct-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; + const service = new RuntimeDepsService({ rootDir }, null); service.registryResolver = { async resolve() { return ""; @@ -77,9 +71,7 @@ describe("RuntimeDepsService", () => { return { stdout: "", stderr: "", code: 0 }; }, } as any; - await service.ensureDependencies({ dependencies: { directPkg: "^1.0.0" } }); - const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); assert.deepEqual(manifest.dependencies, { directPkg: "^1.0.0" }); }); @@ -91,27 +83,19 @@ describe("RuntimeDepsService", () => { fs.writeFileSync(path.join(rootDir, "package.json"), JSON.stringify({ name: "runtime-root", type: "module" }), "utf8"); fs.writeFileSync(path.join(packageDir, "package.json"), JSON.stringify({ name: "runtime-only", type: "module", main: "index.js" }), "utf8"); fs.writeFileSync(path.join(packageDir, "index.js"), "export const value = 42;\n", "utf8"); - - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; + const service = new RuntimeDepsService({ rootDir }, null); service.commandRunner = { async run() { throw new Error("install should not run"); }, } as any; - const mod = await service.importRuntime("runtime-only"); - assert.equal(mod.value, 42); }); it("installs configured lazy dependency when import target is missing", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; - service.lazyDependencies = { - "lazy-pkg": "^1.2.3", - }; + const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "lazy-pkg": "^1.2.3" } }, null); service.registryResolver = { async resolve() { return ""; @@ -130,9 +114,7 @@ describe("RuntimeDepsService", () => { return { stdout: "", stderr: "", code: 0 }; }, } as any; - const mod = await service.importRuntime("lazy-pkg/sub/entry.js"); - const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); assert.deepEqual(manifest.dependencies, { "lazy-pkg": "^1.2.3" }); assert.equal(mod.value, 7); @@ -140,11 +122,7 @@ describe("RuntimeDepsService", () => { it("resolves scoped package names for lazy imports", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; - service.lazyDependencies = { - "@scope/lazy": "^2.0.0", - }; + const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "@scope/lazy": "^2.0.0" } }, null); service.registryResolver = { async resolve() { return ""; @@ -163,9 +141,7 @@ describe("RuntimeDepsService", () => { return { stdout: "", stderr: "", code: 0 }; }, } as any; - const mod = await service.importRuntime("@scope/lazy/dist/index.js"); - const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); assert.deepEqual(manifest.dependencies, { "@scope/lazy": "^2.0.0" }); assert.equal(mod.scoped, true); @@ -173,55 +149,20 @@ describe("RuntimeDepsService", () => { it("reports missing lazy dependency configuration", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-missing-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; - service.lazyDependencies = {}; - + const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} }, null); await assert.rejects(() => service.importRuntime("missing-pkg/sub.js"), /未配置懒加载版本: missing-pkg/); }); it("falls back to project node_modules when lazy dependency is not configured", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; - service.lazyDependencies = {}; - + const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} }, null); const mod = await service.importRuntime("dayjs"); - - assert.equal(typeof mod.default, "function"); - }); - - it("falls back to project node_modules when lazy install fails", async () => { - const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-install-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; - service.lazyDependencies = { - dayjs: "^1.11.7", - }; - service.registryResolver = { - async resolve() { - return ""; - }, - } as any; - service.commandRunner = { - async run(command: string, args: string[]) { - assert.equal(command, "pnpm"); - if (args.includes("--version")) { - return { stdout: "9.1.0\n", stderr: "", code: 0 }; - } - return { stdout: "", stderr: "install failed in test", code: 1 }; - }, - } as any; - - const mod = await service.importRuntime("dayjs"); - assert.equal(typeof mod.default, "function"); }); it("keeps previously installed dependencies when installing a later plugin", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; + const service = new RuntimeDepsService({ rootDir }, null); service.registryResolver = { async resolve() { return ""; @@ -237,22 +178,17 @@ describe("RuntimeDepsService", () => { return { stdout: "", stderr: "", code: 0 }; }, } as any; - await service.ensureInstalled({ plugins: [{ name: "a", pluginType: "deploy", dependPackages: { foo: "^1.0.0" } }] }); await service.ensureInstalled({ plugins: [{ name: "b", pluginType: "deploy", dependPackages: { bar: "^2.0.0" } }] }); - const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); - assert.deepEqual(manifest.dependencies, { - foo: "^1.0.0", - bar: "^2.0.0", - }); + assert.deepEqual(manifest.dependencies, { foo: "^1.0.0", bar: "^2.0.0" }); }); - it("includes npm dependencies from dependent plugins", () => { - const service = new RuntimeDepsService(); + it("includes npm dependencies from dependent plugins", async () => { + const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry }); accessRegistry.register("runtimeDepsAccess", { define: { name: "runtimeDepsAccess", title: "access", dependPackages: { accessOnly: "^1.0.0" } } as any, - target: async () => ({} as any), + target: async () => ({}) as any, }); try { const resolved = service.resolvePluginDependencies({ @@ -262,11 +198,7 @@ describe("RuntimeDepsService", () => { dependPackages: { deployOnly: "^1.0.0" }, }); const merged = service.collectDependencies(resolved); - - assert.deepEqual(merged.dependencies, { - deployOnly: "^1.0.0", - accessOnly: "^1.0.0", - }); + assert.deepEqual(merged.dependencies, { deployOnly: "^1.0.0", accessOnly: "^1.0.0" }); } finally { accessRegistry.unRegister("runtimeDepsAccess"); } @@ -274,8 +206,7 @@ describe("RuntimeDepsService", () => { it("installs dependencies by registered plugin key", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-key-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; + const service = new RuntimeDepsService({ rootDir }, { pluginRegistry, accessRegistry }); service.registryResolver = { async resolve() { return ""; @@ -293,11 +224,11 @@ describe("RuntimeDepsService", () => { } as any; pluginRegistry.register("runtimeDepsKey", { define: { name: "runtimeDepsKey", title: "key", dependPackages: { keyed: "^1.0.0" } } as any, - target: async () => ({} as any), + target: async () => ({}) as any, }); try { + service.setRegistries({ pluginRegistry, accessRegistry }); await service.ensureRuntimeDependencies({ pluginKeys: "plugin:runtimeDepsKey" }); - const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); assert.deepEqual(manifest.dependencies, { keyed: "^1.0.0" }); } finally { @@ -305,58 +236,16 @@ describe("RuntimeDepsService", () => { } }); - it("installs dependencies from multiple plugin keys including addon subtype keys", async () => { - const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-keys-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; - service.registryResolver = { - async resolve() { - return ""; - }, - } as any; - service.commandRunner = { - async run(command: string, args: string[]) { - assert.equal(command, "pnpm"); - if (args.includes("--version")) { - return { stdout: "9.1.0\n", stderr: "", code: 0 }; - } - fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true }); - return { stdout: "", stderr: "", code: 0 }; - }, - } as any; - accessRegistry.register("runtimeDepsArrayAccess", { - define: { name: "runtimeDepsArrayAccess", title: "access", dependPackages: { accessPkg: "^1.0.0" } } as any, - target: async () => ({} as any), - }); - addonRegistry.register("captcha:runtimeDepsArrayAddon", { - define: { addonType: "captcha", name: "runtimeDepsArrayAddon", title: "addon", dependPackages: { addonPkg: "^2.0.0" } } as any, - target: async () => ({} as any), - }); - try { - await service.ensureRuntimeDependencies({ pluginKeys: ["access:runtimeDepsArrayAccess", "addon:captcha:runtimeDepsArrayAddon"] }); - - const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); - assert.deepEqual(manifest.dependencies, { - accessPkg: "^1.0.0", - addonPkg: "^2.0.0", - }); - } finally { - accessRegistry.unRegister("runtimeDepsArrayAccess"); - addonRegistry.unRegister("captcha:runtimeDepsArrayAddon"); - } - }); - it("reports missing dependent plugins", () => { - const service = new RuntimeDepsService(); - + const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry }); assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { "access:access": "*" } }), /插件依赖缺失/); }); it("reports incompatible dependent plugin versions", () => { - const service = new RuntimeDepsService(); + const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry }); accessRegistry.register("runtimeDepsVersionedAccess", { define: { name: "runtimeDepsVersionedAccess", title: "access", version: "1.4.0", dependPackages: { accessOnly: "^1.0.0" } } as any, - target: async () => ({} as any), + target: async () => ({}) as any, }); try { assert.throws( @@ -374,73 +263,10 @@ describe("RuntimeDepsService", () => { }); it("reports bare dependent plugin names as invalid format", () => { - const service = new RuntimeDepsService(); - + const service = new RuntimeDepsService({}, null); assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { runtimeDepsBareName: "*" } }), /插件依赖格式错误/); }); - it("records runtime install environment state", async () => { - const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-state-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; - service.registryResolver = { - async resolve() { - return ""; - }, - } as any; - service.commandRunner = { - async run(command: string, args: string[]) { - assert.equal(command, "pnpm"); - if (args.includes("--version")) { - return { stdout: "9.1.0\n", stderr: "", code: 0 }; - } - assert.equal(args[0], "install"); - return { stdout: "", stderr: "", code: 0 }; - }, - } as any; - - await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] }); - - const state = JSON.parse(fs.readFileSync(path.join(rootDir, "install-state.json"), "utf8")); - assert.equal(state.nodeVersion, process.version); - assert.equal(state.pnpmVersion, "9.1.0"); - assert.equal(state.lastError, undefined); - }); - - it("serializes installs with a file lock", async () => { - const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lock-")); - const serviceA = new RuntimeDepsService(); - const serviceB = new RuntimeDepsService(); - for (const service of [serviceA, serviceB]) { - service.runtimeDepsRootDir = rootDir; - service.registryResolver = { - async resolve() { - return ""; - }, - } as any; - } - let installCount = 0; - const commandRunner = { - async run(command: string, args: string[]) { - assert.equal(command, "pnpm"); - if (args.includes("--version")) { - return { stdout: "9.1.0\n", stderr: "", code: 0 }; - } - assert.equal(args[0], "install"); - installCount++; - await new Promise(resolve => setTimeout(resolve, 50)); - fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true }); - return { stdout: "", stderr: "", code: 0 }; - }, - }; - serviceA.commandRunner = commandRunner as any; - serviceB.commandRunner = commandRunner as any; - - await Promise.all([serviceA.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] }), serviceB.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] })]); - - assert.equal(installCount, 1); - }); - it("does not pass node debugger options to pnpm child process", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-env-")); const oldNodeOptions = process.env.NODE_OPTIONS; @@ -448,8 +274,7 @@ describe("RuntimeDepsService", () => { process.env.NODE_OPTIONS = "--inspect=127.0.0.1:9229 --max-old-space-size=4096"; process.env.VSCODE_INSPECTOR_OPTIONS = '{"inspectorIpc":"test"}'; try { - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; + const service = new RuntimeDepsService({ rootDir }, null); service.registryResolver = { async resolve() { return ""; @@ -467,7 +292,6 @@ describe("RuntimeDepsService", () => { return { stdout: "", stderr: "", code: 0 }; }, } as any; - await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] }); } finally { if (oldNodeOptions == null) { @@ -483,27 +307,38 @@ describe("RuntimeDepsService", () => { } }); - it.skip("clears runtime dependency directory", async () => { - const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-")); - const runtimeRootDir = path.join(rootDir, ".runtime-deps"); - fs.mkdirSync(path.join(runtimeRootDir, "node_modules", "foo"), { recursive: true }); - fs.writeFileSync(path.join(runtimeRootDir, "package.json"), "{}", "utf8"); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = runtimeRootDir; - service.installTimeoutMs = 1000; - - await service.clearRuntimeDeps(); - - assert.equal(fs.existsSync(runtimeRootDir), true); - const remainingEntries = fs.readdirSync(runtimeRootDir).filter(e => e !== ".install.lock"); - assert.equal(remainingEntries.length, 0); - }); - it("rejects clearing unexpected runtime dependency path", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-")); - const service = new RuntimeDepsService(); - service.runtimeDepsRootDir = rootDir; - + const service = new RuntimeDepsService({ rootDir }, null); await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/); }); }); + +describe("NpmRegistryResolver", () => { + it("chooses the fastest successful registry in auto mode", async () => { + const resolver = new NpmRegistryResolver({ + mode: "auto", + candidates: ["https://slow.example.com", "https://fast.example.com"], + probeTimeoutMs: 100, + cacheTtlMs: 1000, + }); + resolver.probe = async (registryUrl: string) => ({ + registryUrl, + ok: true, + elapsedMs: registryUrl.includes("fast") ? 10 : 50, + }); + const result = await resolver.resolve(); + assert.equal(result, "https://fast.example.com"); + }); + + it("uses fixed registry without probing", async () => { + const resolver = new NpmRegistryResolver({ + mode: "fixed", + fixedUrl: "https://registry.example.com", + probeTimeoutMs: 100, + cacheTtlMs: 1000, + }); + const result = await resolver.resolve(); + assert.equal(result, "https://registry.example.com"); + }); +}); diff --git a/packages/core/pipeline/src/service/runtime.ts b/packages/core/pipeline/src/service/runtime.ts index 38207bac8..8c7ea7a4c 100644 --- a/packages/core/pipeline/src/service/runtime.ts +++ b/packages/core/pipeline/src/service/runtime.ts @@ -1,27 +1,773 @@ -/** - * 运行时动态导入函数类型 - */ -export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise; - -/** - * 日志接口 - */ +import fs from "fs"; +import path from "path"; +import { spawn } from "child_process"; +import crypto from "crypto"; +import { createRequire } from "module"; +import { pathToFileURL } from "url"; +import { logger as defaultLogger } from "@certd/basic"; +import type { Registry } from "../registry/registry.js"; export type ILogger = { info: (message: string) => void; + warn?: (message: string) => void; + error?: (message: string, ...args: any[]) => void; }; -/** - * 运行时依赖服务参数 - */ +export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise; + export type EnsureRuntimeDepsOptions = { pluginKeys: string | string[]; logger?: ILogger; }; -/** - * 运行时依赖服务接口 - */ export interface IRuntimeDepsService { ensureRuntimeDependencies(options: EnsureRuntimeDepsOptions): Promise; importRuntime: ImportRuntime; } + +export type RuntimeDependencyPluginDefine = { + name: string; + key?: string; + title?: string; + version?: string; + pluginType?: string; + addonType?: string; + dependPlugins?: Record; + dependPackages?: Record; +}; + +type RegisteredDefineLike = RuntimeDependencyPluginDefine & { + key?: string; + pluginType?: string; + addonType?: string; + dependPlugins?: Record; + dependPackages?: Record; +}; + +type DependencyConflict = { + packageName: string; + ranges: Array<{ pluginName: string; range: string }>; +}; + +type CollectDependenciesResult = { + dependencies: Record; + conflicts: DependencyConflict[]; +}; + +type InstallResult = { + registryUrl: string; + packageJsonPath: string; +}; + +type RuntimeImportResolveResult = { + resolved: string; + packageName: string; +}; + +type CommandRunnerResult = { + stdout: string; + stderr: string; + code: number; +}; + +type CommandRunner = { + run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise; +}; + +export type NpmRegistryResolverConfig = { + mode?: "auto" | "fixed" | "system"; + fixedUrl?: string; + candidates?: string[]; + probeTimeoutMs?: number; + cacheTtlMs?: number; +}; + +export type RegistryProbeResult = { + registryUrl: string; + ok: boolean; + elapsedMs: number; +}; + +export class NpmRegistryResolver { + config: NpmRegistryResolverConfig; + private cache?: { registryUrl: string; expiresAt: number }; + + constructor(config?: NpmRegistryResolverConfig) { + this.config = config || {}; + } + + async resolve(): Promise { + const config = this.config; + if (config?.mode === "fixed" && config.fixedUrl) { + return config.fixedUrl; + } + if (config?.mode === "system") { + return ""; + } + const cached = this.cache; + if (cached && cached.expiresAt > Date.now()) { + return cached.registryUrl; + } + const candidates = (config?.candidates || []).filter(Boolean); + if (candidates.length === 0) { + return ""; + } + const probes = await Promise.allSettled(candidates.map(registryUrl => this.probe(registryUrl))); + const okList = probes.map(item => (item.status === "fulfilled" ? item.value : null)).filter((item): item is RegistryProbeResult => !!item && item.ok); + if (okList.length > 0) { + okList.sort((a, b) => a.elapsedMs - b.elapsedMs); + const best = okList[0].registryUrl; + this.cache = { registryUrl: best, expiresAt: Date.now() + (config?.cacheTtlMs || 6 * 60 * 60 * 1000) }; + return best; + } + return ""; + } + + async probe(registryUrl: string): Promise { + const timeoutMs = this.config?.probeTimeoutMs || 3000; + const started = Date.now(); + try { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await fetch(`${registryUrl.replace(/\/$/, "")}/-/ping`, { signal: controller.signal }); + return { registryUrl, ok: res.ok, elapsedMs: Date.now() - started }; + } finally { + clearTimeout(timer); + } + } catch { + return { registryUrl, ok: false, elapsedMs: Date.now() - started }; + } + } +} + +export type RuntimeDepsConfig = { + rootDir?: string; + autoInstall?: boolean; + enabled?: boolean; + installTimeoutMs?: number; + pnpmCommand?: string; + lazyDependencies?: Record; + registry?: NpmRegistryResolverConfig; +}; + +function normalizeRange(range: string) { + return range.trim().replace(/^\^/, "").replace(/^~?/, ""); +} + +function areRangesCompatible(a: string, b: string) { + if (!a || !b) { + return true; + } + if (a === "*" || b === "*") { + return true; + } + const left = normalizeRange(a).split("."); + const right = normalizeRange(b).split("."); + return left[0] === right[0]; +} + +const PROCESS_LOCKS = new Map>(); + +class DefaultCommandRunner implements CommandRunner { + async run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise { + return await new Promise(resolve => { + let stdout = ""; + let stderr = ""; + let settled = false; + const child = spawn(command, args, { cwd: options.cwd, env: options.env, windowsHide: true, shell: process.platform === "win32" }); + const timer = setTimeout(() => { + if (settled) { + return; + } + settled = true; + child.kill("SIGTERM"); + resolve({ stdout, stderr: stderr || `command timeout after ${options.timeoutMs}ms`, code: 1 }); + }, options.timeoutMs); + child.stdout?.on("data", chunk => { + stdout += chunk.toString(); + }); + child.stderr?.on("data", chunk => { + stderr += chunk.toString(); + }); + child.on("error", error => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + resolve({ stdout, stderr: error.message, code: 1 }); + }); + child.on("close", code => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + resolve({ stdout, stderr, code: code || 0 }); + }); + }); + } +} + +export class RuntimeDepsService { + runtimeDepsRootDir: string; + autoInstall: boolean; + enabled: boolean; + installTimeoutMs: number; + pnpmCommand: string; + lazyDependencies: Record; + registryResolver!: NpmRegistryResolver; + commandRunner: CommandRunner = new DefaultCommandRunner(); + pluginLazyDependencies: Record = {}; + private installPromises = new Map>(); + private registriesMap: Record; pluginType: string; addonType?: string }> | null = null; + + constructor(config: RuntimeDepsConfig, registries: any) { + this.runtimeDepsRootDir = config?.rootDir ?? "./data/.runtime-deps"; + this.autoInstall = config?.autoInstall ?? true; + this.enabled = config?.enabled ?? true; + this.installTimeoutMs = config?.installTimeoutMs ?? 120000; + this.pnpmCommand = config?.pnpmCommand ?? ""; + this.lazyDependencies = config?.lazyDependencies ?? {}; + this.registryResolver = new NpmRegistryResolver(config?.registry); + if (registries) { + this.setRegistries(registries); + } + } + + setRegistries(registries: { pluginRegistry?: Registry; accessRegistry?: Registry; notificationRegistry?: Registry; dnsProviderRegistry?: Registry; addonRegistry?: Registry }) { + const map: Record; pluginType: string; addonType?: string }> = {}; + if (registries.pluginRegistry) { + map["plugin"] = { registry: registries.pluginRegistry, pluginType: "plugin" }; + } + if (registries.accessRegistry) { + map["access"] = { registry: registries.accessRegistry, pluginType: "access" }; + } + if (registries.notificationRegistry) { + map["notification"] = { registry: registries.notificationRegistry, pluginType: "notification" }; + } + if (registries.dnsProviderRegistry) { + map["dnsProvider"] = { registry: registries.dnsProviderRegistry, pluginType: "dnsProvider" }; + } + if (registries.addonRegistry) { + map["addon"] = { registry: registries.addonRegistry, pluginType: "addon", addonType: "" }; + } + this.registriesMap = map; + } + + collectDependencies(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult { + const merged: Record = {}; + const seen: Record> = {}; + for (const plugin of plugins) { + const deps = plugin.dependPackages || {}; + for (const [packageName, range] of Object.entries(deps)) { + seen[packageName] ||= []; + seen[packageName].push({ pluginName: plugin.name, range }); + } + } + const conflicts: DependencyConflict[] = []; + for (const [packageName, ranges] of Object.entries(seen)) { + const first = ranges[0]?.range; + if (!first) { + continue; + } + const conflict = ranges.some(item => !areRangesCompatible(first, item.range)); + if (conflict) { + conflicts.push({ packageName, ranges }); + continue; + } + merged[packageName] = first; + } + return { dependencies: merged, conflicts }; + } + + async ensureInstalled(options: { plugins: RuntimeDependencyPluginDefine[]; logger?: ILogger }): Promise { + const { plugins, logger: log } = options; + const { dependencies, conflicts } = this.resolveDependenciesFromPlugins(plugins); + if (conflicts.length > 0) { + const conflict = conflicts[0]; + throw new Error(`动态依赖版本冲突: ${conflict.packageName} => ${conflict.ranges.map(item => `${item.pluginName}:${item.range}`).join(", ")}`); + } + return await this.ensureDependencies({ dependencies, logger: log }); + } + + async ensureDependencies(options: { dependencies: Record; logger?: ILogger }): Promise { + const { dependencies, logger: log } = options; + if (!this.enabled) { + return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") }; + } + if (!this.autoInstall) { + return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") }; + } + const dependenciesHash = this.createDependenciesHash(dependencies); + let installPromise = this.installPromises.get(dependenciesHash); + if (installPromise) { + const nodeModulesPath = path.join(this.getRuntimeDepsRootDir(), "node_modules"); + if (!fs.existsSync(nodeModulesPath)) { + this.installPromises.delete(dependenciesHash); + installPromise = undefined; + } + } + if (!installPromise) { + installPromise = this.doEnsureInstalled({ dependencies, logger: log }).catch(error => { + this.installPromises.delete(dependenciesHash); + throw error; + }); + this.installPromises.set(dependenciesHash, installPromise); + } + return await installPromise; + } + + resolveDependenciesFromPlugins(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult { + const expandedPlugins = plugins.flatMap(plugin => this.resolvePluginDependencies(plugin)); + return this.collectDependencies(expandedPlugins); + } + + async ensureRuntimeDependencies(options: { pluginKeys: string | string[]; logger?: ILogger }): Promise { + const { pluginKeys, logger: log } = options; + const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys]; + const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey)); + if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) { + return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") }; + } + const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine)); + return await this.ensureInstalled({ plugins: expandedPluginDefines, logger: log }); + } + + async importRuntime(specifier: string, logger: ILogger = defaultLogger) { + if (this.isNativeImportSpecifier(specifier)) { + return await import(specifier); + } + const resolved = await this.resolveImportSpecifier(specifier, logger); + return await import(pathToFileURL(resolved).href); + } + + private async resolveImportSpecifier(specifier: string, logger: ILogger = defaultLogger) { + try { + return this.resolveRuntimeSpecifier(specifier).resolved; + } catch (runtimeError: any) { + if (!this.isModuleNotFoundError(runtimeError)) { + throw runtimeError; + } + return await this.resolveMissingRuntimeSpecifier(specifier, runtimeError, logger); + } + } + + private async resolveMissingRuntimeSpecifier(specifier: string, runtimeError: any, logger?: ILogger) { + const packageName = this.parsePackageName(specifier); + const mergedDeps = this.getMergedLazyDependencies(); + const lazyRange = mergedDeps[packageName]; + if (!lazyRange) { + try { + return this.resolveProjectSpecifier(specifier, runtimeError).resolved; + } catch { + throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`); + } + } + try { + await this.ensureLazyDependency(packageName, logger); + return this.resolveRuntimeSpecifier(specifier).resolved; + } catch (lazyError: any) { + return this.resolveProjectSpecifier(specifier, lazyError).resolved; + } + } + + private isNativeImportSpecifier(specifier: string) { + return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:"); + } + + private resolveRuntimeSpecifier(specifier: string): RuntimeImportResolveResult { + const packageName = this.parsePackageName(specifier); + const packageJsonPath = path.join(this.getRuntimeDepsRootDir(), "package.json"); + const require = createRequire(packageJsonPath); + const resolved = require.resolve(specifier); + return { packageName, resolved }; + } + + private resolveProjectSpecifier(specifier: string, cause?: any): RuntimeImportResolveResult { + try { + const packageName = this.parsePackageName(specifier); + const packageJsonPath = path.resolve("package.json"); + const require = createRequire(packageJsonPath); + const resolved = require.resolve(specifier); + return { packageName, resolved }; + } catch (projectError: any) { + if (cause) { + projectError.cause = cause; + } + throw projectError; + } + } + + private parsePackageName(specifier: string) { + if (!specifier || specifier.trim() !== specifier) { + throw new Error(`动态依赖导入路径无效: ${specifier}`); + } + const parts = specifier.split("/"); + if (specifier.startsWith("@")) { + if (parts.length < 2 || !parts[0] || !parts[1]) { + throw new Error(`动态依赖导入路径无效: ${specifier}`); + } + return `${parts[0]}/${parts[1]}`; + } + if (!parts[0]) { + throw new Error(`动态依赖导入路径无效: ${specifier}`); + } + return parts[0]; + } + + private async ensureLazyDependency(packageName: string, logger?: ILogger) { + const range = this.lazyDependencies?.[packageName]; + if (!range) { + throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`); + } + await this.ensureDependencies({ dependencies: { [packageName]: range }, logger }); + } + + private isModuleNotFoundError(error: any) { + return error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND"; + } + + resolvePluginDependencies(current: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine[] { + const resolved: RuntimeDependencyPluginDefine[] = []; + const visited = new Set(); + const visit = (item: RuntimeDependencyPluginDefine) => { + const key = this.buildPluginDependencyKey(item); + if (visited.has(key)) { + return; + } + visited.add(key); + resolved.push(item); + for (const [dependencyName, expectedRange] of Object.entries(item.dependPlugins || {})) { + const dependency = this.getDefineByPluginKey(dependencyName, item); + if (!isPluginVersionCompatible(dependency, expectedRange)) { + throw new Error(`插件依赖版本冲突: ${item.name} 依赖 ${dependencyName}@${expectedRange},当前版本为 ${dependency.version || "未声明"}`); + } + visit(dependency); + } + }; + visit(current); + return resolved; + } + + private buildPluginDependencyKey(plugin: RuntimeDependencyPluginDefine) { + if (plugin.pluginType === "addon" && plugin.addonType) { + return `addon:${plugin.addonType}:${plugin.name}`; + } + const pluginType = plugin.pluginType === "deploy" ? "plugin" : plugin.pluginType || "unknown"; + return `${pluginType}:${plugin.name}`; + } + + private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine { + const parts = pluginKey.split(":"); + let pluginType: string, name: string, subtype: string | undefined; + if (parts.length === 2) { + [pluginType, name] = parts; + } else if (parts.length === 3) { + [pluginType, subtype, name] = parts; + } else { + const ownerName = owner?.name || pluginKey; + throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey}`); + } + if (!this.registriesMap) { + throw new Error("注册表未设置,请先调用 setRegistries"); + } + const target = this.registriesMap[pluginType]; + if (!target) { + const ownerName = owner?.name || pluginKey; + throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},未知插件类型 ${pluginType}`); + } + // addon 类型的 key 需要包含 subtype + const registryKey = pluginType === "addon" && subtype ? `${subtype}:${name}` : name; + const define = target.registry.getDefine(registryKey) as RegisteredDefineLike; + if (!define) { + throw new Error(`插件依赖缺失: ${owner?.name || pluginKey} 依赖 ${pluginKey},但该插件未注册或已禁用`); + } + return { ...define, key: pluginKey, pluginType: target.pluginType, addonType: target.addonType }; + } + + private async doEnsureInstalled(options: { dependencies: Record; logger?: ILogger }): Promise { + let { dependencies } = options; + const log = options.logger || defaultLogger; + return await this.withInstallLock(async () => { + const rootDir = this.getRuntimeDepsRootDir(); + const packageJsonPath = path.join(rootDir, "package.json"); + const lockPath = path.join(rootDir, "pnpm-lock.yaml"); + log.info(`第三方依赖安装: ${JSON.stringify(dependencies)}`); + dependencies = this.mergeInstalledDependencies(this.readManifestDependencies(packageJsonPath), dependencies); + const dependenciesHash = this.createDependenciesHash(dependencies); + const statePath = path.join(rootDir, "install-state.json"); + const currentState = this.readInstallState(statePath); + if (currentState?.dependenciesHash === dependenciesHash && fs.existsSync(path.join(rootDir, "node_modules"))) { + log.info("第三方依赖已安装"); + return { registryUrl: currentState.registryUrl || "", packageJsonPath }; + } + const manifest = { name: "certd-runtime-deps", private: true, type: "module", dependencies }; + fs.writeFileSync(packageJsonPath, JSON.stringify(manifest, null, 2), "utf8"); + const registryUrl = await this.registryResolver.resolve(); + const env = this.buildChildEnv(registryUrl); + const command = this.getPnpmCommand(); + const pnpmVersion = await this.getPnpmVersion(command, env); + const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--no-frozen-lockfile", "--reporter=append-only"]; + if (registryUrl) { + args.push(`--registry=${registryUrl}`); + } + log.info(`开始安装第三方依赖: ${Object.keys(dependencies).join(", ")}`); + const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env }); + if (result.code !== 0) { + const message = result.stderr || result.stdout || "unknown error"; + this.writeInstallState(statePath, { + ...currentState, + installedAt: currentState?.installedAt, + failedAt: new Date().toISOString(), + registryUrl, + dependenciesHash, + nodeVersion: process.version, + pnpmVersion, + lockFileExists: fs.existsSync(lockPath), + lastError: message, + }); + throw new Error(`动态依赖安装失败: ${message}`); + } + this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) }); + log.info("第三方依赖安装完成"); + return { registryUrl, packageJsonPath }; + }); + } + + private async withInstallLock(run: () => Promise): Promise { + const rootDir = this.getRuntimeDepsRootDir(); + fs.mkdirSync(rootDir, { recursive: true }); + const lockFile = path.join(rootDir, ".install.lock"); + const previous = PROCESS_LOCKS.get(lockFile); + if (previous) { + await previous.catch(() => undefined); + } + let releaseProcessLock!: () => void; + const current = new Promise(resolve => { + releaseProcessLock = resolve; + }); + PROCESS_LOCKS.set(lockFile, current); + let fd: number | undefined; + try { + fd = await this.acquireFileLock(lockFile); + return await run(); + } finally { + if (fd != null) { + fs.closeSync(fd); + try { + fs.rmSync(lockFile, { force: true }); + } catch { + try { + fs.rmSync(lockFile, { force: true }); + } catch {} + } + } + releaseProcessLock(); + if (PROCESS_LOCKS.get(lockFile) === current) { + PROCESS_LOCKS.delete(lockFile); + } + } + } + + private async acquireFileLock(lockFile: string) { + const deadline = Date.now() + this.installTimeoutMs; + while (true) { + try { + const fd = fs.openSync(lockFile, "wx"); + fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8"); + return fd; + } catch (error: any) { + if (error?.code !== "EEXIST") { + throw error; + } + if (Date.now() > deadline) { + throw new Error(`动态依赖安装锁等待超时: ${lockFile}`); + } + await this.waitForExternalLock(lockFile, deadline); + } + } + } + + private async waitForExternalLock(lockFile: string, deadline: number) { + while (fs.existsSync(lockFile)) { + if (Date.now() > deadline) { + throw new Error(`动态依赖安装锁等待超时: ${lockFile}`); + } + await new Promise(resolve => setTimeout(resolve, 300)); + } + } + + async clearRuntimeDeps() { + const rootDir = this.getRuntimeDepsRootDir(); + const normalizedRootDir = path.normalize(rootDir); + if (!normalizedRootDir.endsWith(path.normalize(".runtime-deps"))) { + throw new Error(`动态依赖目录配置异常,拒绝清理: ${rootDir}`); + } + await this.withInstallLock(async () => { + if (fs.existsSync(rootDir)) { + const entries = fs.readdirSync(rootDir); + for (const entry of entries) { + if (entry === ".install.lock") { + continue; + } + fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true }); + } + } + this.installPromises.clear(); + return undefined; + }); + } + + getMergedLazyDependencies(): Record { + return { ...this.lazyDependencies, ...this.pluginLazyDependencies }; + } + + collectPluginDeps(logger?: ILogger) { + if (!this.registriesMap) { + return; + } + const deps: Record = {}; + for (const { registry } of Object.values(this.registriesMap)) { + const defineList = registry.getDefineList(); + for (const define of defineList) { + const dependPackages = (define as any).dependPackages as Record | undefined; + if (!dependPackages) { + continue; + } + for (const [pkgName, range] of Object.entries(dependPackages)) { + const existing = deps[pkgName]; + if (existing && !areRangesCompatible(existing, range)) { + (logger || defaultLogger).warn?.(`懒加载依赖版本冲突: ${pkgName} => ${existing} vs ${range},保留已有版本`); + continue; + } + deps[pkgName] = range; + } + } + } + this.pluginLazyDependencies = deps; + (logger || defaultLogger).info(`从插件注册表收集到 ${Object.keys(deps).length} 个懒加载依赖`); + } + + refreshPluginDeps(logger?: ILogger) { + this.collectPluginDeps(logger); + } + + private readInstallState(statePath: string): any { + if (!fs.existsSync(statePath)) { + return null; + } + try { + return JSON.parse(fs.readFileSync(statePath, "utf8")); + } catch { + return null; + } + } + + private writeInstallState(statePath: string, state: any) { + fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8"); + } + + private readManifestDependencies(packageJsonPath: string): Record { + if (!fs.existsSync(packageJsonPath)) { + return {}; + } + try { + const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + return manifest.dependencies || {}; + } catch { + return {}; + } + } + + private mergeInstalledDependencies(installed: Record, requested: Record) { + const dependencies = { ...installed }; + for (const [packageName, range] of Object.entries(requested)) { + const installedRange = dependencies[packageName]; + if (installedRange && !areRangesCompatible(installedRange, range)) { + throw new Error(`动态依赖版本冲突: ${packageName} => installed:${installedRange}, requested:${range}`); + } + dependencies[packageName] = installedRange || range; + } + return dependencies; + } + + private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) { + const result = await this.commandRunner.run(command, ["--version"], { cwd: this.getRuntimeDepsRootDir(), timeoutMs: Math.min(this.installTimeoutMs, 10000), env }); + if (result.code !== 0) { + return ""; + } + return (result.stdout || result.stderr || "").trim(); + } + + private getPnpmCommand() { + return this.pnpmCommand || "pnpm"; + } + + private buildChildEnv(registryUrl: string) { + const env = { ...process.env }; + for (const key of ["NODE_OPTIONS", "VSCODE_INSPECTOR_OPTIONS", "NODE_INSPECTOR_PORT", "NODE_DEBUG"]) { + if (!env[key]) { + continue; + } + if (key === "NODE_OPTIONS") { + env[key] = this.stripDebugNodeOptions(env[key] as string); + } else { + delete env[key]; + } + } + if (registryUrl) { + env.npm_config_registry = registryUrl; + env.pnpm_config_registry = registryUrl; + } + env.CI = env.CI || "true"; + env.npm_config_confirm_modules_purge = "false"; + env.pnpm_config_confirm_modules_purge = "false"; + return env; + } + + private stripDebugNodeOptions(value: string) { + return value + .split(/\s+/) + .filter(Boolean) + .filter(item => !/^--inspect(-brk|-port)?(=|$)/.test(item)) + .filter(item => !/^--debug(=|$)/.test(item)) + .join(" "); + } + + getRuntimeDepsRootDir() { + return path.resolve(this.runtimeDepsRootDir); + } + + private createDependenciesHash(dependencies: Record) { + return crypto.createHash("sha256").update(JSON.stringify(dependencies)).digest("hex"); + } +} + +function isPluginVersionCompatible(plugin: RuntimeDependencyPluginDefine, expectedRange: string) { + if (!expectedRange || expectedRange === "*") { + return true; + } + if (!plugin.version) { + return false; + } + return areRangesCompatible(expectedRange, plugin.version); +} + +let runtimeDepsServiceInstance: RuntimeDepsService | null = null; + +export function initRuntimeDepsService(config: RuntimeDepsConfig, registries: any): RuntimeDepsService { + runtimeDepsServiceInstance = new RuntimeDepsService(config, registries); + return runtimeDepsServiceInstance; +} + +export function getRuntimeDepsService(): RuntimeDepsService { + if (!runtimeDepsServiceInstance) { + throw new Error("RuntimeDepsService 未初始化"); + } + return runtimeDepsServiceInstance!; +} + +export async function importRuntime(specifier: string, logger: ILogger = defaultLogger): Promise { + return getRuntimeDepsService().importRuntime(specifier, logger); +} diff --git a/packages/libs/lib-huawei/CHANGELOG.md b/packages/libs/lib-huawei/CHANGELOG.md index 83f932c7e..89efeba72 100644 --- a/packages/libs/lib-huawei/CHANGELOG.md +++ b/packages/libs/lib-huawei/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +**Note:** Version bump only for package @certd/lib-huawei + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) **Note:** Version bump only for package @certd/lib-huawei diff --git a/packages/libs/lib-huawei/package.json b/packages/libs/lib-huawei/package.json index 5ece06357..e29042ee6 100644 --- a/packages/libs/lib-huawei/package.json +++ b/packages/libs/lib-huawei/package.json @@ -1,7 +1,7 @@ { "name": "@certd/lib-huawei", "private": false, - "version": "1.42.4", + "version": "1.42.5", "main": "./dist/bundle.js", "module": "./dist/bundle.js", "types": "./dist/d/index.d.ts", @@ -26,9 +26,10 @@ "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "cross-env": "^7.0.3", + "eslint": "^8.57.0", "esmock": "^2.7.5", "prettier": "3.3.3", "tslib": "^2.8.1" }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/libs/lib-iframe/CHANGELOG.md b/packages/libs/lib-iframe/CHANGELOG.md index 31324ea78..28e35d779 100644 --- a/packages/libs/lib-iframe/CHANGELOG.md +++ b/packages/libs/lib-iframe/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +**Note:** Version bump only for package @certd/lib-iframe + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) **Note:** Version bump only for package @certd/lib-iframe diff --git a/packages/libs/lib-iframe/package.json b/packages/libs/lib-iframe/package.json index 1b033f740..c40afe542 100644 --- a/packages/libs/lib-iframe/package.json +++ b/packages/libs/lib-iframe/package.json @@ -1,7 +1,7 @@ { "name": "@certd/lib-iframe", "private": false, - "version": "1.42.4", + "version": "1.42.5", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", @@ -37,5 +37,5 @@ "tslib": "^2.8.1", "typescript": "^5.4.2" }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/libs/lib-jdcloud/CHANGELOG.md b/packages/libs/lib-jdcloud/CHANGELOG.md index f2fe238bf..dc18b8c78 100644 --- a/packages/libs/lib-jdcloud/CHANGELOG.md +++ b/packages/libs/lib-jdcloud/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +**Note:** Version bump only for package @certd/jdcloud + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) **Note:** Version bump only for package @certd/jdcloud diff --git a/packages/libs/lib-jdcloud/package.json b/packages/libs/lib-jdcloud/package.json index 12ecab32b..2f5d6ba3f 100644 --- a/packages/libs/lib-jdcloud/package.json +++ b/packages/libs/lib-jdcloud/package.json @@ -1,6 +1,6 @@ { "name": "@certd/jdcloud", - "version": "1.42.4", + "version": "1.42.5", "description": "jdcloud openApi sdk", "main": "./dist/bundle.js", "module": "./dist/bundle.js", @@ -35,6 +35,7 @@ "chai": "^5.1.0", "config": "^1.30.0", "cross-env": "^7.0.3", + "eslint": "^8.57.0", "esmock": "^2.7.5", "js-yaml": "^3.11.0", "mocha": "^10.6.0", @@ -62,5 +63,5 @@ "fetch" ] }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/libs/lib-k8s/CHANGELOG.md b/packages/libs/lib-k8s/CHANGELOG.md index 7fe0455e0..864a19912 100644 --- a/packages/libs/lib-k8s/CHANGELOG.md +++ b/packages/libs/lib-k8s/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +**Note:** Version bump only for package @certd/lib-k8s + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) **Note:** Version bump only for package @certd/lib-k8s diff --git a/packages/libs/lib-k8s/package.json b/packages/libs/lib-k8s/package.json index 884c0e94c..d003e926a 100644 --- a/packages/libs/lib-k8s/package.json +++ b/packages/libs/lib-k8s/package.json @@ -1,7 +1,7 @@ { "name": "@certd/lib-k8s", "private": false, - "version": "1.42.4", + "version": "1.42.5", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", @@ -21,7 +21,7 @@ "lint": "eslint --fix" }, "dependencies": { - "@certd/basic": "^1.42.4", + "@certd/basic": "^1.42.5", "@kubernetes/client-node": "0.21.0" }, "devDependencies": { @@ -38,5 +38,5 @@ "tslib": "^2.8.1", "typescript": "^5.4.2" }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/libs/lib-server/CHANGELOG.md b/packages/libs/lib-server/CHANGELOG.md index 8eacd7fc9..a67c6677c 100644 --- a/packages/libs/lib-server/CHANGELOG.md +++ b/packages/libs/lib-server/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +### Bug Fixes + +* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167)) + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) ### Bug Fixes diff --git a/packages/libs/lib-server/package.json b/packages/libs/lib-server/package.json index cf5d3a727..74cd4cfcf 100644 --- a/packages/libs/lib-server/package.json +++ b/packages/libs/lib-server/package.json @@ -1,6 +1,6 @@ { "name": "@certd/lib-server", - "version": "1.42.4", + "version": "1.42.5", "description": "midway with flyway, sql upgrade way ", "private": false, "type": "module", @@ -29,11 +29,11 @@ ], "license": "AGPL", "dependencies": { - "@certd/acme-client": "^1.42.4", - "@certd/basic": "^1.42.4", - "@certd/pipeline": "^1.42.4", - "@certd/plugin-lib": "^1.42.4", - "@certd/plus-core": "^1.42.4", + "@certd/acme-client": "^1.42.5", + "@certd/basic": "^1.42.5", + "@certd/pipeline": "^1.42.5", + "@certd/plugin-lib": "^1.42.5", + "@certd/plus-core": "^1.42.5", "@midwayjs/cache": "3.14.0", "@midwayjs/core": "3.20.11", "@midwayjs/i18n": "3.20.13", @@ -69,5 +69,5 @@ "typeorm": "^0.3.20", "typescript": "^5.4.2" }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/libs/lib-server/src/user/addon/api/api.ts b/packages/libs/lib-server/src/user/addon/api/api.ts index b5ccf937f..3c0200f09 100644 --- a/packages/libs/lib-server/src/user/addon/api/api.ts +++ b/packages/libs/lib-server/src/user/addon/api/api.ts @@ -1,15 +1,6 @@ import { HttpClient, ILogger, utils } from "@certd/basic"; -import {upperFirst} from "lodash-es"; -import { - accessRegistry, - FormItemProps, - IAccessService, - IRuntimeDepsService, - IServiceGetter, - PluginRequestHandleReq, - Registrable -} from "@certd/pipeline"; - +import { upperFirst } from "lodash-es"; +import { accessRegistry, FormItemProps, IAccessService, IServiceGetter, PluginRequestHandleReq, Registrable, getRuntimeDepsService } from "@certd/pipeline"; export type AddonRequestHandleReqInput = { id?: number; @@ -19,7 +10,7 @@ export type AddonRequestHandleReqInput = { export type AddonRequestHandleReq = { addonType: string; -} &PluginRequestHandleReq>; +} & PluginRequestHandleReq>; export type AddonInputDefine = FormItemProps & { title: string; @@ -48,8 +39,6 @@ export type AddonInstanceConfig = { }; }; - - export interface IAddon { ctx: AddonContext; [key: string]: any; @@ -67,13 +56,9 @@ export abstract class BaseAddon implements IAddon { ctx!: AddonContext; http!: HttpClient; logger!: ILogger; - runtimeDepsService?: IRuntimeDepsService; async importRuntime(specifier: string) { - if (!this.runtimeDepsService) { - return await import(specifier); - } - return await this.runtimeDepsService.importRuntime(specifier, this.logger); + return await getRuntimeDepsService().importRuntime(specifier, this.logger); } title!: string; @@ -85,7 +70,7 @@ export abstract class BaseAddon implements IAddon { if (accessId == null) { throw new Error("您还没有配置授权"); } - const accessService = await this.ctx.serviceGetter.get("accessService") + const accessService = await this.ctx.serviceGetter.get("accessService"); let res: any = null; if (isCommon) { res = await accessService.getCommonById(accessId); @@ -118,15 +103,12 @@ export abstract class BaseAddon implements IAddon { this.ctx = ctx; this.http = ctx.http; this.logger = ctx.logger; - if (!this.runtimeDepsService && this.ctx.serviceGetter) { - this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService"); - } } - setDefine = (define:AddonDefine) => { + setDefine = (define: AddonDefine) => { this.define = define; }; - async onRequest(req:AddonRequestHandleReq) { + async onRequest(req: AddonRequestHandleReq) { if (!req.action) { throw new Error("action is required"); } @@ -144,10 +126,8 @@ export abstract class BaseAddon implements IAddon { } throw new Error(`action ${req.action} not found`); } - } - export interface IAddonGetter { getById(id: any): Promise; getCommonById(id: any): Promise; diff --git a/packages/libs/midway-flyway-js/CHANGELOG.md b/packages/libs/midway-flyway-js/CHANGELOG.md index 4387d3279..2a2505c57 100644 --- a/packages/libs/midway-flyway-js/CHANGELOG.md +++ b/packages/libs/midway-flyway-js/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +### Performance Improvements + +* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3)) + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) **Note:** Version bump only for package @certd/midway-flyway-js diff --git a/packages/libs/midway-flyway-js/package.json b/packages/libs/midway-flyway-js/package.json index 625be1329..a44ceab29 100644 --- a/packages/libs/midway-flyway-js/package.json +++ b/packages/libs/midway-flyway-js/package.json @@ -1,6 +1,6 @@ { "name": "@certd/midway-flyway-js", - "version": "1.42.4", + "version": "1.42.5", "description": "midway with flyway, sql upgrade way ", "private": false, "type": "module", @@ -52,5 +52,5 @@ "typeorm": "^0.3.20", "typescript": "^5.4.2" }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/libs/midway-flyway-js/src/flyway.ts b/packages/libs/midway-flyway-js/src/flyway.ts index 6e0c0881e..569d9e5c5 100644 --- a/packages/libs/midway-flyway-js/src/flyway.ts +++ b/packages/libs/midway-flyway-js/src/flyway.ts @@ -1,8 +1,8 @@ -import * as path from 'path'; -import * as fs from 'fs'; -import { QueryRunner, Table } from 'typeorm'; -import { FlywayHistory } from './entity.js'; -import * as crypto from 'crypto'; +import * as path from "path"; +import * as fs from "fs"; +import { QueryRunner, Table } from "typeorm"; +import { FlywayHistory } from "./entity.js"; +import * as crypto from "crypto"; /** * 脚本文件信息 @@ -32,10 +32,10 @@ const DefaultLogger = { }, }; -let customLogger:any = null; -export function setFlywayLogger (logger: any) { +let customLogger: any = null; +export function setFlywayLogger(logger: any) { customLogger = logger; -}; +} export class Flyway { scriptDir; @@ -45,8 +45,8 @@ export class Flyway { connection; logger; constructor(opts: any) { - this.scriptDir = opts.scriptDir ?? 'db/migration'; - this.flywayTableName = opts.flywayTableName ?? 'flyway_history'; + this.scriptDir = opts.scriptDir ?? "db/migration"; + this.flywayTableName = opts.flywayTableName ?? "flyway_history"; this.baseline = opts.baseline ?? false; this.allowHashNotMatch = opts.allowHashNotMatch ?? false; this.logger = customLogger || opts.logger || DefaultLogger; @@ -54,9 +54,9 @@ export class Flyway { } async run(ignores?: (RegExp | string)[]) { - this.logger.info('[ midfly ] start-------------'); + this.logger.info("[ midfly ] start-------------"); if (!fs.existsSync(this.scriptDir)) { - this.logger.info('[ midfly ] scriptDir<' + this.scriptDir + '> not found'); + this.logger.info("[ midfly ] scriptDir<" + this.scriptDir + "> not found"); return; } @@ -77,7 +77,7 @@ export class Flyway { continue; } if (!file.isBaseline) { - this.logger.info('need exec script file: ', file.script); + this.logger.info("need exec script file: ", file.script); //执行sql文件 if (/\.sql$/.test(file.script)) { await this.execSql(filepath, queryRunner); @@ -87,7 +87,7 @@ export class Flyway { // await this.execJsOrTs(filepath, t); // } } else { - this.logger.info('baseline script file: ', file.script); + this.logger.info("baseline script file: ", file.script); } await this.storeSqlExecLog(file.script, filepath, true, queryRunner); await queryRunner.commitTransaction(); @@ -95,10 +95,15 @@ export class Flyway { this.logger.error(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-------------'); + this.logger.info("[ midfly ] end-------------"); } private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) { @@ -160,17 +165,17 @@ export class Flyway { name: this.flywayTableName, columns: [ { - name: 'id', + name: "id", type: this.connection.driver.normalizeType({ type: this.connection.driver.mappedDataTypes.migrationId, }), isGenerated: true, - generationStrategy: 'increment', + generationStrategy: "increment", isPrimary: true, isNullable: false, }, { - name: 'timestamp', + name: "timestamp", type: this.connection.driver.normalizeType({ type: this.connection.driver.mappedDataTypes.migrationTimestamp, }), @@ -178,23 +183,23 @@ export class Flyway { isNullable: false, }, { - name: 'name', + name: "name", type: this.connection.driver.normalizeType({ type: this.connection.driver.mappedDataTypes.migrationName, }), isNullable: false, }, { - name: 'hash', + name: "hash", type: this.connection.driver.normalizeType({ type: this.connection.driver.mappedDataTypes.migrationName, }), isNullable: true, }, { - name: 'success', + name: "success", type: this.connection.driver.normalizeType({ - type: 'boolean', + type: "boolean", }), isNullable: true, }, @@ -210,7 +215,7 @@ export class Flyway { } let ret = false; for (const ignore of ignores) { - if (typeof ignore === 'string' && file === ignore) { + if (typeof ignore === "string" && file === ignore) { ret = true; break; } @@ -233,20 +238,20 @@ export class Flyway { if (history.hash !== hash && this.allowHashNotMatch === false) { throw new Error(file + `hash conflict ,old: ${history.hash} != new: ${hash}`); } - this.logger.info('[ midfly ] script<' + file + '> already executed'); + this.logger.info("[ midfly ] script<" + file + "> already executed"); return true; } - this.logger.info('[ midfly ] script<' + file + '> not yet execute'); + this.logger.info("[ midfly ] script<" + file + "> not yet execute"); return false; } private async getFileHash(filepath: string) { const content = fs.readFileSync(filepath).toString(); - return crypto.createHash('md5').update(content.toString()).digest('hex'); + return crypto.createHash("md5").update(content.toString()).digest("hex"); } private async execSql(filepath: string, queryRunner: QueryRunner) { - this.logger.info('[ midfly ] exec ', filepath); + this.logger.info("[ midfly ] exec ", filepath); const content = fs.readFileSync(filepath).toString().trim(); const arr = this.splitSql2Array(content); for (const s of arr) { @@ -255,11 +260,11 @@ export class Flyway { } private async execOnePart(sql: string, queryRunner: QueryRunner) { - this.logger.debug('exec sql index: ', sql); + this.logger.debug("exec sql index: ", sql); try { await queryRunner.query(sql); } catch (err: any) { - this.logger.error('exec sql error : ', err.message, err); + this.logger.error("exec sql error : ", err.message, err); throw err; } } @@ -275,11 +280,11 @@ export class Flyway { const temp = String(str).trim(); - if (temp === 'null') { + if (temp === "null") { return []; } - const semicolon = ';'; + const semicolon = ";"; const deepChars = ['"', "'"]; const splits = []; @@ -289,7 +294,7 @@ export class Flyway { if (deepChars.indexOf(charAt) >= 0) { //如果是深度char - if (i !== 0 && temp.charAt(i - 1) === '\\') { + if (i !== 0 && temp.charAt(i - 1) === "\\") { //如果前一个是转义字符,忽略它 } else { //说明需要进出深度了 diff --git a/packages/plugins/plugin-cert/CHANGELOG.md b/packages/plugins/plugin-cert/CHANGELOG.md index 883cd55a5..035afaaa3 100644 --- a/packages/plugins/plugin-cert/CHANGELOG.md +++ b/packages/plugins/plugin-cert/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +**Note:** Version bump only for package @certd/plugin-cert + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) **Note:** Version bump only for package @certd/plugin-cert diff --git a/packages/plugins/plugin-cert/package.json b/packages/plugins/plugin-cert/package.json index d8e14ad92..e93247fab 100644 --- a/packages/plugins/plugin-cert/package.json +++ b/packages/plugins/plugin-cert/package.json @@ -1,7 +1,7 @@ { "name": "@certd/plugin-cert", "private": false, - "version": "1.42.4", + "version": "1.42.5", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -20,7 +20,7 @@ "lint": "eslint --fix" }, "dependencies": { - "@certd/plugin-lib": "^1.42.4" + "@certd/plugin-lib": "^1.42.5" }, "devDependencies": { "@types/chai": "^4.3.12", @@ -38,5 +38,5 @@ "tslib": "^2.8.1", "typescript": "^5.4.2" }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/plugins/plugin-lib/CHANGELOG.md b/packages/plugins/plugin-lib/CHANGELOG.md index cc90f9688..6f5bbe21e 100644 --- a/packages/plugins/plugin-lib/CHANGELOG.md +++ b/packages/plugins/plugin-lib/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +### Bug Fixes + +* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167)) + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) ### Bug Fixes diff --git a/packages/plugins/plugin-lib/package.json b/packages/plugins/plugin-lib/package.json index a01889b31..76b662484 100644 --- a/packages/plugins/plugin-lib/package.json +++ b/packages/plugins/plugin-lib/package.json @@ -1,7 +1,7 @@ { "name": "@certd/plugin-lib", "private": false, - "version": "1.42.4", + "version": "1.42.5", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -17,9 +17,9 @@ "lint": "eslint --fix" }, "dependencies": { - "@certd/acme-client": "^1.42.4", - "@certd/basic": "^1.42.4", - "@certd/pipeline": "^1.42.4", + "@certd/acme-client": "^1.42.5", + "@certd/basic": "^1.42.5", + "@certd/pipeline": "^1.42.5", "dayjs": "^1.11.7", "jszip": "^3.10.1", "lodash-es": "^4.17.21", @@ -45,5 +45,5 @@ "tslib": "^2.8.1", "typescript": "^5.4.2" }, - "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7" + "gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0" } diff --git a/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts b/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts index 45e1c2933..ebf928102 100644 --- a/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts +++ b/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts @@ -1,5 +1,5 @@ import { HttpClient, ILogger } from "@certd/basic"; -import { IAccessService, IRuntimeDepsService, PageRes, PageSearch } from "@certd/pipeline"; +import { IAccessService, PageRes, PageSearch, getRuntimeDepsService } from "@certd/pipeline"; import punycode from "punycode.js"; import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DnsResolveRecord, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js"; import { dnsProviderRegistry } from "./registry.js"; @@ -7,13 +7,9 @@ export abstract class AbstractDnsProvider implements IDnsProvider { ctx!: DnsProviderContext; http!: HttpClient; logger!: ILogger; - runtimeDepsService?: IRuntimeDepsService; async importRuntime(specifier: string) { - if (!this.runtimeDepsService) { - throw new Error("runtimeDepsService 未初始化"); - } - return await this.runtimeDepsService.importRuntime(specifier, this.logger); + return await getRuntimeDepsService().importRuntime(specifier, this.logger); } usePunyCode(): boolean { @@ -42,9 +38,6 @@ export abstract class AbstractDnsProvider implements IDnsProvider { this.ctx = ctx; this.logger = ctx.logger; this.http = ctx.http; - if (!this.runtimeDepsService && this.ctx.serviceGetter) { - this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService"); - } } async parseDomain(fullDomain: string) { diff --git a/packages/ui/certd-client/CHANGELOG.md b/packages/ui/certd-client/CHANGELOG.md index 55cf22b64..97aa3dfc1 100644 --- a/packages/ui/certd-client/CHANGELOG.md +++ b/packages/ui/certd-client/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +### Bug Fixes + +* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167)) + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) **Note:** Version bump only for package @certd/ui-client diff --git a/packages/ui/certd-client/package.json b/packages/ui/certd-client/package.json index 15a72f3d5..05d2cd75f 100644 --- a/packages/ui/certd-client/package.json +++ b/packages/ui/certd-client/package.json @@ -1,6 +1,6 @@ { "name": "@certd/ui-client", - "version": "1.42.4", + "version": "1.42.5", "private": true, "scripts": { "dev": "vite --open", @@ -59,7 +59,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "core-js": "^3.36.0", - "cos-js-sdk-v5": "^1.7.0", "cron-parser": "^4.9.0", "cropperjs": "^1.6.1", "cssnano": "^7.0.6", @@ -105,8 +104,8 @@ "zod-defaults": "^0.1.3" }, "devDependencies": { - "@certd/lib-iframe": "^1.42.4", - "@certd/pipeline": "^1.42.4", + "@certd/lib-iframe": "^1.42.5", + "@certd/pipeline": "^1.42.5", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@types/chai": "^4.3.12", diff --git a/packages/ui/certd-server/.eslintrc b/packages/ui/certd-server/.eslintrc deleted file mode 100644 index 7ddaf032e..000000000 --- a/packages/ui/certd-server/.eslintrc +++ /dev/null @@ -1,26 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "ignorePatterns": ["dist"], - "extends": [ - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - "prettier" - ], - "env": { - "mocha": true - }, - "rules": { - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/ban-ts-ignore": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-this-alias": "off", - // 允许any - "@typescript-eslint/no-unsafe-anyassignment": "off" - } -} diff --git a/packages/ui/certd-server/CHANGELOG.md b/packages/ui/certd-server/CHANGELOG.md index a3c9b79a1..3feb25809 100644 --- a/packages/ui/certd-server/CHANGELOG.md +++ b/packages/ui/certd-server/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15) + +### Bug Fixes + +* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167)) +* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b)) + +### Performance Improvements + +* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461)) + ## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11) ### Bug Fixes diff --git a/packages/ui/certd-server/package.json b/packages/ui/certd-server/package.json index a9bc7b9fb..0389ecdce 100644 --- a/packages/ui/certd-server/package.json +++ b/packages/ui/certd-server/package.json @@ -1,6 +1,6 @@ { "name": "@certd/ui-server", - "version": "1.42.4", + "version": "1.42.5", "description": "fast-server base midway", "private": true, "type": "module", @@ -25,6 +25,7 @@ "format": "prettier --write src", "lint": "mwts fix", "ci": "pnpm run cov", + "lint2": "cross-env NODE_ENV=production mwtsc -p tsconfig.build.json", "build-only": "cross-env NODE_ENV=production mwtsc -p tsconfig.build.json --cleanOutDir --skipLibCheck", "build": "pnpm run build-only && pnpm run export-metadata", "export-metadata": "node export-plugin-yaml.js", @@ -41,20 +42,20 @@ "lint1": "eslint --fix" }, "dependencies": { - "@certd/acme-client": "^1.42.4", - "@certd/basic": "^1.42.4", - "@certd/commercial-core": "^1.42.4", + "@certd/acme-client": "^1.42.5", + "@certd/basic": "^1.42.5", + "@certd/commercial-core": "^1.42.5", "@certd/cv4pve-api-javascript": "^8.4.2", - "@certd/jdcloud": "^1.42.4", - "@certd/lib-huawei": "^1.42.4", - "@certd/lib-k8s": "^1.42.4", - "@certd/lib-server": "^1.42.4", - "@certd/midway-flyway-js": "^1.42.4", - "@certd/pipeline": "^1.42.4", - "@certd/plugin-cert": "^1.42.4", - "@certd/plugin-lib": "^1.42.4", - "@certd/plugin-plus": "^1.42.4", - "@certd/plus-core": "^1.42.4", + "@certd/jdcloud": "^1.42.5", + "@certd/lib-huawei": "^1.42.5", + "@certd/lib-k8s": "^1.42.5", + "@certd/lib-server": "^1.42.5", + "@certd/midway-flyway-js": "^1.42.5", + "@certd/pipeline": "^1.42.5", + "@certd/plugin-cert": "^1.42.5", + "@certd/plugin-lib": "^1.42.5", + "@certd/plugin-plus": "^1.42.5", + "@certd/plus-core": "^1.42.5", "@koa/cors": "^5.0.0", "@midwayjs/bootstrap": "3.20.11", "@midwayjs/cache": "3.14.0", @@ -125,6 +126,7 @@ "@types/nodemailer": "^6.4.8", "c8": "^10.1.2", "cross-env": "^7.0.3", + "eslint": "^7.32.0", "esmock": "^2.7.5", "mocha": "^10.6.0", "mwts": "^1.3.0", diff --git a/packages/ui/certd-server/src/controller/sys/settings/sys-settings-controller.ts b/packages/ui/certd-server/src/controller/sys/settings/sys-settings-controller.ts index b6e65df35..06d36cffb 100644 --- a/packages/ui/certd-server/src/controller/sys/settings/sys-settings-controller.ts +++ b/packages/ui/certd-server/src/controller/sys/settings/sys-settings-controller.ts @@ -1,3 +1,4 @@ +import { getRuntimeDepsService } from "@certd/pipeline"; import { ALL, Body, Controller, Inject, Post, Provide, Query, RequestIP } from "@midwayjs/core"; import { addonRegistry, AddonService, CrudController, SysPrivateSettings, SysPublicSettings, SysSafeSetting, SysSettingsEntity, SysSettingsService } from "@certd/lib-server"; import { cloneDeep, merge } from "lodash-es"; @@ -7,7 +8,6 @@ import { getEmailSettings } from "../../../modules/sys/settings/fix.js"; import { http, logger, utils } from "@certd/basic"; import { CodeService } from "../../../modules/basic/service/code-service.js"; import { SmsServiceFactory } from "../../../modules/basic/sms/factory.js"; -import { RuntimeDepsService } from "../../../modules/runtime-deps/runtime-deps-service.js"; import { AuditType } from "../../../modules/sys/enterprise/service/audit-constants.js"; /** @@ -25,9 +25,6 @@ export class SysSettingsController extends CrudController { codeService: CodeService; @Inject() addonService: AddonService; - @Inject() - runtimeDepsService: RuntimeDepsService; - getService() { return this.service; } @@ -240,7 +237,7 @@ export class SysSettingsController extends CrudController { @Post("/clearRuntimeDeps", { description: "sys:settings:edit", summary: "清除运行时依赖" }) async clearRuntimeDeps() { - await this.runtimeDepsService.clearRuntimeDeps(); + await getRuntimeDepsService().clearRuntimeDeps(); await this.auditLog({ content: "清除了运行时依赖" }); return this.ok(true); } diff --git a/packages/ui/certd-server/src/modules/auto/auto-a-register.ts b/packages/ui/certd-server/src/modules/auto/auto-a-register.ts index 315822905..d92ecf91f 100644 --- a/packages/ui/certd-server/src/modules/auto/auto-a-register.ts +++ b/packages/ui/certd-server/src/modules/auto/auto-a-register.ts @@ -1,11 +1,11 @@ -import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core"; +import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core"; +import { AutoCron } from "./auto-cron.js"; import { AutoInitSite } from "./auto-init-site.js"; import { AutoLoadPlugins } from "./auto-load-plugins.js"; -import { AutoCron } from "./auto-cron.js"; import { AutoMitterRegister } from "./auto-mitter-register.js"; import { AutoPipelineEmitterRegister } from "./auto-pipeline-emitter-register.js"; -import { AutoFix } from "./fix/auto-fix.js"; import { AutoPrint } from "./auto-print.js"; +import { AutoFix } from "./fix/auto-fix.js"; @Autoload() @Scope(ScopeEnum.Request, { allowDowngrade: true }) diff --git a/packages/ui/certd-server/src/modules/auto/auto-load-plugins.ts b/packages/ui/certd-server/src/modules/auto/auto-load-plugins.ts index 3b9ed1b84..de99e848f 100644 --- a/packages/ui/certd-server/src/modules/auto/auto-load-plugins.ts +++ b/packages/ui/certd-server/src/modules/auto/auto-load-plugins.ts @@ -1,8 +1,11 @@ -import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { logger } from "@certd/basic"; import { PluginService } from "../plugin/service/plugin-service.js"; import { registerPaymentProviders } from "../suite/payments/index.js"; -import { RuntimeDepsService } from "../runtime-deps/runtime-deps-service.js"; +import { getRuntimeDepsService, initRuntimeDepsService } from "@certd/pipeline"; +import { pluginRegistry, accessRegistry, notificationRegistry } from "@certd/pipeline"; +import { dnsProviderRegistry } from "@certd/plugin-lib"; +import { addonRegistry } from "@certd/lib-server"; @Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) @@ -10,8 +13,8 @@ export class AutoLoadPlugins { @Inject() pluginService: PluginService; - @Inject() - runtimeDepsService: RuntimeDepsService; + @Config("runtimeDeps") + runtimeDepsConfig: any; async init() { logger.info(`加载插件开始,加载模式:${process.env.certd_plugin_loadmode}`); @@ -35,7 +38,16 @@ export class AutoLoadPlugins { await registerPaymentProviders(); logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`); + //初始化第三方依赖服务 + initRuntimeDepsService(this.runtimeDepsConfig, { + pluginRegistry, + accessRegistry, + notificationRegistry, + dnsProviderRegistry, + addonRegistry, + }); // 收集插件 dependPackages 并安装 - await this.runtimeDepsService.refreshPluginDeps(); + const service = getRuntimeDepsService(); + service.refreshPluginDeps(); } } diff --git a/packages/ui/certd-server/src/modules/basic/service/code-service.ts b/packages/ui/certd-server/src/modules/basic/service/code-service.ts index 76b2486ae..f24177afa 100644 --- a/packages/ui/certd-server/src/modules/basic/service/code-service.ts +++ b/packages/ui/certd-server/src/modules/basic/service/code-service.ts @@ -1,4 +1,4 @@ -import { cache, isDev, randomNumber, simpleNanoId } from "@certd/basic"; +import { cache, isDev, randomNumber, simpleNanoId } from "@certd/basic"; import { AccessService, AccessSysGetter, CodeErrorException, SysSettingsService } from "@certd/lib-server"; import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { ISmsService } from "../sms/api.js"; @@ -6,7 +6,6 @@ import { SmsServiceFactory } from "../sms/factory.js"; import { CaptchaService } from "./captcha-service.js"; import { EmailService } from "./email-service.js"; import { CaptchaRequest } from "../../../plugins/plugin-captcha/api.js"; -import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js"; // {data: '', text: 'abcd'} /** @@ -25,9 +24,6 @@ export class CodeService { @Inject() captchaService: CaptchaService; - @Inject() - runtimeDepsService: RuntimeDepsService; - async checkCaptcha(body: any, req: CaptchaRequest) { return await this.captchaService.doValidate({ form: body, req }); } @@ -60,7 +56,6 @@ export class CodeService { await sender.setCtx({ accessService: accessGetter, config: smsConfig, - runtimeDepsService: this.runtimeDepsService, }); const smsCode = randomNumber(verificationCodeLength); await sender.sendSmsCode({ diff --git a/packages/ui/certd-server/src/modules/basic/sms/api.ts b/packages/ui/certd-server/src/modules/basic/sms/api.ts index 930c665e6..1dde3e055 100644 --- a/packages/ui/certd-server/src/modules/basic/sms/api.ts +++ b/packages/ui/certd-server/src/modules/basic/sms/api.ts @@ -1,9 +1,8 @@ -import { FormItemProps, IAccessService } from "@certd/pipeline"; -import type { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js"; +import { FormItemProps, IAccessService } from "@certd/pipeline"; export interface ISmsService { sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }): Promise; - setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any }; runtimeDepsService?: RuntimeDepsService }): Promise; + setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any } }): Promise; } export type PluginInputs = { @@ -13,5 +12,4 @@ export type PluginInputs = { export type SmsPluginCtx = { accessService: IAccessService; config: T; - runtimeDepsService?: RuntimeDepsService; }; diff --git a/packages/ui/certd-server/src/modules/basic/sms/tencent-sms.ts b/packages/ui/certd-server/src/modules/basic/sms/tencent-sms.ts index fba25b27f..71080b15b 100644 --- a/packages/ui/certd-server/src/modules/basic/sms/tencent-sms.ts +++ b/packages/ui/certd-server/src/modules/basic/sms/tencent-sms.ts @@ -1,5 +1,6 @@ -import { TencentAccess } from "../../../plugins/plugin-lib/tencent/access.js"; -import { ISmsService, PluginInputs, SmsPluginCtx } from "./api.js"; +import { TencentAccess } from "../../../plugins/plugin-lib/tencent/access.js"; +import { importRuntime } from "@certd/pipeline"; +import { ISmsService, PluginInputs } from "./api.js"; export type TencentSmsConfig = { accessId: string; @@ -66,26 +67,16 @@ export class TencentSmsService implements ISmsService { }; } - ctx: SmsPluginCtx; + ctx: { accessService: any; config: TencentSmsConfig }; async setCtx(ctx: any) { this.ctx = ctx; - if (this.ctx.runtimeDepsService) { - await this.ctx.runtimeDepsService.ensureDependencies({ - dependencies: { - "tencentcloud-sdk-nodejs": "^4.1.112", - }, - }); - } } async getClient() { - if (!this.ctx.runtimeDepsService) { - throw new Error("动态依赖服务未初始化,无法加载腾讯云短信SDK"); - } - const sdk = await this.ctx.runtimeDepsService.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js"); + const sdk = await importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js"); const client = sdk.v20210111.Client; - const access = await this.ctx.accessService.getById(this.ctx.config.accessId); + const access: TencentAccess = await this.ctx.accessService.getById(this.ctx.config.accessId); // const region = this.region; const clientConfig = { diff --git a/packages/ui/certd-server/src/modules/mine/service/two-factor-service.ts b/packages/ui/certd-server/src/modules/mine/service/two-factor-service.ts index 8075afc62..040017ae1 100644 --- a/packages/ui/certd-server/src/modules/mine/service/two-factor-service.ts +++ b/packages/ui/certd-server/src/modules/mine/service/two-factor-service.ts @@ -1,8 +1,8 @@ +import { importRuntime } from "@certd/pipeline"; import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { UserSettingsService } from "./user-settings-service.js"; import { UserTwoFactorSetting } from "./models.js"; import { UserService } from "../../sys/authority/service/user-service.js"; -import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js"; /** * 授权 @@ -14,15 +14,12 @@ export class TwoFactorService { userSettingsService: UserSettingsService; @Inject() userService: UserService; - @Inject() - runtimeDepsService: RuntimeDepsService; - async getAuthenticatorQrCode(userId: any) { const setting = await this.getSetting(userId); const authenticatorSetting = setting.authenticator; if (!authenticatorSetting.secret) { - const { authenticator } = await this.runtimeDepsService.importRuntime("otplib"); + const { authenticator } = await importRuntime("otplib"); authenticatorSetting.secret = authenticator.generateSecret(); await this.userSettingsService.saveSetting(userId, null, setting); @@ -41,7 +38,7 @@ export class TwoFactorService { async saveAuthenticator(req: { userId: any; verifyCode: any }) { const userId = req.userId; - const { authenticator } = await this.runtimeDepsService.importRuntime("otplib"); + const { authenticator } = await importRuntime("otplib"); const setting = await this.getSetting(userId); const authenticatorSetting = setting.authenticator; @@ -80,7 +77,7 @@ export class TwoFactorService { } async verifyAuthenticatorCode(userId: any, verifyCode: string) { - const { authenticator } = await this.runtimeDepsService.importRuntime("otplib"); + const { authenticator } = await importRuntime("otplib"); const setting = await this.getSetting(userId); if (!setting.authenticator.enabled) { throw new Error("authenticator 未开启"); diff --git a/packages/ui/certd-server/src/modules/pipeline/service/getter/task-service-getter.ts b/packages/ui/certd-server/src/modules/pipeline/service/getter/task-service-getter.ts index 2f60acb82..5e144d2e8 100644 --- a/packages/ui/certd-server/src/modules/pipeline/service/getter/task-service-getter.ts +++ b/packages/ui/certd-server/src/modules/pipeline/service/getter/task-service-getter.ts @@ -1,4 +1,4 @@ -import { IServiceGetter } from "@certd/pipeline"; +import { IServiceGetter } from "@certd/pipeline"; import { ApplicationContext, IMidwayContainer, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { AccessGetter, AccessService } from "@certd/lib-server"; import { CnameProxyService } from "./cname-proxy-service.js"; @@ -13,7 +13,6 @@ import { CertInfoGetter } from "./cert-info-getter.js"; import { CertInfoService } from "../../../monitor/index.js"; import { ICertInfoGetter } from "@certd/plugin-lib"; import { CnameProviderService } from "../../../cname/service/cname-provider-service.js"; -import { RuntimeDepsService } from "../../../runtime-deps/runtime-deps-service.js"; const serviceNames = ["ocrService"]; export class TaskServiceGetter implements IServiceGetter { @@ -39,8 +38,6 @@ export class TaskServiceGetter implements IServiceGetter { return (await this.getDomainVerifierGetter()) as T; } else if (serviceName === "certInfoGetter") { return (await this.getCertInfoGetter()) as T; - } else if (serviceName === "runtimeDepsService") { - return (await this.getRuntimeDepsService()) as T; } else { if (!serviceNames.includes(serviceName)) { throw new Error(`${serviceName} not in whitelist`); @@ -84,10 +81,6 @@ export class TaskServiceGetter implements IServiceGetter { const domainService: DomainService = await this.appCtx.getAsync("domainService"); return new DomainVerifierGetter(this.userId, this.projectId, domainService); } - - async getRuntimeDepsService(): Promise { - return await this.appCtx.getAsync("runtimeDepsService"); - } } @Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) diff --git a/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts b/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts index b7a2f4676..ca9dea670 100644 --- a/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts +++ b/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts @@ -1,4 +1,4 @@ -import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { addonRegistry, BaseService, PageReq } from "@certd/lib-server"; import { PluginEntity } from "../entity/plugin.js"; import { InjectEntityModel } from "@midwayjs/typeorm"; @@ -6,14 +6,13 @@ import { IsNull, Not, Repository } from "typeorm"; import { isComm } from "@certd/plus-core"; import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js"; import { merge } from "lodash-es"; -import { accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline"; import { dnsProviderRegistry } from "@certd/plugin-cert"; import { logger } from "@certd/basic"; import yaml from "js-yaml"; import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js"; import fs from "fs"; import path from "path"; -import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js"; +import { importRuntime as importRuntimeDirect, getRuntimeDepsService, pluginRegistry, accessRegistry, notificationRegistry } from "@certd/pipeline"; export type PluginImportReq = { content: string; @@ -49,9 +48,6 @@ export class PluginService extends BaseService { @Inject() builtInPluginService: BuiltInPluginService; - @Inject() - runtimeDepsService: RuntimeDepsService; - //@ts-ignore getRepository() { return this.repository; @@ -270,7 +266,7 @@ export class PluginService extends BaseService { return; } await this.registerPlugin(item); - await this.runtimeDepsService.refreshPluginDeps(); + await this.refreshPluginDeps(); } async unRegisterById(id: any) { @@ -298,7 +294,12 @@ export class PluginService extends BaseService { } else { logger.warn(`不支持的插件类型:${item.pluginType}`); } - await this.runtimeDepsService.refreshPluginDeps(); + await this.refreshPluginDeps(); + } + + async refreshPluginDeps() { + const service = getRuntimeDepsService(); + service.refreshPluginDeps(); } async update(param: any) { @@ -334,7 +335,7 @@ export class PluginService extends BaseService { if (!isBareModuleSpecifier(modulePath)) { return await importLocalModule(modulePath); } - return await this.runtimeDepsService.importRuntime(modulePath, logger); + return await importRuntimeDirect(modulePath, logger); } private async getPluginClassFromFile(item: any) { diff --git a/packages/ui/certd-server/src/modules/runtime-deps/npm-registry-resolver.test.ts b/packages/ui/certd-server/src/modules/runtime-deps/npm-registry-resolver.test.ts deleted file mode 100644 index 47c9b88b1..000000000 --- a/packages/ui/certd-server/src/modules/runtime-deps/npm-registry-resolver.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import assert from "assert"; -import { NpmRegistryResolver } from "./npm-registry-resolver.js"; - -describe("NpmRegistryResolver", () => { - it("chooses the fastest successful registry in auto mode", async () => { - const resolver = new NpmRegistryResolver(); - resolver.config = { - mode: "auto", - fixedUrl: "", - candidates: ["https://slow.example.com", "https://fast.example.com"], - probeTimeoutMs: 100, - cacheTtlMs: 1000, - }; - resolver.probe = async registryUrl => { - return { - registryUrl, - ok: true, - elapsedMs: registryUrl.includes("fast") ? 10 : 50, - }; - }; - - const result = await resolver.resolve(); - - assert.equal(result, "https://fast.example.com"); - }); - - it("uses fixed registry without probing", async () => { - const resolver = new NpmRegistryResolver(); - resolver.config = { - mode: "fixed", - fixedUrl: "https://registry.example.com", - candidates: [], - probeTimeoutMs: 100, - cacheTtlMs: 1000, - }; - - const result = await resolver.resolve(); - - assert.equal(result, "https://registry.example.com"); - }); -}); diff --git a/packages/ui/certd-server/src/modules/runtime-deps/npm-registry-resolver.ts b/packages/ui/certd-server/src/modules/runtime-deps/npm-registry-resolver.ts deleted file mode 100644 index 3247cfdad..000000000 --- a/packages/ui/certd-server/src/modules/runtime-deps/npm-registry-resolver.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Config, Provide, Scope, ScopeEnum } from "@midwayjs/core"; - -export type NpmRegistryResolverConfig = { - mode: "auto" | "fixed" | "system"; - fixedUrl: string; - candidates: string[]; - probeTimeoutMs: number; - cacheTtlMs: number; -}; - -export type RegistryProbeResult = { - registryUrl: string; - ok: boolean; - elapsedMs: number; -}; - -@Provide() -@Scope(ScopeEnum.Singleton) -export class NpmRegistryResolver { - @Config("runtimeDeps.registry") - config!: NpmRegistryResolverConfig; - - private cache?: { registryUrl: string; expiresAt: number }; - - async resolve(): Promise { - const config = this.config; - if (config?.mode === "fixed" && config.fixedUrl) { - return config.fixedUrl; - } - if (config?.mode === "system") { - return ""; - } - - const cached = this.cache; - if (cached && cached.expiresAt > Date.now()) { - return cached.registryUrl; - } - - const candidates = (config?.candidates || []).filter(Boolean); - const probes = await Promise.allSettled(candidates.map(registryUrl => this.probe(registryUrl))); - const okList = probes.map(item => (item.status === "fulfilled" ? item.value : null)).filter((item): item is RegistryProbeResult => !!item && item.ok); - - if (okList.length > 0) { - okList.sort((a, b) => a.elapsedMs - b.elapsedMs); - const best = okList[0].registryUrl; - this.cache = { - registryUrl: best, - expiresAt: Date.now() + (config?.cacheTtlMs || 0), - }; - return best; - } - - return ""; - } - - async probe(registryUrl: string): Promise { - const timeoutMs = this.config?.probeTimeoutMs || 3000; - const started = Date.now(); - try { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeoutMs); - try { - const res = await fetch(`${registryUrl.replace(/\/$/, "")}/-/ping`, { signal: controller.signal }); - return { - registryUrl, - ok: res.ok, - elapsedMs: Date.now() - started, - }; - } finally { - clearTimeout(timer); - } - } catch { - return { - registryUrl, - ok: false, - elapsedMs: Date.now() - started, - }; - } - } -} diff --git a/packages/ui/certd-server/src/modules/runtime-deps/runtime-deps-service.ts b/packages/ui/certd-server/src/modules/runtime-deps/runtime-deps-service.ts deleted file mode 100644 index d6c7ef39b..000000000 --- a/packages/ui/certd-server/src/modules/runtime-deps/runtime-deps-service.ts +++ /dev/null @@ -1,733 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { spawn } from "child_process"; -import crypto from "crypto"; -import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; -import { createRequire } from "module"; -import { pathToFileURL } from "url"; -import { NpmRegistryResolver } from "./npm-registry-resolver.js"; -import { Registry, accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline"; -import { dnsProviderRegistry } from "@certd/plugin-lib"; -import { addonRegistry } from "@certd/lib-server"; -import { logger, ILogger } from "@certd/basic"; - -export type RuntimeDependencyPluginDefine = { - name: string; - key?: string; - title?: string; - version?: string; - pluginType?: string; - addonType?: string; - dependPlugins?: Record; - dependPackages?: Record; -}; - -type RegisteredDefineLike = RuntimeDependencyPluginDefine & { - key?: string; - pluginType?: string; - addonType?: string; - dependPlugins?: Record; - dependPackages?: Record; -}; - -function normalizeRange(range: string) { - return range.trim().replace(/^\^/, "").replace(/^~?/, ""); -} - -function areRangesCompatible(a: string, b: string) { - if (!a || !b) { - return true; - } - if (a === "*" || b === "*") { - return true; - } - const left = normalizeRange(a).split("."); - const right = normalizeRange(b).split("."); - return left[0] === right[0]; -} - -type DependencyConflict = { - packageName: string; - ranges: Array<{ pluginName: string; range: string }>; -}; - -type CollectDependenciesResult = { - dependencies: Record; - conflicts: DependencyConflict[]; -}; - -type InstallResult = { - registryUrl: string; - packageJsonPath: string; -}; - -type RuntimeImportResolveResult = { - resolved: string; - packageName: string; -}; - -type CommandRunnerResult = { - stdout: string; - stderr: string; - code: number; -}; - -type CommandRunner = { - // @ts-ignore - run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise; -}; - -const PROCESS_LOCKS = new Map>(); - -class DefaultCommandRunner implements CommandRunner { - // @ts-ignore - async run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise { - return await new Promise(resolve => { - let stdout = ""; - let stderr = ""; - let settled = false; - const child = spawn(command, args, { - cwd: options.cwd, - env: options.env, - windowsHide: true, - // @ts-ignore - shell: process.platform === "win32", - }); - - const timer = setTimeout(() => { - if (settled) { - return; - } - settled = true; - child.kill("SIGTERM"); - resolve({ stdout, stderr: stderr || `command timeout after ${options.timeoutMs}ms`, code: 1 }); - }, options.timeoutMs); - - child.stdout?.on("data", chunk => { - stdout += chunk.toString(); - }); - child.stderr?.on("data", chunk => { - stderr += chunk.toString(); - }); - child.on("error", error => { - if (settled) { - return; - } - settled = true; - clearTimeout(timer); - resolve({ stdout, stderr: error.message, code: 1 }); - }); - child.on("close", code => { - if (settled) { - return; - } - settled = true; - clearTimeout(timer); - resolve({ stdout, stderr, code: code || 0 }); - }); - }); - } -} - -@Provide() -@Scope(ScopeEnum.Singleton) -export class RuntimeDepsService { - @Config("runtimeDeps.rootDir") - runtimeDepsRootDir = "./data/.runtime-deps"; - - @Config("runtimeDeps.autoInstall") - autoInstall = true; - - @Config("runtimeDeps.enabled") - enabled = true; - - @Config("runtimeDeps.installTimeoutMs") - installTimeoutMs = 120000; - - @Config("runtimeDeps.pnpmCommand") - pnpmCommand = ""; - - @Config("runtimeDeps.lazyDependencies") - lazyDependencies: Record = {}; - - /** - * 从插件 registry 收集到的懒加载依赖,键为包名,值为版本范围 - */ - pluginLazyDependencies: Record = {}; - - @Inject() - registryResolver!: NpmRegistryResolver; - - commandRunner: CommandRunner = new DefaultCommandRunner(); - - private installPromises = new Map>(); - - collectDependencies(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult { - const merged: Record = {}; - const seen: Record> = {}; - - for (const plugin of plugins) { - const deps = plugin.dependPackages || {}; - for (const [packageName, range] of Object.entries(deps)) { - seen[packageName] ||= []; - seen[packageName].push({ pluginName: plugin.name, range }); - } - } - - const conflicts: DependencyConflict[] = []; - for (const [packageName, ranges] of Object.entries(seen)) { - const first = ranges[0]?.range; - if (!first) { - continue; - } - const conflict = ranges.some(item => !areRangesCompatible(first, item.range)); - if (conflict) { - conflicts.push({ packageName, ranges }); - continue; - } - merged[packageName] = first; - } - - return { dependencies: merged, conflicts }; - } - - async ensureInstalled(options: { plugins: RuntimeDependencyPluginDefine[]; logger?: ILogger }): Promise { - const { plugins, logger: log } = options; - const { dependencies, conflicts } = this.resolveDependenciesFromPlugins(plugins); - if (conflicts.length > 0) { - const conflict = conflicts[0]; - throw new Error(`动态依赖版本冲突: ${conflict.packageName} => ${conflict.ranges.map(item => `${item.pluginName}:${item.range}`).join(", ")}`); - } - return await this.ensureDependencies({ dependencies, logger: log }); - } - - async ensureDependencies(options: { dependencies: Record; logger?: ILogger }): Promise { - const { dependencies, logger: log } = options; - if (!this.enabled) { - return { - registryUrl: "", - packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"), - }; - } - if (!this.autoInstall) { - return { - registryUrl: "", - packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"), - }; - } - const dependenciesHash = this.createDependenciesHash(dependencies); - let installPromise = this.installPromises.get(dependenciesHash); - if (installPromise) { - const nodeModulesPath = path.join(this.getRuntimeDepsRootDir(), "node_modules"); - if (!fs.existsSync(nodeModulesPath)) { - this.installPromises.delete(dependenciesHash); - installPromise = undefined; - } - } - if (!installPromise) { - installPromise = this.doEnsureInstalled({ dependencies, logger: log }).catch(error => { - this.installPromises.delete(dependenciesHash); - throw error; - }); - this.installPromises.set(dependenciesHash, installPromise); - } - return await installPromise; - } - - resolveDependenciesFromPlugins(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult { - const expandedPlugins = plugins.flatMap(plugin => this.resolvePluginDependencies(plugin)); - return this.collectDependencies(expandedPlugins); - } - - async ensureRuntimeDependencies(options: { pluginKeys: string | string[]; logger?: ILogger }): Promise { - const { pluginKeys, logger: log } = options; - const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys]; - const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey)); - if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) { - return { - registryUrl: "", - packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"), - }; - } - const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine)); - return await this.ensureInstalled({ plugins: expandedPluginDefines, logger: log }); - } - - private async doEnsureInstalled(options: { dependencies: Record; logger?: ILogger }): Promise { - let { dependencies } = options; - const log = options.logger || logger; - return await this.withInstallLock(async () => { - const rootDir = this.getRuntimeDepsRootDir(); - const packageJsonPath = path.join(rootDir, "package.json"); - const lockPath = path.join(rootDir, "pnpm-lock.yaml"); - log.info(`第三方依赖安装: ${JSON.stringify(dependencies)}`); - dependencies = this.mergeInstalledDependencies(this.readManifestDependencies(packageJsonPath), dependencies); - const dependenciesHash = this.createDependenciesHash(dependencies); - const statePath = path.join(rootDir, "install-state.json"); - const currentState = this.readInstallState(statePath); - if (currentState?.dependenciesHash === dependenciesHash && fs.existsSync(path.join(rootDir, "node_modules"))) { - log.info("第三方依赖已安装"); - return { registryUrl: currentState.registryUrl || "", packageJsonPath }; - } - const manifest = { - name: "certd-runtime-deps", - private: true, - type: "module", - dependencies, - }; - fs.writeFileSync(packageJsonPath, JSON.stringify(manifest, null, 2), "utf8"); - - const registryUrl = await this.registryResolver.resolve(); - const env = this.buildChildEnv(registryUrl); - const command = this.getPnpmCommand(); - const pnpmVersion = await this.getPnpmVersion(command, env); - const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--no-frozen-lockfile", "--reporter=append-only"]; - if (registryUrl) { - args.push(`--registry=${registryUrl}`); - } - - log.info(`开始安装第三方依赖: ${Object.keys(dependencies).join(", ")}`); - const result = await this.commandRunner.run(command, args, { - cwd: rootDir, - timeoutMs: this.installTimeoutMs, - env, - }); - if (result.code !== 0) { - const message = result.stderr || result.stdout || "unknown error"; - this.writeInstallState(statePath, { - ...currentState, - installedAt: currentState?.installedAt, - failedAt: new Date().toISOString(), - registryUrl, - dependenciesHash, - // @ts-ignore - nodeVersion: process.version, - pnpmVersion, - lockFileExists: fs.existsSync(lockPath), - lastError: message, - }); - throw new Error(`动态依赖安装失败: ${message}`); - } - this.writeInstallState(statePath, { - installedAt: new Date().toISOString(), - registryUrl, - dependenciesHash, - // @ts-ignore - nodeVersion: process.version, - pnpmVersion, - lockFileExists: fs.existsSync(lockPath), - }); - log.info("第三方依赖安装完成"); - return { registryUrl, packageJsonPath }; - }); - } - - async importRuntime(specifier: string, logger?: ILogger) { - if (this.isNativeImportSpecifier(specifier)) { - return await import(specifier); - } - - const resolved = await this.resolveImportSpecifier(specifier, logger); - return await import(pathToFileURL(resolved).href); - } - - private async resolveImportSpecifier(specifier: string, logger?: ILogger) { - try { - return this.resolveRuntimeSpecifier(specifier).resolved; - } catch (runtimeError: any) { - if (!this.isModuleNotFoundError(runtimeError)) { - throw runtimeError; - } - return await this.resolveMissingRuntimeSpecifier(specifier, runtimeError, logger); - } - } - - private async resolveMissingRuntimeSpecifier(specifier: string, runtimeError: any, logger?: ILogger) { - const packageName = this.parsePackageName(specifier); - const mergedDeps = this.getMergedLazyDependencies(); - const lazyRange = mergedDeps[packageName]; - if (!lazyRange) { - try { - return this.resolveProjectSpecifier(specifier, runtimeError).resolved; - } catch { - throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`); - } - } - try { - await this.ensureLazyDependency(packageName, logger); - return this.resolveRuntimeSpecifier(specifier).resolved; - } catch (lazyError: any) { - return this.resolveProjectSpecifier(specifier, lazyError).resolved; - } - } - - private isNativeImportSpecifier(specifier: string) { - return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:"); - } - - private resolveRuntimeSpecifier(specifier: string): RuntimeImportResolveResult { - const packageName = this.parsePackageName(specifier); - const packageJsonPath = path.join(this.getRuntimeDepsRootDir(), "package.json"); - const require = createRequire(packageJsonPath); - const resolved = require.resolve(specifier); - return { packageName, resolved }; - } - - private resolveProjectSpecifier(specifier: string, cause?: any): RuntimeImportResolveResult { - try { - const packageName = this.parsePackageName(specifier); - const packageJsonPath = path.resolve("package.json"); - const require = createRequire(packageJsonPath); - const resolved = require.resolve(specifier); - return { packageName, resolved }; - } catch (projectError: any) { - if (cause) { - projectError.cause = cause; - } - throw projectError; - } - } - - private parsePackageName(specifier: string) { - if (!specifier || specifier.trim() !== specifier) { - throw new Error(`动态依赖导入路径无效: ${specifier}`); - } - const parts = specifier.split("/"); - if (specifier.startsWith("@")) { - if (parts.length < 2 || !parts[0] || !parts[1]) { - throw new Error(`动态依赖导入路径无效: ${specifier}`); - } - return `${parts[0]}/${parts[1]}`; - } - if (!parts[0]) { - throw new Error(`动态依赖导入路径无效: ${specifier}`); - } - return parts[0]; - } - - private async ensureLazyDependency(packageName: string, logger?: ILogger) { - const range = this.lazyDependencies?.[packageName]; - if (!range) { - throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`); - } - const dependencies = { - [packageName]: range, - }; - await this.ensureDependencies({ dependencies, logger }); - } - - private isModuleNotFoundError(error: any) { - return error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND"; - } - - resolvePluginDependencies(current: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine[] { - const resolved: RuntimeDependencyPluginDefine[] = []; - const visited = new Set(); - - const visit = (item: RuntimeDependencyPluginDefine) => { - const key = this.buildPluginDependencyKey(item); - if (visited.has(key)) { - return; - } - visited.add(key); - resolved.push(item); - for (const [dependencyName, expectedRange] of Object.entries(item.dependPlugins || {})) { - const dependency = this.getDefineByPluginKey(dependencyName, item); - if (!isPluginVersionCompatible(dependency, expectedRange)) { - throw new Error(`插件依赖版本冲突: ${item.name} 依赖 ${dependencyName}@${expectedRange},当前版本为 ${dependency.version || "未声明"}`); - } - visit(dependency); - } - }; - - visit(current); - return resolved; - } - - private buildPluginDependencyKey(plugin: RuntimeDependencyPluginDefine) { - if (plugin.pluginType === "addon" && plugin.addonType) { - return `addon:${plugin.addonType}:${plugin.name}`; - } - const pluginType = plugin.pluginType === "deploy" ? "plugin" : plugin.pluginType || "unknown"; - return `${pluginType}:${plugin.name}`; - } - - private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine { - const parts = pluginKey.split(":"); - const [pluginType, subtype, rawName] = parts; - let name = rawName; - if (parts.length === 2) { - name = subtype; - } else if (parts.length === 3) { - //无修改 - } else { - const ownerName = owner?.name || pluginKey; - throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},请使用 plugin:name、access:name、notification:name、dnsProvider:name 或 addon:subtype:name 格式`); - } - const registryMap: Record; key: string; pluginType: string; addonType?: string }> = { - plugin: { registry: pluginRegistry, key: name, pluginType: "plugin" }, - access: { registry: accessRegistry, key: name, pluginType: "access" }, - notification: { registry: notificationRegistry, key: name, pluginType: "notification" }, - dnsProvider: { registry: dnsProviderRegistry, key: name, pluginType: "dnsProvider" }, - addon: { registry: addonRegistry, key: `${subtype}:${name}`, pluginType: "addon", addonType: subtype }, - }; - const target = registryMap[pluginType]; - if (!target) { - const ownerName = owner?.name || pluginKey; - throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},未知插件类型 ${pluginType}`); - } - const define = target.registry.getDefine(target.key) as RegisteredDefineLike; - if (!define) { - throw new Error(`插件依赖缺失: ${owner?.name || pluginKey} 依赖 ${pluginKey},但该插件未注册或已禁用`); - } - return { ...define, key: pluginKey, pluginType: target.pluginType, addonType: target.addonType }; - } - - private async withInstallLock(run: () => Promise): Promise { - const rootDir = this.getRuntimeDepsRootDir(); - fs.mkdirSync(rootDir, { recursive: true }); - const lockFile = path.join(rootDir, ".install.lock"); - const previous = PROCESS_LOCKS.get(lockFile); - if (previous) { - await previous.catch(() => undefined); - } - let releaseProcessLock!: () => void; - const current = new Promise(resolve => { - releaseProcessLock = resolve; - }); - PROCESS_LOCKS.set(lockFile, current); - let fd: number | undefined; - try { - fd = await this.acquireFileLock(lockFile); - return await run(); - } finally { - if (fd != null) { - fs.closeSync(fd); - try { - fs.rmSync(lockFile, { force: true }); - } catch { - try { - fs.rmSync(lockFile, { force: true }); - } catch { - // Windows 下 closeSync 后文件句柄可能未立即释放,忽略清理失败 - } - } - } - releaseProcessLock(); - if (PROCESS_LOCKS.get(lockFile) === current) { - PROCESS_LOCKS.delete(lockFile); - } - } - } - - private async acquireFileLock(lockFile: string) { - const deadline = Date.now() + this.installTimeoutMs; - while (true) { - try { - const fd = fs.openSync(lockFile, "wx"); - // @ts-ignore - fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8"); - return fd; - } catch (error: any) { - if (error?.code !== "EEXIST") { - throw error; - } - if (Date.now() > deadline) { - throw new Error(`动态依赖安装锁等待超时: ${lockFile}`); - } - await this.waitForExternalLock(lockFile, deadline); - } - } - } - - private async waitForExternalLock(lockFile: string, deadline: number) { - while (fs.existsSync(lockFile)) { - if (Date.now() > deadline) { - throw new Error(`动态依赖安装锁等待超时: ${lockFile}`); - } - await new Promise(resolve => setTimeout(resolve, 300)); - } - } - - async clearRuntimeDeps() { - const rootDir = this.getRuntimeDepsRootDir(); - const normalizedRootDir = path.normalize(rootDir); - if (!normalizedRootDir.endsWith(path.normalize(".runtime-deps"))) { - throw new Error(`动态依赖目录配置异常,拒绝清理: ${rootDir}`); - } - await this.withInstallLock(async () => { - if (fs.existsSync(rootDir)) { - const entries = fs.readdirSync(rootDir); - for (const entry of entries) { - if (entry === ".install.lock") { - continue; - } - const entryPath = path.join(rootDir, entry); - fs.rmSync(entryPath, { recursive: true, force: true }); - } - } - this.installPromises.clear(); - return undefined; - }); - } - - /** - * 合并 package.json 的 lazyDependencies 和插件注册表收集到的 dependPackages - * 插件依赖优先(同名时使用插件声明的版本) - */ - getMergedLazyDependencies(): Record { - return { ...this.lazyDependencies, ...this.pluginLazyDependencies }; - } - - /** - * 从所有插件注册表中收集 dependPackages,合并到 pluginLazyDependencies - */ - collectPluginDeps() { - const registries: Array<{ registry: Registry; keyFormatter?: (name: string) => string }> = [ - { registry: pluginRegistry }, - { registry: accessRegistry }, - { registry: notificationRegistry }, - { registry: dnsProviderRegistry }, - { registry: addonRegistry }, - ]; - - const deps: Record = {}; - for (const { registry } of registries) { - const defineList = registry.getDefineList(); - for (const define of defineList) { - const dependPackages = (define as any).dependPackages as Record | undefined; - if (!dependPackages) { - continue; - } - for (const [pkgName, range] of Object.entries(dependPackages)) { - const existing = deps[pkgName]; - if (existing && !areRangesCompatible(existing, range)) { - logger.warn(`懒加载依赖版本冲突: ${pkgName} => ${existing} vs ${range},保留已有版本`); - continue; - } - deps[pkgName] = range; - } - } - } - - this.pluginLazyDependencies = deps; - logger.info(`从插件注册表收集到 ${Object.keys(deps).length} 个懒加载依赖`); - } - - /** - * 刷新插件懒加载依赖:收集所有插件的 dependPackages 到内存列表,实际安装延迟到 importRuntime 时触发 - */ - async refreshPluginDeps() { - this.collectPluginDeps(); - } - - private readInstallState(statePath: string): any { - if (!fs.existsSync(statePath)) { - return null; - } - try { - return JSON.parse(fs.readFileSync(statePath, "utf8")); - } catch { - return null; - } - } - - private writeInstallState(statePath: string, state: any) { - fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8"); - } - - private readManifestDependencies(packageJsonPath: string): Record { - if (!fs.existsSync(packageJsonPath)) { - return {}; - } - try { - const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); - return manifest.dependencies || {}; - } catch { - return {}; - } - } - - private mergeInstalledDependencies(installed: Record, requested: Record) { - const dependencies = { ...installed }; - for (const [packageName, range] of Object.entries(requested)) { - const installedRange = dependencies[packageName]; - if (installedRange && !areRangesCompatible(installedRange, range)) { - throw new Error(`动态依赖版本冲突: ${packageName} => installed:${installedRange}, requested:${range}`); - } - dependencies[packageName] = installedRange || range; - } - return dependencies; - } - - // @ts-ignore - private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) { - const rootDir = this.getRuntimeDepsRootDir(); - const result = await this.commandRunner.run(command, ["--version"], { - cwd: rootDir, - timeoutMs: Math.min(this.installTimeoutMs, 10000), - env, - }); - if (result.code !== 0) { - return ""; - } - return (result.stdout || result.stderr || "").trim(); - } - - private getPnpmCommand() { - if (this.pnpmCommand) { - return this.pnpmCommand; - } - return "pnpm"; - } - - private buildChildEnv(registryUrl: string) { - // @ts-ignore - const env = { ...process.env }; - for (const key of ["NODE_OPTIONS", "VSCODE_INSPECTOR_OPTIONS", "NODE_INSPECTOR_PORT", "NODE_DEBUG"]) { - if (!env[key]) { - continue; - } - if (key === "NODE_OPTIONS") { - env[key] = this.stripDebugNodeOptions(env[key] as string); - } else { - delete env[key]; - } - } - if (registryUrl) { - env.npm_config_registry = registryUrl; - env.pnpm_config_registry = registryUrl; - } - env.CI = env.CI || "true"; - env.npm_config_confirm_modules_purge = "false"; - env.pnpm_config_confirm_modules_purge = "false"; - return env; - } - - private stripDebugNodeOptions(value: string) { - return value - .split(/\s+/) - .filter(Boolean) - .filter(item => !/^--inspect(-brk|-port)?(=|$)/.test(item)) - .filter(item => !/^--debug(=|$)/.test(item)) - .join(" "); - } - - private getRuntimeDepsRootDir() { - return path.resolve(this.runtimeDepsRootDir); - } - - private createDependenciesHash(dependencies: Record) { - return crypto.createHash("sha256").update(JSON.stringify(dependencies)).digest("hex"); - } -} - -function isPluginVersionCompatible(plugin: RuntimeDependencyPluginDefine, expectedRange: string) { - if (!expectedRange || expectedRange === "*") { - return true; - } - if (!plugin.version) { - return false; - } - return areRangesCompatible(expectedRange, plugin.version); -} diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts index 86d4c8589..63bf0afc0 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts @@ -1,4 +1,4 @@ -import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, DnsResolveRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; +import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, DnsResolveRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; import { AliyunAccess } from "../../plugin-lib/aliyun/access/aliyun-access.js"; import { AliyunClient } from "../../plugin-lib/aliyun/index.js"; import { Pager, PageRes, PageSearch } from "@certd/pipeline"; @@ -16,7 +16,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider { async onInstance() { const access: AliyunAccess = this.ctx.access as AliyunAccess; - this.client = new AliyunClient({ logger: this.logger, importRuntime: this.importRuntime.bind(this) }); + this.client = new AliyunClient({ logger: this.logger }); await this.client.init({ accessKeyId: access.accessKeyId, accessKeySecret: access.accessKeySecret, diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-ack/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-ack/index.ts index 71167bf48..26284b59a 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-ack/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-ack/index.ts @@ -1,4 +1,4 @@ -import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { utils } from "@certd/basic"; import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; @@ -241,7 +241,7 @@ export class DeployCertToAliyunAckPlugin extends AbstractTaskPlugin { } async getClient(aliyunProvider: any, regionId: string) { - const client = new AliyunClient({ logger: this.logger, useROAClient: true, importRuntime: aliyunProvider.importRuntime.bind(aliyunProvider) }); + const client = new AliyunClient({ logger: this.logger, useROAClient: true }); await client.init({ accessKeyId: aliyunProvider.accessKeyId, accessKeySecret: aliyunProvider.accessKeySecret, diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-alb/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-alb/index.ts index 04ab71ecc..2c9be0752 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-alb/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-alb/index.ts @@ -1,4 +1,4 @@ -import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; @@ -128,7 +128,7 @@ export class AliyunDeployCertToALB extends AbstractTaskPlugin { async onInstance() {} async getLBClient(access: AliyunAccess, region: string) { - const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) }); + const client = new AliyunClient({ logger: this.logger }); const version = "2020-06-16"; await client.init({ diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-cdn/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-cdn/index.ts index d4aef9121..1450b2d84 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-cdn/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-cdn/index.ts @@ -1,4 +1,4 @@ -import { optionsUtils } from "@certd/basic"; +import { optionsUtils } from "@certd/basic"; import { AbstractTaskPlugin, CertTargetItem, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline"; import { CertApplyPluginNames, CertReader } from "@certd/plugin-cert"; import { CertInfo, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; @@ -209,7 +209,7 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin { } async getClient(access: AliyunAccess) { - const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) }); + const client = new AliyunClient({ logger: this.logger }); await client.init({ accessKeyId: access.accessKeyId, accessKeySecret: access.accessKeySecret, diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-dcdn/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-dcdn/index.ts index 0ee34e4d4..e65be98b0 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-dcdn/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-dcdn/index.ts @@ -1,4 +1,4 @@ -import { AbstractTaskPlugin, CertTargetItem, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline"; +import { AbstractTaskPlugin, CertTargetItem, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import dayjs from "dayjs"; import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; @@ -143,7 +143,7 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin { } async getClient(access: AliyunAccess) { - const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) }); + const client = new AliyunClient({ logger: this.logger }); await client.init({ accessKeyId: access.accessKeyId, accessKeySecret: access.accessKeySecret, diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-fc/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-fc/index.ts index 29caf9fed..9168b025c 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-fc/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-fc/index.ts @@ -137,8 +137,8 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin { const client = await this.getClient(access); - const $Util = await access.importRuntime("@alicloud/tea-util"); - const $OpenApi = await access.importRuntime("@alicloud/openapi-client"); + const $Util = await this.importRuntime("@alicloud/tea-util"); + const $OpenApi = await this.importRuntime("@alicloud/openapi-client"); let privateKey = this.cert.key; try { @@ -204,7 +204,7 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin { } async getClient(access: AliyunAccess) { - const $OpenApi = await access.importRuntime("@alicloud/openapi-client"); + const $OpenApi = await this.importRuntime("@alicloud/openapi-client"); const config = new $OpenApi.Config({ accessKeyId: access.accessKeyId, accessKeySecret: access.accessKeySecret, @@ -221,8 +221,8 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin { const access = await this.getAccess(this.accessId); const client = await this.getClient(access); - const $OpenApi = await access.importRuntime("@alicloud/openapi-client"); - const $Util = await access.importRuntime("@alicloud/tea-util"); + const $OpenApi = await this.importRuntime("@alicloud/openapi-client"); + const $Util = await this.importRuntime("@alicloud/tea-util"); const params = new $OpenApi.Params({ // 接口名称 action: "ListCustomDomains", diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-nlb/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-nlb/index.ts index 28f937f8e..ed151e3a8 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-nlb/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-nlb/index.ts @@ -1,4 +1,4 @@ -import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CertInfo, CertReader } from "@certd/plugin-cert"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { CertApplyPluginNames } from "@certd/plugin-cert"; @@ -117,7 +117,7 @@ export class AliyunDeployCertToNLB extends AbstractTaskPlugin { async onInstance() {} async getLBClient(access: AliyunAccess, region: string) { - const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) }); + const client = new AliyunClient({ logger: this.logger }); const version = "2022-04-30"; await client.init({ diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-oss/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-oss/index.ts index 1691d02dc..02a09d8de 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-oss/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-oss/index.ts @@ -196,7 +196,7 @@ export class DeployCertToAliyunOSS extends AbstractTaskPlugin { async getClient(access: AliyunAccess) { // @ts-ignore - const OSS = await access.importRuntime("ali-oss"); + const OSS = await this.importRuntime("ali-oss"); return new OSS.default({ accessKeyId: access.accessKeyId, accessKeySecret: access.accessKeySecret, diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-slb/index.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-slb/index.ts index 399a4f852..afe17c013 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-slb/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-slb/index.ts @@ -1,4 +1,4 @@ -import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CertInfo } from "@certd/plugin-cert"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { CertApplyPluginNames } from "@certd/plugin-cert"; @@ -130,7 +130,7 @@ export class AliyunDeployCertToSLB extends AbstractTaskPlugin { async onInstance() {} async getLBClient(access: AliyunAccess, region: string) { - const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) }); + const client = new AliyunClient({ logger: this.logger }); const version = "2014-05-15"; await client.init({ accessKeyId: access.accessKeyId, diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-waf/deploy-to-waf-cloud.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-waf/deploy-to-waf-cloud.ts index 6320f5518..185a2b223 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-waf/deploy-to-waf-cloud.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-waf/deploy-to-waf-cloud.ts @@ -1,4 +1,4 @@ -import { AbstractTaskPlugin, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { AbstractTaskPlugin, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; @@ -103,7 +103,7 @@ export class AliyunDeployCertToWafCloud extends AbstractTaskPlugin { async onInstance() {} async getWafClient(access: AliyunAccess) { - const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) }); + const client = new AliyunClient({ logger: this.logger }); await client.init({ accessKeyId: access.accessKeyId, accessKeySecret: access.accessKeySecret, diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-waf/deploy-to-waf-cname.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-waf/deploy-to-waf-cname.ts index a655798de..058f44331 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-waf/deploy-to-waf-cname.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-waf/deploy-to-waf-cname.ts @@ -1,4 +1,4 @@ -import { AbstractTaskPlugin, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { AbstractTaskPlugin, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; @@ -115,7 +115,7 @@ export class AliyunDeployCertToWaf extends AbstractTaskPlugin { async onInstance() {} async getWafClient(access: AliyunAccess) { - const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) }); + const client = new AliyunClient({ logger: this.logger }); await client.init({ accessKeyId: access.accessKeyId, accessKeySecret: access.accessKeySecret, diff --git a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/access/alioss-access.ts b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/access/alioss-access.ts index 372325e78..c18d34ade 100644 --- a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/access/alioss-access.ts +++ b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/access/alioss-access.ts @@ -101,7 +101,7 @@ export class AliossAccess extends BaseAccess { async getClient(access: AliyunAccess) { // @ts-ignore - const OSS = await access.importRuntime("ali-oss"); + const OSS = await this.importRuntime("ali-oss"); return new OSS.default({ accessKeyId: access.accessKeyId, accessKeySecret: access.accessKeySecret, diff --git a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/access/aliyun-access.ts b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/access/aliyun-access.ts index 090cb7dbd..79e62c4f5 100644 --- a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/access/aliyun-access.ts +++ b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/access/aliyun-access.ts @@ -1,4 +1,4 @@ -import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline"; +import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline"; import { AliyunClientV2 } from "../lib/aliyun-client-v2.js"; import { AliyunSslClient } from "../lib/ssl-client.js"; @IsAccess({ diff --git a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/aliyun-client-v2.ts b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/aliyun-client-v2.ts index 11e8a869f..554c0a2e7 100644 --- a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/aliyun-client-v2.ts +++ b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/aliyun-client-v2.ts @@ -1,5 +1,6 @@ -import { ILogger } from "@certd/basic"; +import { ILogger } from "@certd/basic"; import { AliyunAccess } from "../access/aliyun-access.js"; +import { importRuntime as importRuntimeDirect } from "@certd/pipeline"; export type AliyunClientV2Req = { action: string; @@ -26,11 +27,15 @@ export class AliyunClientV2 { this.endpoint = opts.endpoint; } + async importRuntime(name: string) { + return await importRuntimeDirect(name, this.logger); + } + async getClient() { if (this.client) { return this.client; } - const $OpenApi = await this.access.importRuntime("@alicloud/openapi-client"); + const $OpenApi = await this.importRuntime("@alicloud/openapi-client"); // const Credential = await import("@alicloud/credentials"); // //@ts-ignore // const credential = new Credential.default.default({ @@ -52,9 +57,9 @@ export class AliyunClientV2 { async doRequest(req: AliyunClientV2Req) { const client = await this.getClient(); - const $OpenApi = await this.access.importRuntime("@alicloud/openapi-client"); - const $Util = await this.access.importRuntime("@alicloud/tea-util"); - const OpenApiUtil = await this.access.importRuntime("@alicloud/openapi-util"); + const $OpenApi = await this.importRuntime("@alicloud/openapi-client"); + const $Util = await this.importRuntime("@alicloud/tea-util"); + const OpenApiUtil = await this.importRuntime("@alicloud/openapi-util"); const params = new $OpenApi.Params({ // 接口名称 action: req.action, diff --git a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/base-client.ts b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/base-client.ts index b2950ddc4..c66733b18 100644 --- a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/base-client.ts +++ b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/base-client.ts @@ -1,21 +1,24 @@ -import { getGlobalAgents, ILogger } from "@certd/basic"; -import { ImportRuntime } from "@certd/pipeline"; +import { getGlobalAgents, ILogger } from "@certd/basic"; +import { importRuntime as importRuntimeDirect } from "@certd/pipeline"; export class AliyunClient { client: any; logger: ILogger; agent: any; useROAClient: boolean; - importRuntime: ImportRuntime; - constructor(opts: { logger: ILogger; useROAClient?: boolean; importRuntime?: ImportRuntime }) { + constructor(opts: { logger: ILogger; useROAClient?: boolean }) { this.logger = opts.logger; this.useROAClient = opts.useROAClient || false; - this.importRuntime = opts.importRuntime || (async (specifier: string) => await import(specifier)); + const agents = getGlobalAgents(); this.agent = agents.httpsAgent; } + async importRuntime(specifier: string) { + return await importRuntimeDirect(specifier, this.logger); + } + async getSdk() { if (this.useROAClient) { return await this.getROAClient(); diff --git a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/ssl-client.ts b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/ssl-client.ts index 4c26223ad..319a81eab 100644 --- a/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/ssl-client.ts +++ b/packages/ui/certd-server/src/plugins/plugin-lib/aliyun/lib/ssl-client.ts @@ -1,4 +1,4 @@ -import { ILogger, utils } from "@certd/basic"; +import { ILogger, utils } from "@certd/basic"; import { AliyunAccess } from "../access/index.js"; import { AliyunClient } from "./index.js"; import { CertInfo, CertReader, SimpleCertDetail } from "@certd/plugin-lib"; @@ -42,6 +42,7 @@ export type CasCertId = { export class AliyunSslClient { opts: AliyunSslClientOpts; logger: ILogger; + constructor(opts: AliyunSslClientOpts) { this.opts = opts; this.logger = opts.logger; @@ -55,7 +56,7 @@ export class AliyunSslClient { async getClient() { const access = this.opts.access; - const client = new AliyunClient({ logger: this.opts.logger, importRuntime: access.importRuntime.bind(access) }); + const client = new AliyunClient({ logger: this.opts.logger }); let endpoint = this.opts.endpoint || "cas.aliyuncs.com"; if (this.opts.endpoint == null && this.opts.region) { diff --git a/packages/ui/certd-server/src/plugins/plugin-lib/oss/impls/tencentcos.ts b/packages/ui/certd-server/src/plugins/plugin-lib/oss/impls/tencentcos.ts index 5656f3e64..33bc1bfd4 100644 --- a/packages/ui/certd-server/src/plugins/plugin-lib/oss/impls/tencentcos.ts +++ b/packages/ui/certd-server/src/plugins/plugin-lib/oss/impls/tencentcos.ts @@ -1,4 +1,4 @@ -import dayjs from "dayjs"; +import dayjs from "dayjs"; import { TencentAccess, TencentCosAccess, TencentCosClient } from "../../tencent/index.js"; import { BaseOssClient, OssFileItem } from "../api.js"; diff --git a/packages/ui/certd-server/src/plugins/plugin-lib/tencent/access.ts b/packages/ui/certd-server/src/plugins/plugin-lib/tencent/access.ts index 3430e7ea3..923d20ffe 100644 --- a/packages/ui/certd-server/src/plugins/plugin-lib/tencent/access.ts +++ b/packages/ui/certd-server/src/plugins/plugin-lib/tencent/access.ts @@ -109,7 +109,7 @@ export class TencentAccess extends BaseAccess { } async getStsClient() { - const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sts/v20180813/index.js"); + const sdk = await this.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sts/v20180813/index.js"); const StsClient = sdk.v20180813.Client; const clientConfig = { diff --git a/packages/ui/certd-server/src/plugins/plugin-lib/tencent/lib/cos-client.ts b/packages/ui/certd-server/src/plugins/plugin-lib/tencent/lib/cos-client.ts index 7c3a29b4a..567194176 100644 --- a/packages/ui/certd-server/src/plugins/plugin-lib/tencent/lib/cos-client.ts +++ b/packages/ui/certd-server/src/plugins/plugin-lib/tencent/lib/cos-client.ts @@ -1,21 +1,23 @@ -import { TencentAccess } from "../access.js"; +import { TencentAccess } from "../access.js"; import { ILogger, safePromise } from "@certd/basic"; -import { ImportRuntime } from "@certd/pipeline"; import fs from "fs"; +import { importRuntime as importRuntimeDirect } from "@certd/pipeline"; export class TencentCosClient { access: TencentAccess; logger: ILogger; region: string; bucket: string; - importRuntime: ImportRuntime; - constructor(opts: { access: TencentAccess; logger: ILogger; region: string; bucket: string; importRuntime?: ImportRuntime }) { + constructor(opts: { access: TencentAccess; logger: ILogger; region: string; bucket: string }) { this.access = opts.access; this.logger = opts.logger; this.bucket = opts.bucket; this.region = opts.region; - this.importRuntime = opts.importRuntime || (async (specifier: string) => await import(specifier)); + } + + async importRuntime(specifier: string) { + return await importRuntimeDirect(specifier, this.logger); } async getCosClient() { diff --git a/packages/ui/certd-server/src/plugins/plugin-nginx-proxy-manager/NginxProxyManagerDeploy.yaml b/packages/ui/certd-server/src/plugins/plugin-nginx-proxy-manager/NginxProxyManagerDeploy.yaml deleted file mode 100644 index c01567fd7..000000000 --- a/packages/ui/certd-server/src/plugins/plugin-nginx-proxy-manager/NginxProxyManagerDeploy.yaml +++ /dev/null @@ -1,348 +0,0 @@ -name: NginxProxyManagerDeploy -icon: logos:nginx -title: Nginx Proxy Manager-部署到主机 -group: panel -desc: 上传自定义证书到 Nginx Proxy Manager,并绑定到所选主机。 -setting: null -sysSetting: null -type: custom -disabled: false -version: 1.0.0 -pluginType: deploy -author: samler -input: - cert: - title: 域名证书 - helper: 请选择前置任务产出的证书。 - component: - name: output-selector - from: - - ":cert:" - required: true - order: 0 - certDomains: - title: 证书域名 - component: - name: cert-domains-getter - mergeScript: | - return { - component: { - inputKey: ctx.compute(({ form }) => { - return form.cert; - }), - }, - } - required: false - order: 0 - accessId: - title: NPM授权 - component: - name: access-selector - type: samler/nginxProxyManager - helper: 选择用于部署的 Nginx Proxy Manager 授权。 - required: true - order: 0 - proxyHostIds: - title: 代理主机 - component: - name: remote-select - vModel: value - mode: tags - type: plugin - action: onGetProxyHostOptions - search: true - pager: false - multi: true - watches: - - certDomains - - accessId - required: true - helper: 选择要绑定此证书的一个或多个代理主机。 - mergeScript: | - return { - component: { - form: ctx.compute(({ form }) => { - return form; - }), - }, - } - order: 0 - certificateLabel: - title: 证书标识 - component: - name: a-input - allowClear: true - placeholder: certd_npm_example_com - helper: 可选。留空时默认使用 certd_npm_<主域名规范化>. - required: false - order: 0 - cleanupMatchingCertificates: - title: 自动清理未使用证书 - component: - name: a-switch - vModel: checked - helper: 部署成功后,自动删除除当前证书外所有未被任何主机引用的证书。 - required: false - order: 0 -output: {} -default: - strategy: - runStrategy: 1 -showRunStrategy: false -content: | - const { AbstractTaskPlugin } = await import("@certd/pipeline"); - const { CertReader } = await import("@certd/plugin-cert"); - - function normalizeDomain(domain) { - return String(domain ?? "").trim().toLowerCase(); - } - - function wildcardMatches(pattern, candidate) { - if (!pattern.startsWith("*.")) { - return false; - } - - const suffix = pattern.slice(1).toLowerCase(); - return candidate.endsWith(suffix); - } - - function isDomainMatch(left, right) { - const normalizedLeft = normalizeDomain(left); - const normalizedRight = normalizeDomain(right); - - return ( - normalizedLeft === normalizedRight || - wildcardMatches(normalizedLeft, normalizedRight) || - wildcardMatches(normalizedRight, normalizedLeft) - ); - } - - function normalizeDomainIdentity(domain) { - return normalizeDomain(domain).replace(/^\*\./, ""); - } - - function certificateHasBindings(certificate) { - return ( - (certificate.proxy_hosts?.length ?? 0) > 0 || - (certificate.redirection_hosts?.length ?? 0) > 0 || - (certificate.dead_hosts?.length ?? 0) > 0 || - (certificate.streams?.length ?? 0) > 0 - ); - } - - function sanitizeDomainSegment(value) { - const sanitized = String(value ?? "") - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, "") - .replace(/_+/g, "_"); - - return sanitized || "unknown"; - } - - function buildDefaultCertificateLabel(cert) { - const mainDomain = CertReader.getMainDomain(cert.crt); - return `certd_npm_${sanitizeDomainSegment(mainDomain)}`; - } - - function normalizeStringList(input) { - if (Array.isArray(input)) { - return input; - } - - if (input == null || input === "") { - return []; - } - - return [input]; - } - - function resolveCertificateDomains(cert, configuredDomains) { - const configured = normalizeStringList(configuredDomains) - .map((value) => String(value).trim()) - .filter(Boolean); - - if (configured.length > 0) { - return Array.from(new Set(configured)); - } - - return new CertReader(cert).getAllDomains(); - } - - function buildProxyHostLabel(host) { - const domains = host.domain_names?.length ? host.domain_names.join(", ") : "(no domains)"; - return `${domains} <#${host.id}>`; - } - - function hasAnyCertDomainMatch(host, certDomains) { - if (!certDomains.length) { - return false; - } - - const hostDomains = host.domain_names ?? []; - return hostDomains.some((hostDomain) => certDomains.some((certDomain) => isDomainMatch(hostDomain, certDomain))); - } - - function buildProxyHostOptions(hosts, certDomains) { - const sortedHosts = [...hosts].sort((left, right) => { - return buildProxyHostLabel(left).localeCompare(buildProxyHostLabel(right)); - }); - - const matched = []; - const unmatched = []; - - for (const host of sortedHosts) { - const option = { - label: buildProxyHostLabel(host), - value: String(host.id), - domain: host.domain_names?.[0] ?? "", - }; - - if (hasAnyCertDomainMatch(host, certDomains)) { - matched.push(option); - } else { - unmatched.push(option); - } - } - - if (matched.length && unmatched.length) { - return [ - { - label: "匹配证书域名的主机", - options: matched, - }, - { - label: "其他代理主机", - options: unmatched, - }, - ]; - } - - return matched.length ? matched : unmatched; - } - - function normalizeProxyHostIds(proxyHostIds) { - return Array.from( - new Set( - normalizeStringList(proxyHostIds) - .map((value) => Number.parseInt(String(value), 10)) - .filter((value) => Number.isInteger(value) && value > 0), - ), - ); - } - - return class NpmDeployToProxyHosts extends AbstractTaskPlugin { - cert; - certDomains; - accessId; - proxyHostIds; - certificateLabel; - cleanupMatchingCertificates = false; - - async execute() { - const access = await this.getAccess(this.accessId); - const client = access.createClient(); - const proxyHostIds = normalizeProxyHostIds(this.proxyHostIds); - - if (proxyHostIds.length === 0) { - throw new Error("请至少选择一个 Nginx Proxy Manager 代理主机"); - } - - const certificateLabel = this.certificateLabel?.trim() || buildDefaultCertificateLabel(this.cert); - const certificateDomains = resolveCertificateDomains(this.cert, this.certDomains); - - let certificate = await client.findCustomCertificateByNiceName(certificateLabel); - if (!certificate) { - this.logger.info(`在 Nginx Proxy Manager 中创建自定义证书 "${certificateLabel}"`); - certificate = await client.createCustomCertificate(certificateLabel, certificateDomains); - } else { - this.logger.info(`复用已有自定义证书 "${certificateLabel}" (#${certificate.id})`); - } - - await client.uploadCertificate(certificate.id, { - certificate: this.cert.crt, - certificateKey: this.cert.key, - intermediateCertificate: this.cert.ic, - }); - this.logger.info(`证书内容已上传到 Nginx Proxy Manager 证书 #${certificate.id}`); - - for (const proxyHostId of proxyHostIds) { - this.logger.info(`将证书 #${certificate.id} 绑定到代理主机 #${proxyHostId}`); - try { - await client.assignCertificateToProxyHost(proxyHostId, certificate.id); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error(`为代理主机 #${proxyHostId} 绑定证书失败:${message}`); - } - } - - if (this.cleanupMatchingCertificates === true) { - await this.cleanupOldCertificates(client, certificate.id); - } - - this.logger.info(`部署完成,共更新 ${proxyHostIds.length} 个代理主机`); - } - - async onGetProxyHostOptions(req = {}) { - if (!this.accessId) { - throw new Error("请先选择 Nginx Proxy Manager 授权"); - } - - const access = await this.getAccess(this.accessId); - const proxyHosts = await access.getProxyHostList(req); - return buildProxyHostOptions(proxyHosts, normalizeStringList(this.certDomains)); - } - - async cleanupOldCertificates(client, currentCertificateId) { - const certificates = await client.getCertificatesWithExpand(undefined, [ - "proxy_hosts", - "redirection_hosts", - "dead_hosts", - "streams", - ]); - - const candidates = certificates.filter((certificate) => { - return certificate.id !== currentCertificateId; - }); - - if (candidates.length === 0) { - this.logger.info("未发现可自动清理的旧证书"); - return; - } - - const deletedIds = []; - const skippedInUse = []; - const failedDeletes = []; - - for (const candidate of candidates) { - if (certificateHasBindings(candidate)) { - skippedInUse.push(`#${candidate.id} ${candidate.nice_name}`); - continue; - } - - this.logger.info(`自动清理旧证书 #${candidate.id} ${candidate.nice_name}`); - try { - await client.deleteCertificate(candidate.id); - deletedIds.push(candidate.id); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - failedDeletes.push(`#${candidate.id} ${candidate.nice_name}: ${message}`); - } - } - - if (deletedIds.length > 0) { - this.logger.info(`自动清理完成,共删除 ${deletedIds.length} 张旧证书:${deletedIds.map((id) => `#${id}`).join(", ")}`); - } else { - this.logger.info("未删除任何旧证书"); - } - - if (skippedInUse.length > 0) { - this.logger.info(`以下旧证书仍被其他资源引用,已跳过清理:${skippedInUse.join(", ")}`); - } - - if (failedDeletes.length > 0) { - this.logger.warn(`以下旧证书清理失败,已跳过:${failedDeletes.join(", ")}`); - } - } - }; diff --git a/packages/ui/certd-server/src/plugins/plugin-nginx-proxy-manager/nginxProxyManager.yaml b/packages/ui/certd-server/src/plugins/plugin-nginx-proxy-manager/nginxProxyManager.yaml deleted file mode 100644 index 59932d6df..000000000 --- a/packages/ui/certd-server/src/plugins/plugin-nginx-proxy-manager/nginxProxyManager.yaml +++ /dev/null @@ -1,347 +0,0 @@ -name: nginxProxyManager -icon: logos:nginx -title: Nginx Proxy Manager 授权 -group: null -desc: 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 -setting: null -sysSetting: null -type: custom -disabled: false -version: 1.0.0 -pluginType: access -author: samler -input: - endpoint: - title: NPM 地址 - component: - name: a-input - allowClear: true - placeholder: https://npm.example.com - helper: 请输入 Nginx Proxy Manager 根地址,不要带 /api 后缀。 - required: true - email: - title: 邮箱 - component: - name: a-input - allowClear: true - placeholder: admin@example.com - required: true - password: - title: 密码 - component: - name: a-input-password - allowClear: true - placeholder: 请输入密码 - required: true - encrypt: true - totpSecret: - title: TOTP 密钥 - component: - name: a-input-password - allowClear: true - placeholder: Optional base32 TOTP secret - helper: 当 Nginx Proxy Manager 账号开启 2FA 时必填。 - required: false - encrypt: true - ignoreTls: - title: 忽略无效 TLS - component: - name: a-switch - vModel: checked - helper: 仅在 Nginx Proxy Manager 使用自签 HTTPS 证书时开启。 - required: false - testRequest: - title: 测试 - component: - name: api-test - action: TestRequest - helper: 测试登录并拉取代理主机列表。 -content: | - const runtimeImport = import; - const { BaseAccess } = await runtimeImport("@certd/pipeline"); - const httpsModule = await runtimeImport("node:https"); - const { URL } = await runtimeImport("node:url"); - const axiosModule = await runtimeImport("axios"); - const formDataModule = await runtimeImport("form-data"); - - const https = httpsModule.default ?? httpsModule; - const axios = axiosModule.default ?? axiosModule; - const FormData = formDataModule.default ?? formDataModule; - - function normalizeEndpoint(endpoint) { - const trimmed = String(endpoint ?? "").trim(); - if (!trimmed) { - throw new Error("Nginx Proxy Manager 地址不能为空"); - } - - const withoutTrailingSlash = trimmed.replace(/\/+$/, ""); - return withoutTrailingSlash.endsWith("/api") - ? withoutTrailingSlash.slice(0, -4) - : withoutTrailingSlash; - } - - function buildHttpsAgent(endpoint, ignoreTls) { - const url = new URL(endpoint); - if (url.protocol !== "https:") { - return undefined; - } - - return new https.Agent({ - rejectUnauthorized: !ignoreTls, - }); - } - - function describeError(error, action) { - if (axios.isAxiosError(error)) { - const status = error.response?.status; - const data = error.response?.data; - const message = - data?.error?.message || - data?.error || - data?.message || - (typeof data === "string" ? data : null) || - error.message; - return new Error(`${action} failed${status ? ` (${status})` : ""}: ${message}`); - } - - if (error instanceof Error) { - return new Error(`${action} failed: ${error.message}`); - } - - return new Error(`${action} failed`); - } - - class NginxProxyManagerClient { - constructor(options) { - this.options = options; - this.importFn = options.importFn; - this.endpoint = normalizeEndpoint(options.endpoint); - this.apiBaseUrl = `${this.endpoint}/api`; - this.token = undefined; - this.tokenPromise = undefined; - this.httpClient = axios.create({ - baseURL: this.apiBaseUrl, - timeout: 30000, - maxBodyLength: Infinity, - maxContentLength: Infinity, - httpsAgent: buildHttpsAgent(this.endpoint, options.ignoreTls === true), - headers: { - Accept: "application/json", - }, - }); - } - - async verifyAccess() { - const proxyHosts = await this.getProxyHosts(); - return { - proxyHostCount: proxyHosts.length, - }; - } - - async getProxyHosts(searchQuery) { - return await this.requestWithAuth({ - method: "GET", - url: "/nginx/proxy-hosts", - params: { - expand: "certificate", - ...(searchQuery ? { query: searchQuery } : {}), - }, - }); - } - - async getCertificates(searchQuery) { - return await this.requestWithAuth({ - method: "GET", - url: "/nginx/certificates", - params: searchQuery ? { query: searchQuery } : undefined, - }); - } - - async getCertificatesWithExpand(searchQuery, expand = []) { - return await this.requestWithAuth({ - method: "GET", - url: "/nginx/certificates", - params: { - ...(searchQuery ? { query: searchQuery } : {}), - ...(expand.length > 0 ? { expand: expand.join(",") } : {}), - }, - }); - } - - async findCustomCertificateByNiceName(niceName) { - const certificates = await this.getCertificates(niceName); - return certificates.find((certificate) => { - return certificate.provider === "other" && certificate.nice_name === niceName; - }); - } - - async createCustomCertificate(niceName, domainNames = []) { - return await this.requestWithAuth({ - method: "POST", - url: "/nginx/certificates", - data: { - provider: "other", - nice_name: niceName, - domain_names: domainNames, - }, - }); - } - - async deleteCertificate(certificateId) { - await this.requestWithAuth({ - method: "DELETE", - url: `/nginx/certificates/${certificateId}`, - }); - } - - async uploadCertificate(certificateId, payload) { - const form = new FormData(); - form.append("certificate", Buffer.from(payload.certificate, "utf8"), { - filename: "fullchain.pem", - contentType: "application/x-pem-file", - }); - form.append("certificate_key", Buffer.from(payload.certificateKey, "utf8"), { - filename: "privkey.pem", - contentType: "application/x-pem-file", - }); - if (payload.intermediateCertificate) { - form.append("intermediate_certificate", Buffer.from(payload.intermediateCertificate, "utf8"), { - filename: "chain.pem", - contentType: "application/x-pem-file", - }); - } - - await this.requestWithAuth({ - method: "POST", - url: `/nginx/certificates/${certificateId}/upload`, - data: form, - headers: form.getHeaders(), - }); - } - - async assignCertificateToProxyHost(hostId, certificateId) { - await this.requestWithAuth({ - method: "PUT", - url: `/nginx/proxy-hosts/${hostId}`, - data: { - certificate_id: certificateId, - }, - }); - } - - async login() { - if (this.token) { - return this.token; - } - - if (!this.tokenPromise) { - this.tokenPromise = this.performLogin().finally(() => { - this.tokenPromise = undefined; - }); - } - - this.token = await this.tokenPromise; - return this.token; - } - - async performLogin() { - const initialLogin = await this.request({ - method: "POST", - url: "/tokens", - data: { - identity: this.options.email, - secret: this.options.password, - }, - }); - - if (initialLogin.token) { - return initialLogin.token; - } - - if (!initialLogin.requires_2fa || !initialLogin.challenge_token) { - throw new Error("登录失败:Nginx Proxy Manager 未返回访问令牌"); - } - - if (!this.options.totpSecret) { - throw new Error("登录失败:该 Nginx Proxy Manager 账号启用了 2FA,但未配置 totpSecret"); - } - - let code; - try { - const { authenticator } = await this.importFn("otplib"); - code = authenticator.generate(this.options.totpSecret); - } catch (error) { - throw describeError(error, "Generating TOTP code"); - } - - const completedLogin = await this.request({ - method: "POST", - url: "/tokens/2fa", - data: { - challenge_token: initialLogin.challenge_token, - code, - }, - }); - - if (!completedLogin.token) { - throw new Error("2FA 登录失败:Nginx Proxy Manager 未返回访问令牌"); - } - - return completedLogin.token; - } - - async requestWithAuth(config) { - const token = await this.login(); - const headers = { - ...(config.headers ?? {}), - Authorization: `Bearer ${token}`, - }; - - return await this.request({ - ...config, - headers, - }); - } - - async request(config) { - const action = `${config.method ?? "GET"} ${config.url ?? "/"}`; - try { - const response = await this.httpClient.request(config); - return response.data; - } catch (error) { - throw describeError(error, action); - } - } - } - - return class NginxProxyManagerAccess extends BaseAccess { - endpoint = ""; - email = ""; - password = ""; - totpSecret = ""; - ignoreTls = false; - testRequest = true; - - createClient() { - return new NginxProxyManagerClient({ - endpoint: this.endpoint, - email: this.email, - password: this.password, - totpSecret: this.totpSecret || undefined, - ignoreTls: this.ignoreTls === true, - importFn: this.importRuntime.bind(this), - }); - } - - async onTestRequest() { - const client = this.createClient(); - const result = await client.verifyAccess(); - this.ctx.logger.info(`Nginx Proxy Manager 授权验证成功,找到 ${result.proxyHostCount} 个代理主机`); - return `成功(${result.proxyHostCount} 个代理主机)`; - } - - async getProxyHostList(req = {}) { - const client = this.createClient(); - return await client.getProxyHosts(req.searchKey); - } - }; diff --git a/packages/ui/certd-server/src/plugins/plugin-notification/dingtalk/index.ts b/packages/ui/certd-server/src/plugins/plugin-notification/dingtalk/index.ts index b63ebf4ce..8bba66f27 100644 --- a/packages/ui/certd-server/src/plugins/plugin-notification/dingtalk/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-notification/dingtalk/index.ts @@ -121,7 +121,7 @@ export class DingTalkNotification extends BaseNotification { at.isAtAll = true; } - const color = body.errorMessage ? "red" : "green"; + const color = body.errorMessage ? "#CC0000": "#00CC66"; const res = await this.http.request({ url: webhook, method: "POST", diff --git a/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/deploy-to-cos/index.ts b/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/deploy-to-cos/index.ts index ab84df6f2..d48fb58cd 100644 --- a/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/deploy-to-cos/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/deploy-to-cos/index.ts @@ -142,7 +142,7 @@ export class DeployCertToTencentCosPlugin extends AbstractTaskPlugin { async onGetDomainList(data: any) { const access = await this.getAccess(this.accessId); - const cosv5 = await (this as any).importRuntime("cos-nodejs-sdk-v5"); + const cosv5 = await this.importRuntime("cos-nodejs-sdk-v5"); const cos = new cosv5.default({ SecretId: access.secretId, SecretKey: access.secretKey, diff --git a/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/deploy-to-live/index.ts b/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/deploy-to-live/index.ts index 4d11fc8c2..53116a070 100644 --- a/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/deploy-to-live/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/deploy-to-live/index.ts @@ -95,7 +95,7 @@ export class TencentDeployCertToLive extends AbstractTaskPlugin { async getLiveClient() { const accessProvider = await this.getAccess(this.accessId); - const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/live/v20180801/index.js"); + const sdk = await this.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/live/v20180801/index.js"); const CssClient = sdk.v20180801.Client; const clientConfig = { diff --git a/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/upload-to-tencent/index.ts b/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/upload-to-tencent/index.ts index 03b25bc1a..314bcf573 100644 --- a/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/upload-to-tencent/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/upload-to-tencent/index.ts @@ -53,7 +53,7 @@ export class UploadCertToTencent extends AbstractTaskPlugin { Client: any; async onInstance() { - const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js"); + const sdk = await this.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js"); this.Client = sdk.v20191205.Client; } diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/access.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/access.ts index 8c1a3ae96..818b1582b 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/access.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/access.ts @@ -43,7 +43,11 @@ export class VolcengineAccess extends BaseAccess { testRequest = true; async onTestRequest() { - await this.getCallerIdentity(); + const result = await this.getCallerIdentity(); + this.ctx.logger.info("✅ 密钥有效!"); + this.ctx.logger.info(` 账户ID: ${result.accountId}`); + this.ctx.logger.info(` ARN: ${result.arn}`); + this.ctx.logger.info(` 用户ID: ${result.userId}`); return "ok"; } @@ -60,18 +64,18 @@ export class VolcengineAccess extends BaseAccess { }); const result = res.Result || {}; - this.ctx.logger.info("✅ 密钥有效!"); - this.ctx.logger.info(` 账户ID: ${result.AccountId}`); - this.ctx.logger.info(` ARN: ${result.Trn}`); - this.ctx.logger.info(` 用户ID: ${result.IdentityId}`); - return { valid: true, accountId: result.AccountId, arn: result.Trn, - userId: result.IdentityId, + userId: parseInt(result.IdentityId), }; } + + async getUserId() { + const { userId } = await this.getCallerIdentity(); + return userId; + } } new VolcengineAccess(); diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/cdn-client.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/cdn-client.ts index 3678ede0e..14740e89f 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/cdn-client.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/cdn-client.ts @@ -10,12 +10,15 @@ export class VolcengineCdnClient { this.opts = opts; } + async importRuntime(packageName: string) { + return this.opts.access.importRuntime(packageName); + } + async getCdnClient() { if (this.service) { return this.service; } - const importRuntime = this.opts.importRuntime || this.opts.access.importRuntime.bind(this.opts.access); - const { cdn } = await importRuntime("@volcengine/openapi"); + const { cdn } = await this.importRuntime("@volcengine/openapi"); const service = new cdn.CdnService(); // 设置ak、sk service.setAccessKeyId(this.opts.access.accessKeyId); diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/dns-client.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/dns-client.ts index 68cd49738..8f81c1a0a 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/dns-client.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/dns-client.ts @@ -19,10 +19,12 @@ export class VolcengineDnsClient { constructor(opts: VolcengineOpts) { this.opts = opts; } + async importRuntime(packageName: string) { + return this.opts.access.importRuntime(packageName); + } async doRequest(req: VolcengineReq) { - const importRuntime = this.opts.importRuntime || this.opts.access.importRuntime.bind(this.opts.access); - const { Signer } = await importRuntime("@volcengine/openapi"); + const { Signer } = await this.importRuntime("@volcengine/openapi"); // http request data const openApiRequestData: any = { diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.test.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.test.ts index 32bd28a07..eadf82872 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.test.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.test.ts @@ -19,16 +19,23 @@ describe("VolcengineDeployToVKE", () => { plugin.kubeconfigType = "Public"; plugin.logger = { info: () => undefined } as any; - let requestBody: any; - const kubeconfigId = await (plugin as any).createKubeconfig({ + let createRequestBody: any; + const kubeconfig = ["apiVersion: v1", "clusters:", "- cluster:", " server: https://example.com", " name: vke", "contexts: []", "current-context: vke"].join("\n"); + const vkeService = { request: async (req: any) => { - requestBody = req.body; - return { Result: { Id: "kc-123" } }; + if (req.action === "CreateKubeconfig") { + createRequestBody = req.body; + return { Result: { Id: "kc-123" } }; + } else if (req.action === "ListKubeconfigs") { + return { Result: { Items: [{ Id: "kc-123", Kubeconfig: Buffer.from(kubeconfig).toString("base64") }] } }; + } + return {}; }, - }); + }; + const kubeconfigStr = await (plugin as any).createKubeconfig({ vkeService }); - assert.equal(kubeconfigId, "kc-123"); - assert.equal(requestBody.ClusterId, "cc1234567890123456789"); + assert.equal(createRequestBody.ClusterId, "cc1234567890123456789"); + assert.equal(kubeconfigStr, kubeconfig); }); it("decodes the base64 kubeconfig returned by VKE", async () => { @@ -38,16 +45,15 @@ describe("VolcengineDeployToVKE", () => { const kubeconfig = ["apiVersion: v1", "clusters:", "- cluster:", " server: https://example.com", " name: vke", "contexts: []", "current-context: vke"].join("\n"); - const result = await (plugin as any).getKubeconfig( - { - request: async () => ({ - Result: { - Items: [{ Id: "kc-123", Kubeconfig: Buffer.from(kubeconfig).toString("base64") }], - }, - }), - }, - "kc-123" - ); + const vkeService = { + request: async () => ({ + Result: { + Items: [{ Id: "kc-123", Kubeconfig: Buffer.from(kubeconfig).toString("base64") }], + }, + }), + }; + + const result = await (plugin as any).getKubeconfig({ vkeService, kubeconfigId: "kc-123" }); assert.equal(result, kubeconfig); }); diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.ts index 7cf98c127..cc1dc61ad 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-vke.ts @@ -208,10 +208,10 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { this.logger.info("使用已有证书中心ID:" + certId); } - const kubeconfigId = await this.createKubeconfig(vkeService); - - try { - const kubeconfig = await this.getKubeconfig(vkeService, kubeconfigId); + const userId = await access.getUserId(); + this.logger.info(`当前用户ID:${userId}`); + const kubeconfig = await this.getOrCreateKubeconfig({ vkeService, userId }); + try{ const k8sClient = new this.K8sClient({ kubeConfigStr: kubeconfig, logger: this.logger, @@ -224,9 +224,7 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { throw new Error(this.formatK8sError(e.response.body)); } throw e; - } finally { - await this.deleteKubeconfig(vkeService, kubeconfigId); - } + } await utils.sleep(5000); this.logger.info("VKE证书替换完成"); @@ -250,7 +248,19 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { return await client.getVkeService({ region: this.regionId }); } - private async createKubeconfig(vkeService: any) { + async getOrCreateKubeconfig(opts: {vkeService: any, userId: number }) : Promise { + // 先查询 + let kubeconfig = await this.getKubeconfig(opts); + // 如果不存在,再创建 + if (!kubeconfig) { + return await this.createKubeconfig(opts); + } + return kubeconfig; + } + + + private async createKubeconfig(opts: {vkeService: any}) { + const {vkeService} = opts; const clusterId = this.getClusterId(); const res = await vkeService.request({ action: "CreateKubeconfig", @@ -258,7 +268,7 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { body: { ClusterId: clusterId, Type: this.kubeconfigType, - ValidDuration: 3600, + ValidDuration: 26280, //3年 }, }); const kubeconfigId = res.Result?.Id || res.Id; @@ -266,25 +276,49 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { throw new Error(`生成VKE Kubeconfig失败:${JSON.stringify(res)}`); } this.logger.info(`已生成临时Kubeconfig:${kubeconfigId}`); - return kubeconfigId; + + const config = await this.getKubeconfig({ vkeService, kubeconfigId }); + return config; } - private async getKubeconfig(vkeService: any, kubeconfigId: string) { + private async getKubeconfig(opts: {vkeService: any, kubeconfigId?: string, userId?:number }) { + const {vkeService, kubeconfigId, userId} = opts; const clusterId = this.getClusterId(); + const query: any = { + ClusterIds: [clusterId], + Types: [this.kubeconfigType], + } + if (kubeconfigId) { + query.Ids = [kubeconfigId]; + } + if (userId) { + query.UserIds = [userId]; + } const res = await vkeService.request({ action: "ListKubeconfigs", method: "POST", body: { - Filter: { - ClusterIds: [clusterId], - Ids: [kubeconfigId], - Types: [this.kubeconfigType], - }, + Filter: query, PageNumber: 1, PageSize: 10, }, }); - const items = res.Result?.Items || res.Items || []; + let items = res.Result?.Items || res.Items || []; + if (items.length === 0) { + return null; + } + const now = new Date(); + items = items.filter((it: any) => { + if (!it.ExpireTime) { + return true; + } + const expireTime = new Date(it.ExpireTime); + return expireTime > now; + }); + if (items.length === 0) { + return null; + } + const item = items.find((it: any) => it.Id === kubeconfigId) || items[0]; const kubeconfig = item?.Kubeconfig; if (!kubeconfig) { @@ -293,25 +327,25 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { return this.decodeKubeconfig(kubeconfig); } - private async deleteKubeconfig(vkeService: any, kubeconfigId?: string) { - if (!kubeconfigId) { - return; - } - const clusterId = this.getClusterId(); - try { - await vkeService.request({ - action: "DeleteKubeconfigs", - method: "POST", - body: { - ClusterId: clusterId, - Ids: [kubeconfigId], - }, - }); - this.logger.info(`已删除临时Kubeconfig:${kubeconfigId}`); - } catch (e) { - this.logger.warn(`删除临时Kubeconfig失败:${e.message || e}`); - } - } + // private async deleteKubeconfig(vkeService: any, kubeconfigId?: string) { + // if (!kubeconfigId) { + // return; + // } + // const clusterId = this.getClusterId(); + // try { + // await vkeService.request({ + // action: "DeleteKubeconfigs", + // method: "POST", + // body: { + // ClusterId: clusterId, + // Ids: [kubeconfigId], + // }, + // }); + // this.logger.info(`已删除临时Kubeconfig:${kubeconfigId}`); + // } catch (e) { + // this.logger.warn(`删除临时Kubeconfig失败:${e.message || e}`); + // } + // } private getClusterId() { if (!this.clusterId) { @@ -476,24 +510,19 @@ export class VolcengineDeployToVKE extends AbstractPlusTaskPlugin { } const access = await this.getAccess(this.accessId); const vkeService = await this.getVkeService(access); - const kubeconfigId = await this.createKubeconfig(vkeService); - - try { - const kubeconfig = await this.getKubeconfig(vkeService, kubeconfigId); - const k8sClient = new this.K8sClient({ - kubeConfigStr: kubeconfig, - logger: this.logger, - skipTLSVerify: this.skipTLSVerify, - }); - const res = await k8sClient.getSecrets({ namespace: this.namespace || "default" }); - const list = res.body?.items || res.items || []; - return list.map((item: any) => ({ - label: item.metadata.name, - value: item.metadata.name, - })); - } finally { - await this.deleteKubeconfig(vkeService, kubeconfigId); - } + const userId = await access.getUserId(); + const kubeconfig = await this.getOrCreateKubeconfig({ vkeService, userId }); + const k8sClient = new this.K8sClient({ + kubeConfigStr: kubeconfig, + logger: this.logger, + skipTLSVerify: this.skipTLSVerify, + }); + const res = await k8sClient.getSecrets({ namespace: this.namespace || "default" }); + const list = res.body?.items || res.items || []; + return list.map((item: any) => ({ + label: item.metadata.name, + value: item.metadata.name, + })); } } diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/ve-client.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/ve-client.ts index 2bd2725ff..48f3b7490 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/ve-client.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/ve-client.ts @@ -1,12 +1,10 @@ import { VolcengineAccess } from "./access.js"; import { HttpClient, ILogger } from "@certd/basic"; -import { ImportRuntime } from "@certd/pipeline"; export type VolcengineOpts = { access: VolcengineAccess; logger: ILogger; http: HttpClient; - importRuntime?: ImportRuntime; }; export class VolcengineClient { @@ -17,6 +15,10 @@ export class VolcengineClient { this.opts = opts; } + async importRuntime(packageName: string) { + return this.opts.access.importRuntime(packageName); + } + async getCertCenterService() { const CommonService = await this.getServiceCls(); @@ -142,8 +144,7 @@ export class VolcengineClient { } async getTOSService(opts: { region?: string }) { - const importRuntime = this.opts.importRuntime || this.opts.access.importRuntime.bind(this.opts.access); - const { TosClient } = await importRuntime("@volcengine/tos-sdk"); + const { TosClient } = await this.importRuntime("@volcengine/tos-sdk"); const client = new TosClient({ accessKeyId: this.opts.access.accessKeyId, @@ -172,8 +173,7 @@ export class VolcengineClient { if (this.CommonService) { return this.CommonService; } - const importRuntime = this.opts.importRuntime || this.opts.access.importRuntime.bind(this.opts.access); - const { Service } = await importRuntime("@volcengine/openapi"); + const { Service } = await this.importRuntime("@volcengine/openapi"); class CommonService extends Service { Generic: any; diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/volcengine-dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/volcengine-dns-provider.ts index 762a74ae8..3bf5b35ee 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/volcengine-dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/volcengine-dns-provider.ts @@ -22,7 +22,6 @@ export class VolcengineDnsProvider extends AbstractDnsProvider { access: this.access, logger: this.logger, http: this.http, - importRuntime: this.importRuntime.bind(this), }); } diff --git a/trigger/build.trigger b/trigger/build.trigger index f8e0bc578..f1d0cff04 100644 --- a/trigger/build.trigger +++ b/trigger/build.trigger @@ -1 +1 @@ -02:48 +23:35 diff --git a/trigger/release.trigger b/trigger/release.trigger index 4c82fa5e8..6a639182c 100644 --- a/trigger/release.trigger +++ b/trigger/release.trigger @@ -1 +1 @@ -03:41 +23:54