Compare commits

..

9 Commits

Author SHA1 Message Date
xiaojunnuo 6cc74a1c0a chore: lint 2026-07-15 01:25:55 +08:00
xiaojunnuo 167b303fae fix: 修复上传到cos报runtimeDepsService未初始化的问题 2026-07-15 01:09:57 +08:00
xiaojunnuo b91c9e4ea6 perf: 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim 2026-07-14 21:32:32 +08:00
xiaojunnuo 584b7b6f45 build: release 2026-07-12 03:41:59 +08:00
xiaojunnuo 2312c444ef build: release 2026-07-12 03:36:09 +08:00
xiaojunnuo 0373d019f8 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-12 03:11:04 +08:00
xiaojunnuo fe09e75b80 refactor(nginx-proxy-manager): 替换直接动态导入为运行时依赖导入
将插件中直接使用的动态导入改为通过runtimeDepsService统一处理,同时重构导入方式为runtimeImport变量,移除冗余的form-data和otplib直接导入,统一导入逻辑
2026-07-12 03:10:52 +08:00
xiaojunnuo 432c9c6cc1 build: publish 2026-07-12 02:48:34 +08:00
xiaojunnuo 5fc1d55879 build: trigger build image 2026-07-12 02:48:22 +08:00
69 changed files with 516 additions and 1559 deletions
+1
View File
@@ -0,0 +1 @@

+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08) ## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
### Bug Fixes ### Bug Fixes
+1
View File
@@ -39,6 +39,7 @@
"test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit", "test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit",
"pub": "echo 1", "pub": "echo 1",
"dev": "pnpm run -r --parallel compile ", "dev": "pnpm run -r --parallel compile ",
"lint_all": "pnpm run -r --parallel lint ",
"pub_all": "node ./scripts/pub-all.js", "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", "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", "publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
+1 -1
View File
@@ -75,5 +75,5 @@
"bugs": { "bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues" "url": "https://github.com/publishlab/node-acme-client/issues"
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
+1 -1
View File
@@ -54,5 +54,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
+1 -1
View File
@@ -51,5 +51,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
+2 -9
View File
@@ -3,7 +3,7 @@ import { FormItemProps } from "../dt/index.js";
import { HttpClient, ILogger, utils } from "@certd/basic"; import { HttpClient, ILogger, utils } from "@certd/basic";
import * as _ from "lodash-es"; import * as _ from "lodash-es";
import { PluginRequestHandleReq } from "../plugin/index.js"; import { PluginRequestHandleReq } from "../plugin/index.js";
import { IRuntimeDepsService, IServiceGetter } from "../service/index.js"; import { IServiceGetter, getRuntimeDepsService } from "../service/index.js";
// export type AccessRequestHandleReqInput<T = any> = { // export type AccessRequestHandleReqInput<T = any> = {
// id?: number; // id?: number;
@@ -48,20 +48,13 @@ export type AccessContext = {
export abstract class BaseAccess implements IAccess { export abstract class BaseAccess implements IAccess {
ctx!: AccessContext; ctx!: AccessContext;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) { async importRuntime(specifier: string) {
if (!this.runtimeDepsService) { return await getRuntimeDepsService().importRuntime(specifier, this.ctx.logger);
throw new Error("runtimeDepsService 未初始化");
}
return await this.runtimeDepsService.importRuntime(specifier, this.ctx.logger);
} }
async setCtx(ctx: AccessContext) { async setCtx(ctx: AccessContext) {
this.ctx = ctx; this.ctx = ctx;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
} }
async onRequest(req: AccessRequestHandleReq) { async onRequest(req: AccessRequestHandleReq) {
@@ -3,7 +3,7 @@ import { Registrable } from "../registry/index.js";
import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js"; import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js";
import { HttpClient, ILogger, utils } from "@certd/basic"; import { HttpClient, ILogger, utils } from "@certd/basic";
import * as _ from "lodash-es"; import * as _ from "lodash-es";
import { IEmailService, IRuntimeDepsService, IServiceGetter } from "../service/index.js"; import { IEmailService, IServiceGetter, getRuntimeDepsService } from "../service/index.js";
export type NotificationBody = { export type NotificationBody = {
userId?: number; userId?: number;
@@ -89,13 +89,9 @@ export abstract class BaseNotification implements INotification {
ctx!: NotificationContext; ctx!: NotificationContext;
http!: HttpClient; http!: HttpClient;
logger!: ILogger; logger!: ILogger;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) { async importRuntime(specifier: string) {
if (!this.runtimeDepsService) { return await getRuntimeDepsService().importRuntime(specifier, this.logger);
return await import(specifier);
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
} }
async doSend(body: NotificationBody) { async doSend(body: NotificationBody) {
@@ -109,9 +105,6 @@ export abstract class BaseNotification implements INotification {
this.ctx = ctx; this.ctx = ctx;
this.http = ctx.http; this.http = ctx.http;
this.logger = ctx.logger; this.logger = ctx.logger;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
} }
setDefine = (define: NotificationDefine) => { setDefine = (define: NotificationDefine) => {
this.define = define; this.define = define;
+3 -10
View File
@@ -10,7 +10,7 @@ import { INotificationService } from "../notification/index.js";
import { Registrable } from "../registry/index.js"; import { Registrable } from "../registry/index.js";
import { IPluginConfigService } from "../service/config.js"; import { IPluginConfigService } from "../service/config.js";
import { TaskEmitter } from "../service/emit.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<T = any> = { export type PluginRequestHandleReq<T = any> = {
typeName: string; typeName: string;
@@ -76,7 +76,7 @@ export type ITaskPlugin = {
execute(): Promise<void | string>; execute(): Promise<void | string>;
onRequest(req: PluginRequestHandleReq<any>): Promise<any>; onRequest(req: PluginRequestHandleReq<any>): Promise<any>;
setCtx(ctx: TaskInstanceContext): Promise<void>; setCtx(ctx: TaskInstanceContext): Promise<void>;
importRuntime?(specifier: string): Promise<any>; importRuntime(specifier: string): Promise<any>;
[key: string]: any; [key: string]: any;
}; };
@@ -150,13 +150,9 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
logger!: ILogger; logger!: ILogger;
http!: HttpClient; http!: HttpClient;
accessService!: IAccessService; accessService!: IAccessService;
runtimeDepsService!: IRuntimeDepsService;
async importRuntime(specifier: string) { async importRuntime(specifier: string) {
if (!this.runtimeDepsService) { return await getRuntimeDepsService().importRuntime(specifier, this.logger);
throw new Error("runtimeDepsService 未初始化");
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
} }
clearLastStatus() { clearLastStatus() {
@@ -178,9 +174,6 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
this.logger = ctx.logger; this.logger = ctx.logger;
this.accessService = ctx.accessService; this.accessService = ctx.accessService;
this.http = ctx.http; this.http = ctx.http;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
// 将证书加入secret // 将证书加入secret
// @ts-ignore // @ts-ignore
if (this.cert && this.cert.crt && this.cert.key) { if (this.cert && this.cert.crt && this.cert.key) {
@@ -1,41 +1,31 @@
import assert from "assert"; import assert from "assert";
import { NpmRegistryResolver } from "./npm-registry-resolver.js"; import { NpmRegistryResolver } from "./runtime-deps-service.js";
describe("NpmRegistryResolver", () => { describe("NpmRegistryResolver", () => {
it("chooses the fastest successful registry in auto mode", async () => { it("chooses the fastest successful registry in auto mode", async () => {
const resolver = new NpmRegistryResolver(); const resolver = new NpmRegistryResolver({
resolver.config = {
mode: "auto", mode: "auto",
fixedUrl: "",
candidates: ["https://slow.example.com", "https://fast.example.com"], candidates: ["https://slow.example.com", "https://fast.example.com"],
probeTimeoutMs: 100, probeTimeoutMs: 100,
cacheTtlMs: 1000, cacheTtlMs: 1000,
}; });
resolver.probe = async registryUrl => { resolver.probe = async registryUrl => ({
return {
registryUrl, registryUrl,
ok: true, ok: true,
elapsedMs: registryUrl.includes("fast") ? 10 : 50, elapsedMs: registryUrl.includes("fast") ? 10 : 50,
}; });
};
const result = await resolver.resolve(); const result = await resolver.resolve();
assert.equal(result, "https://fast.example.com"); assert.equal(result, "https://fast.example.com");
}); });
it("uses fixed registry without probing", async () => { it("uses fixed registry without probing", async () => {
const resolver = new NpmRegistryResolver(); const resolver = new NpmRegistryResolver({
resolver.config = {
mode: "fixed", mode: "fixed",
fixedUrl: "https://registry.example.com", fixedUrl: "https://registry.example.com",
candidates: [],
probeTimeoutMs: 100, probeTimeoutMs: 100,
cacheTtlMs: 1000, cacheTtlMs: 1000,
}; });
const result = await resolver.resolve(); const result = await resolver.resolve();
assert.equal(result, "https://registry.example.com"); assert.equal(result, "https://registry.example.com");
}); });
}); });
@@ -1,10 +1,10 @@
import assert from "assert"; import assert from "assert";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import os from "os"; import os from "os";
import { RuntimeDepsService, type RuntimeDependencyPluginDefine } from "./runtime-deps-service.js"; import { RuntimeDepsService, type RuntimeDependencyPluginDefine } from "./runtime-deps-service.js";
import { accessRegistry, pluginRegistry } from "@certd/pipeline"; import { accessRegistry } from "../access/registry.js";
import { addonRegistry } from "@certd/lib-server"; import { pluginRegistry } from "../plugin/registry.js";
describe("RuntimeDepsService", () => { describe("RuntimeDepsService", () => {
it("detects conflicting dependency ranges across plugins", () => { it("detects conflicting dependency ranges across plugins", () => {
@@ -13,7 +13,6 @@ describe("RuntimeDepsService", () => {
{ name: "a", dependPackages: { foo: "^1.0.0" } }, { name: "a", dependPackages: { foo: "^1.0.0" } },
{ name: "b", dependPackages: { foo: "^1.2.0" } }, { name: "b", dependPackages: { foo: "^1.2.0" } },
]); ]);
assert.deepEqual(merged.dependencies, { foo: "^1.0.0" }); assert.deepEqual(merged.dependencies, { foo: "^1.0.0" });
assert.equal(merged.conflicts.length, 0); assert.equal(merged.conflicts.length, 0);
}); });
@@ -24,62 +23,45 @@ describe("RuntimeDepsService", () => {
{ name: "a", dependPackages: { foo: "^1.0.0" } }, { name: "a", dependPackages: { foo: "^1.0.0" } },
{ name: "b", dependPackages: { foo: "^2.0.0" } }, { name: "b", dependPackages: { foo: "^2.0.0" } },
]); ]);
assert.equal(merged.conflicts.length, 1); assert.equal(merged.conflicts.length, 1);
assert.equal(merged.conflicts[0].packageName, "foo"); assert.equal(merged.conflicts[0].packageName, "foo");
}); });
it("builds a runtime package manifest in the target directory", async () => { it("builds a runtime package manifest in the target directory", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir });
service.runtimeDepsRootDir = rootDir;
service.registryResolver = { service.registryResolver = {
async resolve() { async resolve() { return "https://registry.npmmirror.com"; },
return "https://registry.npmmirror.com";
},
} as any; } as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
assert.equal(command, "pnpm"); assert.equal(command, "pnpm");
if (args.includes("--version")) { if (args.includes("--version")) { return { stdout: "9.1.0\n", stderr: "", code: 0 }; }
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
assert.equal(args[0], "install"); assert.equal(args[0], "install");
assert.ok(args.includes("--ignore-workspace")); assert.ok(args.includes("--ignore-workspace"));
assert.ok(args.includes("--no-frozen-lockfile")); assert.ok(args.includes("--no-frozen-lockfile"));
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
const plugins: RuntimeDependencyPluginDefine[] = [{ name: "a", dependPackages: { foo: "^1.0.0" } }]; const plugins: RuntimeDependencyPluginDefine[] = [{ name: "a", dependPackages: { foo: "^1.0.0" } }];
const result = await service.ensureInstalled({ plugins }); const result = await service.ensureInstalled({ plugins });
assert.equal(result.registryUrl, "https://registry.npmmirror.com"); assert.equal(result.registryUrl, "https://registry.npmmirror.com");
assert.ok(fs.existsSync(path.join(rootDir, "package.json"))); assert.ok(fs.existsSync(path.join(rootDir, "package.json")));
}); });
it("installs direct dependency maps without plugin metadata", async () => { it("installs direct dependency maps without plugin metadata", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-direct-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-direct-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir });
service.runtimeDepsRootDir = rootDir; service.registryResolver = { async resolve() { return ""; } } as any;
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
assert.equal(command, "pnpm"); assert.equal(command, "pnpm");
if (args.includes("--version")) { if (args.includes("--version")) { return { stdout: "9.1.0\n", stderr: "", code: 0 }; }
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true }); fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
await service.ensureDependencies({ dependencies: { directPkg: "^1.0.0" } }); await service.ensureDependencies({ dependencies: { directPkg: "^1.0.0" } });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { directPkg: "^1.0.0" }); assert.deepEqual(manifest.dependencies, { directPkg: "^1.0.0" });
}); });
@@ -91,38 +73,20 @@ describe("RuntimeDepsService", () => {
fs.writeFileSync(path.join(rootDir, "package.json"), JSON.stringify({ name: "runtime-root", type: "module" }), "utf8"); 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, "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"); fs.writeFileSync(path.join(packageDir, "index.js"), "export const value = 42;\n", "utf8");
const service = new RuntimeDepsService({ rootDir });
const service = new RuntimeDepsService(); service.commandRunner = { async run() { throw new Error("install should not run"); } } as any;
service.runtimeDepsRootDir = rootDir;
service.commandRunner = {
async run() {
throw new Error("install should not run");
},
} as any;
const mod = await service.importRuntime("runtime-only"); const mod = await service.importRuntime("runtime-only");
assert.equal(mod.value, 42); assert.equal(mod.value, 42);
}); });
it("installs configured lazy dependency when import target is missing", async () => { it("installs configured lazy dependency when import target is missing", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "lazy-pkg": "^1.2.3" } });
service.runtimeDepsRootDir = rootDir; service.registryResolver = { async resolve() { return ""; } } as any;
service.lazyDependencies = {
"lazy-pkg": "^1.2.3",
};
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
assert.equal(command, "pnpm"); assert.equal(command, "pnpm");
if (args.includes("--version")) { if (args.includes("--version")) { return { stdout: "9.1.0\n", stderr: "", code: 0 }; }
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
const packageDir = path.join(rootDir, "node_modules", "lazy-pkg", "sub"); const packageDir = path.join(rootDir, "node_modules", "lazy-pkg", "sub");
fs.mkdirSync(packageDir, { recursive: true }); fs.mkdirSync(packageDir, { recursive: true });
fs.writeFileSync(path.join(rootDir, "node_modules", "lazy-pkg", "package.json"), JSON.stringify({ name: "lazy-pkg", type: "module" }), "utf8"); fs.writeFileSync(path.join(rootDir, "node_modules", "lazy-pkg", "package.json"), JSON.stringify({ name: "lazy-pkg", type: "module" }), "utf8");
@@ -130,9 +94,7 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
const mod = await service.importRuntime("lazy-pkg/sub/entry.js"); const mod = await service.importRuntime("lazy-pkg/sub/entry.js");
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { "lazy-pkg": "^1.2.3" }); assert.deepEqual(manifest.dependencies, { "lazy-pkg": "^1.2.3" });
assert.equal(mod.value, 7); assert.equal(mod.value, 7);
@@ -140,22 +102,12 @@ describe("RuntimeDepsService", () => {
it("resolves scoped package names for lazy imports", async () => { it("resolves scoped package names for lazy imports", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "@scope/lazy": "^2.0.0" } });
service.runtimeDepsRootDir = rootDir; service.registryResolver = { async resolve() { return ""; } } as any;
service.lazyDependencies = {
"@scope/lazy": "^2.0.0",
};
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
assert.equal(command, "pnpm"); assert.equal(command, "pnpm");
if (args.includes("--version")) { if (args.includes("--version")) { return { stdout: "9.1.0\n", stderr: "", code: 0 }; }
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
const packageDir = path.join(rootDir, "node_modules", "@scope", "lazy", "dist"); const packageDir = path.join(rootDir, "node_modules", "@scope", "lazy", "dist");
fs.mkdirSync(packageDir, { recursive: true }); fs.mkdirSync(packageDir, { recursive: true });
fs.writeFileSync(path.join(rootDir, "node_modules", "@scope", "lazy", "package.json"), JSON.stringify({ name: "@scope/lazy", type: "module" }), "utf8"); fs.writeFileSync(path.join(rootDir, "node_modules", "@scope", "lazy", "package.json"), JSON.stringify({ name: "@scope/lazy", type: "module" }), "utf8");
@@ -163,9 +115,7 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
const mod = await service.importRuntime("@scope/lazy/dist/index.js"); const mod = await service.importRuntime("@scope/lazy/dist/index.js");
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { "@scope/lazy": "^2.0.0" }); assert.deepEqual(manifest.dependencies, { "@scope/lazy": "^2.0.0" });
assert.equal(mod.scoped, true); assert.equal(mod.scoped, true);
@@ -173,82 +123,36 @@ describe("RuntimeDepsService", () => {
it("reports missing lazy dependency configuration", async () => { it("reports missing lazy dependency configuration", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-missing-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-missing-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} });
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {};
await assert.rejects(() => service.importRuntime("missing-pkg/sub.js"), /未配置懒加载版本: missing-pkg/); 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 () => { 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 rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} });
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {};
const mod = await service.importRuntime("dayjs"); 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"); assert.equal(typeof mod.default, "function");
}); });
it("keeps previously installed dependencies when installing a later plugin", async () => { it("keeps previously installed dependencies when installing a later plugin", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir });
service.runtimeDepsRootDir = rootDir; service.registryResolver = { async resolve() { return ""; } } as any;
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
assert.equal(command, "pnpm"); assert.equal(command, "pnpm");
if (args.includes("--version")) { if (args.includes("--version")) { return { stdout: "9.1.0\n", stderr: "", code: 0 }; }
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true }); fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
await service.ensureInstalled({ plugins: [{ name: "a", pluginType: "deploy", dependPackages: { foo: "^1.0.0" } }] }); 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" } }] }); 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")); const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { assert.deepEqual(manifest.dependencies, { foo: "^1.0.0", bar: "^2.0.0" });
foo: "^1.0.0",
bar: "^2.0.0",
});
}); });
it("includes npm dependencies from dependent plugins", () => { it("includes npm dependencies from dependent plugins", async () => {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService();
accessRegistry.register("runtimeDepsAccess", { accessRegistry.register("runtimeDepsAccess", {
define: { name: "runtimeDepsAccess", title: "access", dependPackages: { accessOnly: "^1.0.0" } } as any, define: { name: "runtimeDepsAccess", title: "access", dependPackages: { accessOnly: "^1.0.0" } } as any,
@@ -256,17 +160,11 @@ describe("RuntimeDepsService", () => {
}); });
try { try {
const resolved = service.resolvePluginDependencies({ const resolved = service.resolvePluginDependencies({
name: "deploy", name: "deploy", pluginType: "deploy", dependPlugins: { "access:runtimeDepsAccess": "*" },
pluginType: "deploy",
dependPlugins: { "access:runtimeDepsAccess": "*" },
dependPackages: { deployOnly: "^1.0.0" }, dependPackages: { deployOnly: "^1.0.0" },
}); });
const merged = service.collectDependencies(resolved); 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 { } finally {
accessRegistry.unRegister("runtimeDepsAccess"); accessRegistry.unRegister("runtimeDepsAccess");
} }
@@ -274,19 +172,12 @@ describe("RuntimeDepsService", () => {
it("installs dependencies by registered plugin key", async () => { it("installs dependencies by registered plugin key", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-key-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-key-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir });
service.runtimeDepsRootDir = rootDir; service.registryResolver = { async resolve() { return ""; } } as any;
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
assert.equal(command, "pnpm"); assert.equal(command, "pnpm");
if (args.includes("--version")) { if (args.includes("--version")) { return { stdout: "9.1.0\n", stderr: "", code: 0 }; }
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true }); fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
@@ -296,8 +187,8 @@ describe("RuntimeDepsService", () => {
target: async () => ({} as any), target: async () => ({} as any),
}); });
try { try {
service.setRegistries({ pluginRegistry, accessRegistry });
await service.ensureRuntimeDependencies({ pluginKeys: "plugin:runtimeDepsKey" }); await service.ensureRuntimeDependencies({ pluginKeys: "plugin:runtimeDepsKey" });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { keyed: "^1.0.0" }); assert.deepEqual(manifest.dependencies, { keyed: "^1.0.0" });
} finally { } finally {
@@ -305,50 +196,8 @@ 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", () => { it("reports missing dependent plugins", () => {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService();
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { "access:access": "*" } }), /插件依赖缺失/); assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { "access:access": "*" } }), /插件依赖缺失/);
}); });
@@ -360,10 +209,8 @@ describe("RuntimeDepsService", () => {
}); });
try { try {
assert.throws( assert.throws(
() => () => service.resolvePluginDependencies({
service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy",
name: "deploy",
pluginType: "deploy",
dependPlugins: { "access:runtimeDepsVersionedAccess": "^2.0.0" }, dependPlugins: { "access:runtimeDepsVersionedAccess": "^2.0.0" },
}), }),
/插件依赖版本冲突/ /插件依赖版本冲突/
@@ -375,72 +222,9 @@ describe("RuntimeDepsService", () => {
it("reports bare dependent plugin names as invalid format", () => { it("reports bare dependent plugin names as invalid format", () => {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService();
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { runtimeDepsBareName: "*" } }), /插件依赖格式错误/); 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 () => { 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 rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-env-"));
const oldNodeOptions = process.env.NODE_OPTIONS; const oldNodeOptions = process.env.NODE_OPTIONS;
@@ -448,62 +232,30 @@ describe("RuntimeDepsService", () => {
process.env.NODE_OPTIONS = "--inspect=127.0.0.1:9229 --max-old-space-size=4096"; process.env.NODE_OPTIONS = "--inspect=127.0.0.1:9229 --max-old-space-size=4096";
process.env.VSCODE_INSPECTOR_OPTIONS = '{"inspectorIpc":"test"}'; process.env.VSCODE_INSPECTOR_OPTIONS = '{"inspectorIpc":"test"}';
try { try {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir });
service.runtimeDepsRootDir = rootDir; service.registryResolver = { async resolve() { return ""; } } as any;
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[], options: { env?: NodeJS.ProcessEnv }) { async run(command: string, args: string[], options: { env?: NodeJS.ProcessEnv }) {
assert.equal(options.env?.NODE_OPTIONS, "--max-old-space-size=4096"); assert.equal(options.env?.NODE_OPTIONS, "--max-old-space-size=4096");
assert.equal(options.env?.VSCODE_INSPECTOR_OPTIONS, undefined); assert.equal(options.env?.VSCODE_INSPECTOR_OPTIONS, undefined);
assert.equal(options.env?.CI, "true"); assert.equal(options.env?.CI, "true");
assert.equal(options.env?.pnpm_config_confirm_modules_purge, "false"); assert.equal(options.env?.pnpm_config_confirm_modules_purge, "false");
if (args.includes("--version")) { if (args.includes("--version")) { return { stdout: "9.1.0\n", stderr: "", code: 0 }; }
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] }); await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] });
} finally { } finally {
if (oldNodeOptions == null) { if (oldNodeOptions == null) { delete process.env.NODE_OPTIONS; }
delete process.env.NODE_OPTIONS; else { process.env.NODE_OPTIONS = oldNodeOptions; }
} else { if (oldInspectorOptions == null) { delete process.env.VSCODE_INSPECTOR_OPTIONS; }
process.env.NODE_OPTIONS = oldNodeOptions; else { process.env.VSCODE_INSPECTOR_OPTIONS = oldInspectorOptions; }
} }
if (oldInspectorOptions == null) {
delete process.env.VSCODE_INSPECTOR_OPTIONS;
} else {
process.env.VSCODE_INSPECTOR_OPTIONS = oldInspectorOptions;
}
}
});
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 () => { it("rejects clearing unexpected runtime dependency path", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir });
service.runtimeDepsRootDir = rootDir;
await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/); await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/);
}); });
}); });
@@ -1,15 +1,28 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { spawn } from "child_process"; import { spawn } from "child_process";
import crypto from "crypto"; import crypto from "crypto";
import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { createRequire } from "module"; import { createRequire } from "module";
import { pathToFileURL } from "url"; import { pathToFileURL } from "url";
import { NpmRegistryResolver } from "./npm-registry-resolver.js"; import { logger as defaultLogger } from "@certd/basic";
import { Registry, accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline"; import type { Registry } from "../registry/registry.js";
import { dnsProviderRegistry } from "@certd/plugin-lib"; export type ILogger = {
import { addonRegistry } from "@certd/lib-server"; info: (message: string) => void;
import { logger, ILogger } from "@certd/basic"; warn?: (message: string) => void;
error?: (message: string, ...args: any[]) => void;
};
export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise<any>;
export type EnsureRuntimeDepsOptions = {
pluginKeys: string | string[];
logger?: ILogger;
};
export interface IRuntimeDepsService {
ensureRuntimeDependencies(options: EnsureRuntimeDepsOptions): Promise<any>;
importRuntime: ImportRuntime;
}
export type RuntimeDependencyPluginDefine = { export type RuntimeDependencyPluginDefine = {
name: string; name: string;
@@ -30,22 +43,6 @@ type RegisteredDefineLike = RuntimeDependencyPluginDefine & {
dependPackages?: Record<string, string>; dependPackages?: Record<string, string>;
}; };
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 = { type DependencyConflict = {
packageName: string; packageName: string;
ranges: Array<{ pluginName: string; range: string }>; ranges: Array<{ pluginName: string; range: string }>;
@@ -73,27 +70,111 @@ type CommandRunnerResult = {
}; };
type CommandRunner = { type CommandRunner = {
// @ts-ignore
run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult>; run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult>;
}; };
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<string> {
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<RegistryProbeResult> {
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<string, string>;
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<string, Promise<unknown>>(); const PROCESS_LOCKS = new Map<string, Promise<unknown>>();
class DefaultCommandRunner implements CommandRunner { class DefaultCommandRunner implements CommandRunner {
// @ts-ignore
async run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult> { async run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult> {
return await new Promise<CommandRunnerResult>(resolve => { return await new Promise<CommandRunnerResult>(resolve => {
let stdout = ""; let stdout = "";
let stderr = ""; let stderr = "";
let settled = false; let settled = false;
const child = spawn(command, args, { const child = spawn(command, args, { cwd: options.cwd, env: options.env, windowsHide: true, shell: process.platform === "win32" });
cwd: options.cwd,
env: options.env,
windowsHide: true,
// @ts-ignore
shell: process.platform === "win32",
});
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (settled) { if (settled) {
return; return;
@@ -102,7 +183,6 @@ class DefaultCommandRunner implements CommandRunner {
child.kill("SIGTERM"); child.kill("SIGTERM");
resolve({ stdout, stderr: stderr || `command timeout after ${options.timeoutMs}ms`, code: 1 }); resolve({ stdout, stderr: stderr || `command timeout after ${options.timeoutMs}ms`, code: 1 });
}, options.timeoutMs); }, options.timeoutMs);
child.stdout?.on("data", chunk => { child.stdout?.on("data", chunk => {
stdout += chunk.toString(); stdout += chunk.toString();
}); });
@@ -129,43 +209,55 @@ class DefaultCommandRunner implements CommandRunner {
} }
} }
@Provide()
@Scope(ScopeEnum.Singleton)
export class RuntimeDepsService { export class RuntimeDepsService {
@Config("runtimeDeps.rootDir") runtimeDepsRootDir: string;
runtimeDepsRootDir = "./data/.runtime-deps"; autoInstall: boolean;
enabled: boolean;
@Config("runtimeDeps.autoInstall") installTimeoutMs: number;
autoInstall = true; pnpmCommand: string;
lazyDependencies: Record<string, string>;
@Config("runtimeDeps.enabled")
enabled = true;
@Config("runtimeDeps.installTimeoutMs")
installTimeoutMs = 120000;
@Config("runtimeDeps.pnpmCommand")
pnpmCommand = "";
@Config("runtimeDeps.lazyDependencies")
lazyDependencies: Record<string, string> = {};
/**
* registry
*/
pluginLazyDependencies: Record<string, string> = {};
@Inject()
registryResolver!: NpmRegistryResolver; registryResolver!: NpmRegistryResolver;
commandRunner: CommandRunner = new DefaultCommandRunner(); commandRunner: CommandRunner = new DefaultCommandRunner();
pluginLazyDependencies: Record<string, string> = {};
private installPromises = new Map<string, Promise<InstallResult>>(); private installPromises = new Map<string, Promise<InstallResult>>();
private registriesMap: Record<string, { registry: Registry<any>; 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<any>; accessRegistry?: Registry<any>; notificationRegistry?: Registry<any>; dnsProviderRegistry?: Registry<any>; addonRegistry?: Registry<any> }) {
const map: Record<string, { registry: Registry<any>; 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 { collectDependencies(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
const merged: Record<string, string> = {}; const merged: Record<string, string> = {};
const seen: Record<string, Array<{ pluginName: string; range: string }>> = {}; const seen: Record<string, Array<{ pluginName: string; range: string }>> = {};
for (const plugin of plugins) { for (const plugin of plugins) {
const deps = plugin.dependPackages || {}; const deps = plugin.dependPackages || {};
for (const [packageName, range] of Object.entries(deps)) { for (const [packageName, range] of Object.entries(deps)) {
@@ -173,7 +265,6 @@ export class RuntimeDepsService {
seen[packageName].push({ pluginName: plugin.name, range }); seen[packageName].push({ pluginName: plugin.name, range });
} }
} }
const conflicts: DependencyConflict[] = []; const conflicts: DependencyConflict[] = [];
for (const [packageName, ranges] of Object.entries(seen)) { for (const [packageName, ranges] of Object.entries(seen)) {
const first = ranges[0]?.range; const first = ranges[0]?.range;
@@ -187,7 +278,6 @@ export class RuntimeDepsService {
} }
merged[packageName] = first; merged[packageName] = first;
} }
return { dependencies: merged, conflicts }; return { dependencies: merged, conflicts };
} }
@@ -204,16 +294,10 @@ export class RuntimeDepsService {
async ensureDependencies(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> { async ensureDependencies(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
const { dependencies, logger: log } = options; const { dependencies, logger: log } = options;
if (!this.enabled) { if (!this.enabled) {
return { return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
registryUrl: "",
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
};
} }
if (!this.autoInstall) { if (!this.autoInstall) {
return { return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
registryUrl: "",
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
};
} }
const dependenciesHash = this.createDependenciesHash(dependencies); const dependenciesHash = this.createDependenciesHash(dependencies);
let installPromise = this.installPromises.get(dependenciesHash); let installPromise = this.installPromises.get(dependenciesHash);
@@ -244,94 +328,21 @@ export class RuntimeDepsService {
const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys]; const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys];
const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey)); const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey));
if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) { if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) {
return { return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
registryUrl: "",
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
};
} }
const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine)); const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine));
return await this.ensureInstalled({ plugins: expandedPluginDefines, logger: log }); return await this.ensureInstalled({ plugins: expandedPluginDefines, logger: log });
} }
private async doEnsureInstalled(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> { async importRuntime(specifier: string, logger: ILogger = defaultLogger) {
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)) { if (this.isNativeImportSpecifier(specifier)) {
return await import(specifier); return await import(specifier);
} }
const resolved = await this.resolveImportSpecifier(specifier, logger); const resolved = await this.resolveImportSpecifier(specifier, logger);
return await import(pathToFileURL(resolved).href); return await import(pathToFileURL(resolved).href);
} }
private async resolveImportSpecifier(specifier: string, logger?: ILogger) { private async resolveImportSpecifier(specifier: string, logger: ILogger = defaultLogger) {
try { try {
return this.resolveRuntimeSpecifier(specifier).resolved; return this.resolveRuntimeSpecifier(specifier).resolved;
} catch (runtimeError: any) { } catch (runtimeError: any) {
@@ -410,10 +421,7 @@ export class RuntimeDepsService {
if (!range) { if (!range) {
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`); throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
} }
const dependencies = { await this.ensureDependencies({ dependencies: { [packageName]: range }, logger });
[packageName]: range,
};
await this.ensureDependencies({ dependencies, logger });
} }
private isModuleNotFoundError(error: any) { private isModuleNotFoundError(error: any) {
@@ -423,7 +431,6 @@ export class RuntimeDepsService {
resolvePluginDependencies(current: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine[] { resolvePluginDependencies(current: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine[] {
const resolved: RuntimeDependencyPluginDefine[] = []; const resolved: RuntimeDependencyPluginDefine[] = [];
const visited = new Set<string>(); const visited = new Set<string>();
const visit = (item: RuntimeDependencyPluginDefine) => { const visit = (item: RuntimeDependencyPluginDefine) => {
const key = this.buildPluginDependencyKey(item); const key = this.buildPluginDependencyKey(item);
if (visited.has(key)) { if (visited.has(key)) {
@@ -439,7 +446,6 @@ export class RuntimeDepsService {
visit(dependency); visit(dependency);
} }
}; };
visit(current); visit(current);
return resolved; return resolved;
} }
@@ -454,35 +460,81 @@ export class RuntimeDepsService {
private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine { private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine {
const parts = pluginKey.split(":"); const parts = pluginKey.split(":");
const [pluginType, subtype, rawName] = parts; let pluginType: string, name: string, subtype: string | undefined;
let name = rawName;
if (parts.length === 2) { if (parts.length === 2) {
name = subtype; [pluginType, name] = parts;
} else if (parts.length === 3) { } else if (parts.length === 3) {
//无修改 [pluginType, subtype, name] = parts;
} else { } else {
const ownerName = owner?.name || pluginKey; const ownerName = owner?.name || pluginKey;
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},请使用 plugin:name、access:name、notification:name、dnsProvider:name 或 addon:subtype:name 格式`); throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey}`);
} }
const registryMap: Record<string, { registry: Registry<any>; key: string; pluginType: string; addonType?: string }> = { if (!this.registriesMap) {
plugin: { registry: pluginRegistry, key: name, pluginType: "plugin" }, throw new Error("注册表未设置,请先调用 setRegistries");
access: { registry: accessRegistry, key: name, pluginType: "access" }, }
notification: { registry: notificationRegistry, key: name, pluginType: "notification" }, const target = this.registriesMap[pluginType];
dnsProvider: { registry: dnsProviderRegistry, key: name, pluginType: "dnsProvider" },
addon: { registry: addonRegistry, key: `${subtype}:${name}`, pluginType: "addon", addonType: subtype },
};
const target = registryMap[pluginType];
if (!target) { if (!target) {
const ownerName = owner?.name || pluginKey; const ownerName = owner?.name || pluginKey;
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},未知插件类型 ${pluginType}`); throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},未知插件类型 ${pluginType}`);
} }
const define = target.registry.getDefine(target.key) as RegisteredDefineLike; // addon 类型的 key 需要包含 subtype
const registryKey = pluginType === "addon" && subtype ? `${subtype}:${name}` : name;
const define = target.registry.getDefine(registryKey) as RegisteredDefineLike;
if (!define) { if (!define) {
throw new Error(`插件依赖缺失: ${owner?.name || pluginKey} 依赖 ${pluginKey},但该插件未注册或已禁用`); throw new Error(`插件依赖缺失: ${owner?.name || pluginKey} 依赖 ${pluginKey},但该插件未注册或已禁用`);
} }
return { ...define, key: pluginKey, pluginType: target.pluginType, addonType: target.addonType }; return { ...define, key: pluginKey, pluginType: target.pluginType, addonType: target.addonType };
} }
private async doEnsureInstalled(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
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<T>(run: () => Promise<T>): Promise<T> { private async withInstallLock<T>(run: () => Promise<T>): Promise<T> {
const rootDir = this.getRuntimeDepsRootDir(); const rootDir = this.getRuntimeDepsRootDir();
fs.mkdirSync(rootDir, { recursive: true }); fs.mkdirSync(rootDir, { recursive: true });
@@ -508,9 +560,7 @@ export class RuntimeDepsService {
} catch { } catch {
try { try {
fs.rmSync(lockFile, { force: true }); fs.rmSync(lockFile, { force: true });
} catch { } catch {}
// Windows 下 closeSync 后文件句柄可能未立即释放,忽略清理失败
}
} }
} }
releaseProcessLock(); releaseProcessLock();
@@ -525,7 +575,6 @@ export class RuntimeDepsService {
while (true) { while (true) {
try { try {
const fd = fs.openSync(lockFile, "wx"); const fd = fs.openSync(lockFile, "wx");
// @ts-ignore
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8"); fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8");
return fd; return fd;
} catch (error: any) { } catch (error: any) {
@@ -562,8 +611,7 @@ export class RuntimeDepsService {
if (entry === ".install.lock") { if (entry === ".install.lock") {
continue; continue;
} }
const entryPath = path.join(rootDir, entry); fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
fs.rmSync(entryPath, { recursive: true, force: true });
} }
} }
this.installPromises.clear(); this.installPromises.clear();
@@ -571,28 +619,16 @@ export class RuntimeDepsService {
}); });
} }
/**
* package.json lazyDependencies dependPackages
* 使
*/
getMergedLazyDependencies(): Record<string, string> { getMergedLazyDependencies(): Record<string, string> {
return { ...this.lazyDependencies, ...this.pluginLazyDependencies }; return { ...this.lazyDependencies, ...this.pluginLazyDependencies };
} }
/** collectPluginDeps(logger?: ILogger) {
* dependPackages pluginLazyDependencies if (!this.registriesMap) {
*/ return;
collectPluginDeps() { }
const registries: Array<{ registry: Registry<any>; keyFormatter?: (name: string) => string }> = [
{ registry: pluginRegistry },
{ registry: accessRegistry },
{ registry: notificationRegistry },
{ registry: dnsProviderRegistry },
{ registry: addonRegistry },
];
const deps: Record<string, string> = {}; const deps: Record<string, string> = {};
for (const { registry } of registries) { for (const { registry } of Object.values(this.registriesMap)) {
const defineList = registry.getDefineList(); const defineList = registry.getDefineList();
for (const define of defineList) { for (const define of defineList) {
const dependPackages = (define as any).dependPackages as Record<string, string> | undefined; const dependPackages = (define as any).dependPackages as Record<string, string> | undefined;
@@ -602,23 +638,19 @@ export class RuntimeDepsService {
for (const [pkgName, range] of Object.entries(dependPackages)) { for (const [pkgName, range] of Object.entries(dependPackages)) {
const existing = deps[pkgName]; const existing = deps[pkgName];
if (existing && !areRangesCompatible(existing, range)) { if (existing && !areRangesCompatible(existing, range)) {
logger.warn(`懒加载依赖版本冲突: ${pkgName} => ${existing} vs ${range},保留已有版本`); (logger || defaultLogger).warn?.(`懒加载依赖版本冲突: ${pkgName} => ${existing} vs ${range},保留已有版本`);
continue; continue;
} }
deps[pkgName] = range; deps[pkgName] = range;
} }
} }
} }
this.pluginLazyDependencies = deps; this.pluginLazyDependencies = deps;
logger.info(`从插件注册表收集到 ${Object.keys(deps).length} 个懒加载依赖`); (logger || defaultLogger).info(`从插件注册表收集到 ${Object.keys(deps).length} 个懒加载依赖`);
} }
/** refreshPluginDeps(logger?: ILogger) {
* dependPackages importRuntime this.collectPluginDeps(logger);
*/
async refreshPluginDeps() {
this.collectPluginDeps();
} }
private readInstallState(statePath: string): any { private readInstallState(statePath: string): any {
@@ -660,14 +692,8 @@ export class RuntimeDepsService {
return dependencies; return dependencies;
} }
// @ts-ignore
private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) { private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) {
const rootDir = this.getRuntimeDepsRootDir(); const result = await this.commandRunner.run(command, ["--version"], { cwd: this.getRuntimeDepsRootDir(), timeoutMs: Math.min(this.installTimeoutMs, 10000), env });
const result = await this.commandRunner.run(command, ["--version"], {
cwd: rootDir,
timeoutMs: Math.min(this.installTimeoutMs, 10000),
env,
});
if (result.code !== 0) { if (result.code !== 0) {
return ""; return "";
} }
@@ -675,14 +701,10 @@ export class RuntimeDepsService {
} }
private getPnpmCommand() { private getPnpmCommand() {
if (this.pnpmCommand) { return this.pnpmCommand || "pnpm";
return this.pnpmCommand;
}
return "pnpm";
} }
private buildChildEnv(registryUrl: string) { private buildChildEnv(registryUrl: string) {
// @ts-ignore
const env = { ...process.env }; const env = { ...process.env };
for (const key of ["NODE_OPTIONS", "VSCODE_INSPECTOR_OPTIONS", "NODE_INSPECTOR_PORT", "NODE_DEBUG"]) { for (const key of ["NODE_OPTIONS", "VSCODE_INSPECTOR_OPTIONS", "NODE_INSPECTOR_PORT", "NODE_DEBUG"]) {
if (!env[key]) { if (!env[key]) {
@@ -713,7 +735,7 @@ export class RuntimeDepsService {
.join(" "); .join(" ");
} }
private getRuntimeDepsRootDir() { getRuntimeDepsRootDir() {
return path.resolve(this.runtimeDepsRootDir); return path.resolve(this.runtimeDepsRootDir);
} }
@@ -731,3 +753,21 @@ function isPluginVersionCompatible(plugin: RuntimeDependencyPluginDefine, expect
} }
return areRangesCompatible(expectedRange, plugin.version); 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<any> {
return getRuntimeDepsService().importRuntime(specifier, logger);
}
+3 -27
View File
@@ -1,27 +1,3 @@
/** // 类型已迁至 runtime-deps-service.ts,通过 index.ts 重新导出
* 运行时动态导入函数类型 // 此文件保留空壳,避免已有引用报错
*/ export * from "./runtime-deps-service.js";
export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise<any>;
/**
* 日志接口
*/
export type ILogger = {
info: (message: string) => void;
};
/**
* 运行时依赖服务参数
*/
export type EnsureRuntimeDepsOptions = {
pluginKeys: string | string[];
logger?: ILogger;
};
/**
* 运行时依赖服务接口
*/
export interface IRuntimeDepsService {
ensureRuntimeDependencies(options: EnsureRuntimeDepsOptions): Promise<any>;
importRuntime: ImportRuntime;
}
+3 -2
View File
@@ -28,7 +28,8 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"esmock": "^2.7.5", "esmock": "^2.7.5",
"prettier": "3.3.3", "prettier": "3.3.3",
"tslib": "^2.8.1" "tslib": "^2.8.1",
"eslint": "^8.57.0"
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
+1 -1
View File
@@ -37,5 +37,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
+3 -2
View File
@@ -39,7 +39,8 @@
"js-yaml": "^3.11.0", "js-yaml": "^3.11.0",
"mocha": "^10.6.0", "mocha": "^10.6.0",
"prettier": "3.3.3", "prettier": "3.3.3",
"tslib": "^2.8.1" "tslib": "^2.8.1",
"eslint": "^8.57.0"
}, },
"engines": { "engines": {
"node": ">= 8.6.0", "node": ">= 8.6.0",
@@ -62,5 +63,5 @@
"fetch" "fetch"
] ]
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
+1 -1
View File
@@ -38,5 +38,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
+1 -1
View File
@@ -69,5 +69,5 @@
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
@@ -1,15 +1,6 @@
import { HttpClient, ILogger, utils } from "@certd/basic"; import { HttpClient, ILogger, utils } from "@certd/basic";
import {upperFirst} from "lodash-es"; import { upperFirst } from "lodash-es";
import { import { accessRegistry, FormItemProps, IAccessService, IServiceGetter, PluginRequestHandleReq, Registrable, getRuntimeDepsService } from "@certd/pipeline";
accessRegistry,
FormItemProps,
IAccessService,
IRuntimeDepsService,
IServiceGetter,
PluginRequestHandleReq,
Registrable
} from "@certd/pipeline";
export type AddonRequestHandleReqInput<T = any> = { export type AddonRequestHandleReqInput<T = any> = {
id?: number; id?: number;
@@ -19,7 +10,7 @@ export type AddonRequestHandleReqInput<T = any> = {
export type AddonRequestHandleReq<T = any> = { export type AddonRequestHandleReq<T = any> = {
addonType: string; addonType: string;
} &PluginRequestHandleReq<AddonRequestHandleReqInput<T>>; } & PluginRequestHandleReq<AddonRequestHandleReqInput<T>>;
export type AddonInputDefine = FormItemProps & { export type AddonInputDefine = FormItemProps & {
title: string; title: string;
@@ -48,8 +39,6 @@ export type AddonInstanceConfig = {
}; };
}; };
export interface IAddon { export interface IAddon {
ctx: AddonContext; ctx: AddonContext;
[key: string]: any; [key: string]: any;
@@ -67,13 +56,9 @@ export abstract class BaseAddon implements IAddon {
ctx!: AddonContext; ctx!: AddonContext;
http!: HttpClient; http!: HttpClient;
logger!: ILogger; logger!: ILogger;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) { async importRuntime(specifier: string) {
if (!this.runtimeDepsService) { return await getRuntimeDepsService().importRuntime(specifier, this.logger);
return await import(specifier);
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
} }
title!: string; title!: string;
@@ -85,7 +70,7 @@ export abstract class BaseAddon implements IAddon {
if (accessId == null) { if (accessId == null) {
throw new Error("您还没有配置授权"); throw new Error("您还没有配置授权");
} }
const accessService = await this.ctx.serviceGetter.get<IAccessService>("accessService") const accessService = await this.ctx.serviceGetter.get<IAccessService>("accessService");
let res: any = null; let res: any = null;
if (isCommon) { if (isCommon) {
res = await accessService.getCommonById(accessId); res = await accessService.getCommonById(accessId);
@@ -118,15 +103,12 @@ export abstract class BaseAddon implements IAddon {
this.ctx = ctx; this.ctx = ctx;
this.http = ctx.http; this.http = ctx.http;
this.logger = ctx.logger; 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; this.define = define;
}; };
async onRequest(req:AddonRequestHandleReq) { async onRequest(req: AddonRequestHandleReq) {
if (!req.action) { if (!req.action) {
throw new Error("action is required"); throw new Error("action is required");
} }
@@ -144,10 +126,8 @@ export abstract class BaseAddon implements IAddon {
} }
throw new Error(`action ${req.action} not found`); throw new Error(`action ${req.action} not found`);
} }
} }
export interface IAddonGetter { export interface IAddonGetter {
getById<T = any>(id: any): Promise<T>; getById<T = any>(id: any): Promise<T>;
getCommonById<T = any>(id: any): Promise<T>; getCommonById<T = any>(id: any): Promise<T>;
+1 -1
View File
@@ -52,5 +52,5 @@
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
+37 -32
View File
@@ -1,8 +1,8 @@
import * as path from 'path'; import * as path from "path";
import * as fs from 'fs'; import * as fs from "fs";
import { QueryRunner, Table } from 'typeorm'; import { QueryRunner, Table } from "typeorm";
import { FlywayHistory } from './entity.js'; import { FlywayHistory } from "./entity.js";
import * as crypto from 'crypto'; import * as crypto from "crypto";
/** /**
* 脚本文件信息 * 脚本文件信息
@@ -32,10 +32,10 @@ const DefaultLogger = {
}, },
}; };
let customLogger:any = null; let customLogger: any = null;
export function setFlywayLogger (logger: any) { export function setFlywayLogger(logger: any) {
customLogger = logger; customLogger = logger;
}; }
export class Flyway { export class Flyway {
scriptDir; scriptDir;
@@ -45,8 +45,8 @@ export class Flyway {
connection; connection;
logger; logger;
constructor(opts: any) { constructor(opts: any) {
this.scriptDir = opts.scriptDir ?? 'db/migration'; this.scriptDir = opts.scriptDir ?? "db/migration";
this.flywayTableName = opts.flywayTableName ?? 'flyway_history'; this.flywayTableName = opts.flywayTableName ?? "flyway_history";
this.baseline = opts.baseline ?? false; this.baseline = opts.baseline ?? false;
this.allowHashNotMatch = opts.allowHashNotMatch ?? false; this.allowHashNotMatch = opts.allowHashNotMatch ?? false;
this.logger = customLogger || opts.logger || DefaultLogger; this.logger = customLogger || opts.logger || DefaultLogger;
@@ -54,9 +54,9 @@ export class Flyway {
} }
async run(ignores?: (RegExp | string)[]) { async run(ignores?: (RegExp | string)[]) {
this.logger.info('[ midfly ] start-------------'); this.logger.info("[ midfly ] start-------------");
if (!fs.existsSync(this.scriptDir)) { if (!fs.existsSync(this.scriptDir)) {
this.logger.info('[ midfly ] scriptDir<' + this.scriptDir + '> not found'); this.logger.info("[ midfly ] scriptDir<" + this.scriptDir + "> not found");
return; return;
} }
@@ -77,7 +77,7 @@ export class Flyway {
continue; continue;
} }
if (!file.isBaseline) { if (!file.isBaseline) {
this.logger.info('need exec script file: ', file.script); this.logger.info("need exec script file: ", file.script);
//执行sql文件 //执行sql文件
if (/\.sql$/.test(file.script)) { if (/\.sql$/.test(file.script)) {
await this.execSql(filepath, queryRunner); await this.execSql(filepath, queryRunner);
@@ -87,7 +87,7 @@ export class Flyway {
// await this.execJsOrTs(filepath, t); // await this.execJsOrTs(filepath, t);
// } // }
} else { } 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 this.storeSqlExecLog(file.script, filepath, true, queryRunner);
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
@@ -95,10 +95,15 @@ export class Flyway {
this.logger.error(err); this.logger.error(err);
await this.storeSqlExecLog(file.script, filepath, false, queryRunner); await this.storeSqlExecLog(file.script, filepath, false, queryRunner);
await queryRunner.rollbackTransaction(); await queryRunner.rollbackTransaction();
if (err.code === "SQLITE_IOERR_WRITE") {
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)", file.script);
}
throw err; throw err;
} }
} }
this.logger.info('[ midfly ] end-------------'); this.logger.info("[ midfly ] end-------------");
} }
private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) { private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) {
@@ -160,17 +165,17 @@ export class Flyway {
name: this.flywayTableName, name: this.flywayTableName,
columns: [ columns: [
{ {
name: 'id', name: "id",
type: this.connection.driver.normalizeType({ type: this.connection.driver.normalizeType({
type: this.connection.driver.mappedDataTypes.migrationId, type: this.connection.driver.mappedDataTypes.migrationId,
}), }),
isGenerated: true, isGenerated: true,
generationStrategy: 'increment', generationStrategy: "increment",
isPrimary: true, isPrimary: true,
isNullable: false, isNullable: false,
}, },
{ {
name: 'timestamp', name: "timestamp",
type: this.connection.driver.normalizeType({ type: this.connection.driver.normalizeType({
type: this.connection.driver.mappedDataTypes.migrationTimestamp, type: this.connection.driver.mappedDataTypes.migrationTimestamp,
}), }),
@@ -178,23 +183,23 @@ export class Flyway {
isNullable: false, isNullable: false,
}, },
{ {
name: 'name', name: "name",
type: this.connection.driver.normalizeType({ type: this.connection.driver.normalizeType({
type: this.connection.driver.mappedDataTypes.migrationName, type: this.connection.driver.mappedDataTypes.migrationName,
}), }),
isNullable: false, isNullable: false,
}, },
{ {
name: 'hash', name: "hash",
type: this.connection.driver.normalizeType({ type: this.connection.driver.normalizeType({
type: this.connection.driver.mappedDataTypes.migrationName, type: this.connection.driver.mappedDataTypes.migrationName,
}), }),
isNullable: true, isNullable: true,
}, },
{ {
name: 'success', name: "success",
type: this.connection.driver.normalizeType({ type: this.connection.driver.normalizeType({
type: 'boolean', type: "boolean",
}), }),
isNullable: true, isNullable: true,
}, },
@@ -210,7 +215,7 @@ export class Flyway {
} }
let ret = false; let ret = false;
for (const ignore of ignores) { for (const ignore of ignores) {
if (typeof ignore === 'string' && file === ignore) { if (typeof ignore === "string" && file === ignore) {
ret = true; ret = true;
break; break;
} }
@@ -233,20 +238,20 @@ export class Flyway {
if (history.hash !== hash && this.allowHashNotMatch === false) { if (history.hash !== hash && this.allowHashNotMatch === false) {
throw new Error(file + `hash conflict ,old: ${history.hash} != new: ${hash}`); 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; return true;
} }
this.logger.info('[ midfly ] script<' + file + '> not yet execute'); this.logger.info("[ midfly ] script<" + file + "> not yet execute");
return false; return false;
} }
private async getFileHash(filepath: string) { private async getFileHash(filepath: string) {
const content = fs.readFileSync(filepath).toString(); 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) { 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 content = fs.readFileSync(filepath).toString().trim();
const arr = this.splitSql2Array(content); const arr = this.splitSql2Array(content);
for (const s of arr) { for (const s of arr) {
@@ -255,11 +260,11 @@ export class Flyway {
} }
private async execOnePart(sql: string, queryRunner: QueryRunner) { private async execOnePart(sql: string, queryRunner: QueryRunner) {
this.logger.debug('exec sql index: ', sql); this.logger.debug("exec sql index: ", sql);
try { try {
await queryRunner.query(sql); await queryRunner.query(sql);
} catch (err: any) { } catch (err: any) {
this.logger.error('exec sql error ', err.message, err); this.logger.error("exec sql error ", err.message, err);
throw err; throw err;
} }
} }
@@ -275,11 +280,11 @@ export class Flyway {
const temp = String(str).trim(); const temp = String(str).trim();
if (temp === 'null') { if (temp === "null") {
return []; return [];
} }
const semicolon = ';'; const semicolon = ";";
const deepChars = ['"', "'"]; const deepChars = ['"', "'"];
const splits = []; const splits = [];
@@ -289,7 +294,7 @@ export class Flyway {
if (deepChars.indexOf(charAt) >= 0) { if (deepChars.indexOf(charAt) >= 0) {
//如果是深度char //如果是深度char
if (i !== 0 && temp.charAt(i - 1) === '\\') { if (i !== 0 && temp.charAt(i - 1) === "\\") {
//如果前一个是转义字符,忽略它 //如果前一个是转义字符,忽略它
} else { } else {
//说明需要进出深度了 //说明需要进出深度了
+1 -1
View File
@@ -38,5 +38,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
+1 -1
View File
@@ -45,5 +45,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "a6ef6996c3a68ce0c2a4577a0934ed74a53f0515" "gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
} }
@@ -1,5 +1,5 @@
import { HttpClient, ILogger } from "@certd/basic"; 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 punycode from "punycode.js";
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DnsResolveRecord, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js"; import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DnsResolveRecord, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
import { dnsProviderRegistry } from "./registry.js"; import { dnsProviderRegistry } from "./registry.js";
@@ -7,13 +7,9 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
ctx!: DnsProviderContext; ctx!: DnsProviderContext;
http!: HttpClient; http!: HttpClient;
logger!: ILogger; logger!: ILogger;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) { async importRuntime(specifier: string) {
if (!this.runtimeDepsService) { return await getRuntimeDepsService().importRuntime(specifier, this.logger);
throw new Error("runtimeDepsService 未初始化");
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
} }
usePunyCode(): boolean { usePunyCode(): boolean {
@@ -42,9 +38,6 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
this.ctx = ctx; this.ctx = ctx;
this.logger = ctx.logger; this.logger = ctx.logger;
this.http = ctx.http; this.http = ctx.http;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
} }
async parseDomain(fullDomain: string) { async parseDomain(fullDomain: string) {
-1
View File
@@ -59,7 +59,6 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"core-js": "^3.36.0", "core-js": "^3.36.0",
"cos-js-sdk-v5": "^1.7.0",
"cron-parser": "^4.9.0", "cron-parser": "^4.9.0",
"cropperjs": "^1.6.1", "cropperjs": "^1.6.1",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
+1 -2
View File
@@ -20,7 +20,6 @@
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-this-alias": "off", "@typescript-eslint/no-this-alias": "off",
// 允许any "@typescript-eslint/no-unsafe-assignment": "off"
"@typescript-eslint/no-unsafe-anyassignment": "off"
} }
} }
+2
View File
@@ -25,6 +25,7 @@
"format": "prettier --write src", "format": "prettier --write src",
"lint": "mwts fix", "lint": "mwts fix",
"ci": "pnpm run cov", "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-only": "cross-env NODE_ENV=production mwtsc -p tsconfig.build.json --cleanOutDir --skipLibCheck",
"build": "pnpm run build-only && pnpm run export-metadata", "build": "pnpm run build-only && pnpm run export-metadata",
"export-metadata": "node export-plugin-yaml.js", "export-metadata": "node export-plugin-yaml.js",
@@ -133,6 +134,7 @@
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"eslint":"^7.32.0",
"typescript": "^5.4.2", "typescript": "^5.4.2",
"why-is-node-running": "^3.2.2" "why-is-node-running": "^3.2.2"
}, },
@@ -1,3 +1,4 @@
import { getRuntimeDepsService } from "@certd/pipeline";
import { ALL, Body, Controller, Inject, Post, Provide, Query, RequestIP } from "@midwayjs/core"; 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 { addonRegistry, AddonService, CrudController, SysPrivateSettings, SysPublicSettings, SysSafeSetting, SysSettingsEntity, SysSettingsService } from "@certd/lib-server";
import { cloneDeep, merge } from "lodash-es"; 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 { http, logger, utils } from "@certd/basic";
import { CodeService } from "../../../modules/basic/service/code-service.js"; import { CodeService } from "../../../modules/basic/service/code-service.js";
import { SmsServiceFactory } from "../../../modules/basic/sms/factory.js"; import { SmsServiceFactory } from "../../../modules/basic/sms/factory.js";
import { RuntimeDepsService } from "../../../modules/runtime-deps/runtime-deps-service.js";
/** /**
*/ */
@@ -24,9 +24,6 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
codeService: CodeService; codeService: CodeService;
@Inject() @Inject()
addonService: AddonService; addonService: AddonService;
@Inject()
runtimeDepsService: RuntimeDepsService;
getService() { getService() {
return this.service; return this.service;
} }
@@ -222,7 +219,7 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
@Post("/clearRuntimeDeps", { description: "sys:settings:edit" }) @Post("/clearRuntimeDeps", { description: "sys:settings:edit" })
async clearRuntimeDeps() { async clearRuntimeDeps() {
await this.runtimeDepsService.clearRuntimeDeps(); await getRuntimeDepsService().clearRuntimeDeps();
return this.ok(true); return this.ok(true);
} }
} }
@@ -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 { AutoInitSite } from "./auto-init-site.js";
import { AutoLoadPlugins } from "./auto-load-plugins.js"; import { AutoLoadPlugins } from "./auto-load-plugins.js";
import { AutoCron } from "./auto-cron.js";
import { AutoMitterRegister } from "./auto-mitter-register.js"; import { AutoMitterRegister } from "./auto-mitter-register.js";
import { AutoPipelineEmitterRegister } from "./auto-pipeline-emitter-register.js"; import { AutoPipelineEmitterRegister } from "./auto-pipeline-emitter-register.js";
import { AutoFix } from "./fix/auto-fix.js";
import { AutoPrint } from "./auto-print.js"; import { AutoPrint } from "./auto-print.js";
import { AutoFix } from "./fix/auto-fix.js";
@Autoload() @Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -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 { logger } from "@certd/basic";
import { PluginService } from "../plugin/service/plugin-service.js"; import { PluginService } from "../plugin/service/plugin-service.js";
import { registerPaymentProviders } from "../suite/payments/index.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() @Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -10,8 +13,8 @@ export class AutoLoadPlugins {
@Inject() @Inject()
pluginService: PluginService; pluginService: PluginService;
@Inject() @Config("runtimeDeps")
runtimeDepsService: RuntimeDepsService; runtimeDepsConfig: any;
async init() { async init() {
logger.info(`加载插件开始,加载模式:${process.env.certd_plugin_loadmode}`); logger.info(`加载插件开始,加载模式:${process.env.certd_plugin_loadmode}`);
@@ -35,7 +38,16 @@ export class AutoLoadPlugins {
await registerPaymentProviders(); await registerPaymentProviders();
logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`); logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`);
//初始化第三方依赖服务
initRuntimeDepsService(this.runtimeDepsConfig, {
pluginRegistry,
accessRegistry,
notificationRegistry,
dnsProviderRegistry,
addonRegistry,
});
// 收集插件 dependPackages 并安装 // 收集插件 dependPackages 并安装
await this.runtimeDepsService.refreshPluginDeps(); const service = getRuntimeDepsService();
service.refreshPluginDeps();
} }
} }
@@ -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 { AccessService, AccessSysGetter, CodeErrorException, SysSettingsService } from "@certd/lib-server";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { ISmsService } from "../sms/api.js"; import { ISmsService } from "../sms/api.js";
@@ -6,7 +6,6 @@ import { SmsServiceFactory } from "../sms/factory.js";
import { CaptchaService } from "./captcha-service.js"; import { CaptchaService } from "./captcha-service.js";
import { EmailService } from "./email-service.js"; import { EmailService } from "./email-service.js";
import { CaptchaRequest } from "../../../plugins/plugin-captcha/api.js"; import { CaptchaRequest } from "../../../plugins/plugin-captcha/api.js";
import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
// {data: '<svg.../svg>', text: 'abcd'} // {data: '<svg.../svg>', text: 'abcd'}
/** /**
@@ -25,9 +24,6 @@ export class CodeService {
@Inject() @Inject()
captchaService: CaptchaService; captchaService: CaptchaService;
@Inject()
runtimeDepsService: RuntimeDepsService;
async checkCaptcha(body: any, req: CaptchaRequest) { async checkCaptcha(body: any, req: CaptchaRequest) {
return await this.captchaService.doValidate({ form: body, req }); return await this.captchaService.doValidate({ form: body, req });
} }
@@ -60,7 +56,6 @@ export class CodeService {
await sender.setCtx({ await sender.setCtx({
accessService: accessGetter, accessService: accessGetter,
config: smsConfig, config: smsConfig,
runtimeDepsService: this.runtimeDepsService,
}); });
const smsCode = randomNumber(verificationCodeLength); const smsCode = randomNumber(verificationCodeLength);
await sender.sendSmsCode({ await sender.sendSmsCode({
@@ -1,9 +1,8 @@
import { FormItemProps, IAccessService } from "@certd/pipeline"; import { FormItemProps, IAccessService } from "@certd/pipeline";
import type { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
export interface ISmsService { export interface ISmsService {
sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }): Promise<void>; sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }): Promise<void>;
setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any }; runtimeDepsService?: RuntimeDepsService }): Promise<void>; setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any } }): Promise<void>;
} }
export type PluginInputs<T = any> = { export type PluginInputs<T = any> = {
@@ -13,5 +12,4 @@ export type PluginInputs<T = any> = {
export type SmsPluginCtx<T = any> = { export type SmsPluginCtx<T = any> = {
accessService: IAccessService; accessService: IAccessService;
config: T; config: T;
runtimeDepsService?: RuntimeDepsService;
}; };
@@ -1,5 +1,6 @@
import { TencentAccess } from "../../../plugins/plugin-lib/tencent/access.js"; import { TencentAccess } from "../../../plugins/plugin-lib/tencent/access.js";
import { ISmsService, PluginInputs, SmsPluginCtx } from "./api.js"; import { importRuntime } from "@certd/pipeline";
import { ISmsService, PluginInputs } from "./api.js";
export type TencentSmsConfig = { export type TencentSmsConfig = {
accessId: string; accessId: string;
@@ -66,26 +67,16 @@ export class TencentSmsService implements ISmsService {
}; };
} }
ctx: SmsPluginCtx<TencentSmsConfig>; ctx: { accessService: any; config: TencentSmsConfig };
async setCtx(ctx: any) { async setCtx(ctx: any) {
this.ctx = ctx; this.ctx = ctx;
if (this.ctx.runtimeDepsService) {
await this.ctx.runtimeDepsService.ensureDependencies({
dependencies: {
"tencentcloud-sdk-nodejs": "^4.1.112",
},
});
}
} }
async getClient() { async getClient() {
if (!this.ctx.runtimeDepsService) { const sdk = await importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
throw new Error("动态依赖服务未初始化,无法加载腾讯云短信SDK");
}
const sdk = await this.ctx.runtimeDepsService.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
const client = sdk.v20210111.Client; const client = sdk.v20210111.Client;
const access = await this.ctx.accessService.getById<TencentAccess>(this.ctx.config.accessId); const access: TencentAccess = await this.ctx.accessService.getById(this.ctx.config.accessId);
// const region = this.region; // const region = this.region;
const clientConfig = { const clientConfig = {
@@ -1,3 +1,4 @@
import { importRuntime } from "@certd/pipeline";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { UserSettingsService } from "./user-settings-service.js"; import { UserSettingsService } from "./user-settings-service.js";
import { UserTwoFactorSetting } from "./models.js"; import { UserTwoFactorSetting } from "./models.js";
@@ -13,13 +14,12 @@ export class TwoFactorService {
userSettingsService: UserSettingsService; userSettingsService: UserSettingsService;
@Inject() @Inject()
userService: UserService; userService: UserService;
async getAuthenticatorQrCode(userId: any) { async getAuthenticatorQrCode(userId: any) {
const setting = await this.getSetting(userId); const setting = await this.getSetting(userId);
const authenticatorSetting = setting.authenticator; const authenticatorSetting = setting.authenticator;
if (!authenticatorSetting.secret) { if (!authenticatorSetting.secret) {
const { authenticator } = await import("otplib"); const { authenticator } = await importRuntime("otplib");
authenticatorSetting.secret = authenticator.generateSecret(); authenticatorSetting.secret = authenticator.generateSecret();
await this.userSettingsService.saveSetting(userId, null, setting); await this.userSettingsService.saveSetting(userId, null, setting);
@@ -38,7 +38,7 @@ export class TwoFactorService {
async saveAuthenticator(req: { userId: any; verifyCode: any }) { async saveAuthenticator(req: { userId: any; verifyCode: any }) {
const userId = req.userId; const userId = req.userId;
const { authenticator } = await import("otplib"); const { authenticator } = await importRuntime("otplib");
const setting = await this.getSetting(userId); const setting = await this.getSetting(userId);
const authenticatorSetting = setting.authenticator; const authenticatorSetting = setting.authenticator;
@@ -77,7 +77,7 @@ export class TwoFactorService {
} }
async verifyAuthenticatorCode(userId: any, verifyCode: string) { async verifyAuthenticatorCode(userId: any, verifyCode: string) {
const { authenticator } = await import("otplib"); const { authenticator } = await importRuntime("otplib");
const setting = await this.getSetting(userId); const setting = await this.getSetting(userId);
if (!setting.authenticator.enabled) { if (!setting.authenticator.enabled) {
throw new Error("authenticator 未开启"); throw new Error("authenticator 未开启");
@@ -1,4 +1,4 @@
import { IServiceGetter } from "@certd/pipeline"; import { IServiceGetter } from "@certd/pipeline";
import { ApplicationContext, IMidwayContainer, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { ApplicationContext, IMidwayContainer, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { AccessGetter, AccessService } from "@certd/lib-server"; import { AccessGetter, AccessService } from "@certd/lib-server";
import { CnameProxyService } from "./cname-proxy-service.js"; 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 { CertInfoService } from "../../../monitor/index.js";
import { ICertInfoGetter } from "@certd/plugin-lib"; import { ICertInfoGetter } from "@certd/plugin-lib";
import { CnameProviderService } from "../../../cname/service/cname-provider-service.js"; import { CnameProviderService } from "../../../cname/service/cname-provider-service.js";
import { RuntimeDepsService } from "../../../runtime-deps/runtime-deps-service.js";
const serviceNames = ["ocrService"]; const serviceNames = ["ocrService"];
export class TaskServiceGetter implements IServiceGetter { export class TaskServiceGetter implements IServiceGetter {
@@ -39,8 +38,6 @@ export class TaskServiceGetter implements IServiceGetter {
return (await this.getDomainVerifierGetter()) as T; return (await this.getDomainVerifierGetter()) as T;
} else if (serviceName === "certInfoGetter") { } else if (serviceName === "certInfoGetter") {
return (await this.getCertInfoGetter()) as T; return (await this.getCertInfoGetter()) as T;
} else if (serviceName === "runtimeDepsService") {
return (await this.getRuntimeDepsService()) as T;
} else { } else {
if (!serviceNames.includes(serviceName)) { if (!serviceNames.includes(serviceName)) {
throw new Error(`${serviceName} not in whitelist`); throw new Error(`${serviceName} not in whitelist`);
@@ -84,10 +81,6 @@ export class TaskServiceGetter implements IServiceGetter {
const domainService: DomainService = await this.appCtx.getAsync("domainService"); const domainService: DomainService = await this.appCtx.getAsync("domainService");
return new DomainVerifierGetter(this.userId, this.projectId, domainService); return new DomainVerifierGetter(this.userId, this.projectId, domainService);
} }
async getRuntimeDepsService(): Promise<RuntimeDepsService> {
return await this.appCtx.getAsync("runtimeDepsService");
}
} }
@Provide() @Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -962,7 +962,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
return result; return result;
} }
async batchDelete(ids: number[], userId?: number, projectId?: number):Promise<number> { async batchDelete(ids: number[], userId?: number, projectId?: number): Promise<number> {
if (!isPlus()) { if (!isPlus()) {
throw new NeedVIPException("此功能需要升级Certd专业版"); throw new NeedVIPException("此功能需要升级Certd专业版");
} }
@@ -976,7 +976,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.delete(id); await this.delete(id);
ids.push(id); ids.push(id);
} }
return ids.length return ids.length;
} }
async batchUpdateGroup(ids: number[], groupId: number, userId: any, projectId?: number) { async batchUpdateGroup(ids: number[], groupId: number, userId: any, projectId?: number) {
@@ -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 { addonRegistry, BaseService, PageReq } from "@certd/lib-server";
import { PluginEntity } from "../entity/plugin.js"; import { PluginEntity } from "../entity/plugin.js";
import { InjectEntityModel } from "@midwayjs/typeorm"; import { InjectEntityModel } from "@midwayjs/typeorm";
@@ -6,14 +6,13 @@ import { IsNull, Not, Repository } from "typeorm";
import { isComm } from "@certd/plus-core"; import { isComm } from "@certd/plus-core";
import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js"; import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js";
import { merge } from "lodash-es"; import { merge } from "lodash-es";
import { accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline";
import { dnsProviderRegistry } from "@certd/plugin-cert"; import { dnsProviderRegistry } from "@certd/plugin-cert";
import { logger } from "@certd/basic"; import { logger } from "@certd/basic";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js"; import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js";
import fs from "fs"; import fs from "fs";
import path from "path"; 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 = { export type PluginImportReq = {
content: string; content: string;
@@ -49,9 +48,6 @@ export class PluginService extends BaseService<PluginEntity> {
@Inject() @Inject()
builtInPluginService: BuiltInPluginService; builtInPluginService: BuiltInPluginService;
@Inject()
runtimeDepsService: RuntimeDepsService;
//@ts-ignore //@ts-ignore
getRepository() { getRepository() {
return this.repository; return this.repository;
@@ -270,7 +266,7 @@ export class PluginService extends BaseService<PluginEntity> {
return; return;
} }
await this.registerPlugin(item); await this.registerPlugin(item);
await this.runtimeDepsService.refreshPluginDeps(); await this.refreshPluginDeps();
} }
async unRegisterById(id: any) { async unRegisterById(id: any) {
@@ -298,7 +294,12 @@ export class PluginService extends BaseService<PluginEntity> {
} else { } else {
logger.warn(`不支持的插件类型:${item.pluginType}`); logger.warn(`不支持的插件类型:${item.pluginType}`);
} }
await this.runtimeDepsService.refreshPluginDeps(); await this.refreshPluginDeps();
}
async refreshPluginDeps() {
const service = getRuntimeDepsService();
service.refreshPluginDeps();
} }
async update(param: any) { async update(param: any) {
@@ -334,7 +335,7 @@ export class PluginService extends BaseService<PluginEntity> {
if (!isBareModuleSpecifier(modulePath)) { if (!isBareModuleSpecifier(modulePath)) {
return await importLocalModule(modulePath); return await importLocalModule(modulePath);
} }
return await this.runtimeDepsService.importRuntime(modulePath, logger); return await importRuntimeDirect(modulePath, logger);
} }
private async getPluginClassFromFile(item: any) { private async getPluginClassFromFile(item: any) {
@@ -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<string> {
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<RegistryProbeResult> {
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,
};
}
}
}
@@ -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 { AliyunAccess } from "../../plugin-lib/aliyun/access/aliyun-access.js";
import { AliyunClient } from "../../plugin-lib/aliyun/index.js"; import { AliyunClient } from "../../plugin-lib/aliyun/index.js";
import { Pager, PageRes, PageSearch } from "@certd/pipeline"; import { Pager, PageRes, PageSearch } from "@certd/pipeline";
@@ -16,7 +16,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
async onInstance() { async onInstance() {
const access: AliyunAccess = this.ctx.access as AliyunAccess; 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({ await this.client.init({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret, accessKeySecret: access.accessKeySecret,
@@ -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 { utils } from "@certd/basic";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
@@ -241,7 +241,7 @@ export class DeployCertToAliyunAckPlugin extends AbstractTaskPlugin {
} }
async getClient(aliyunProvider: any, regionId: string) { 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({ await client.init({
accessKeyId: aliyunProvider.accessKeyId, accessKeyId: aliyunProvider.accessKeyId,
accessKeySecret: aliyunProvider.accessKeySecret, accessKeySecret: aliyunProvider.accessKeySecret,
@@ -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 { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js";
@@ -128,7 +128,7 @@ export class AliyunDeployCertToALB extends AbstractTaskPlugin {
async onInstance() {} async onInstance() {}
async getLBClient(access: AliyunAccess, region: string) { 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"; const version = "2020-06-16";
await client.init({ await client.init({
@@ -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 { AbstractTaskPlugin, CertTargetItem, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline";
import { CertApplyPluginNames, CertReader } from "@certd/plugin-cert"; import { CertApplyPluginNames, CertReader } from "@certd/plugin-cert";
import { CertInfo, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { CertInfo, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
@@ -209,7 +209,7 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
} }
async getClient(access: AliyunAccess) { 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({ await client.init({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret, accessKeySecret: access.accessKeySecret,
@@ -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 { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js";
@@ -143,7 +143,7 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin {
} }
async getClient(access: AliyunAccess) { 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({ await client.init({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret, accessKeySecret: access.accessKeySecret,
@@ -137,8 +137,8 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
const client = await this.getClient(access); const client = await this.getClient(access);
const $Util = await access.importRuntime("@alicloud/tea-util"); const $Util = await this.importRuntime("@alicloud/tea-util");
const $OpenApi = await access.importRuntime("@alicloud/openapi-client"); const $OpenApi = await this.importRuntime("@alicloud/openapi-client");
let privateKey = this.cert.key; let privateKey = this.cert.key;
try { try {
@@ -204,7 +204,7 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
} }
async getClient(access: AliyunAccess) { 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({ const config = new $OpenApi.Config({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret, accessKeySecret: access.accessKeySecret,
@@ -221,8 +221,8 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
const access = await this.getAccess<AliyunAccess>(this.accessId); const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = await this.getClient(access); const client = await this.getClient(access);
const $OpenApi = await access.importRuntime("@alicloud/openapi-client"); const $OpenApi = await this.importRuntime("@alicloud/openapi-client");
const $Util = await access.importRuntime("@alicloud/tea-util"); const $Util = await this.importRuntime("@alicloud/tea-util");
const params = new $OpenApi.Params({ const params = new $OpenApi.Params({
// 接口名称 // 接口名称
action: "ListCustomDomains", action: "ListCustomDomains",
@@ -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 { CertInfo, CertReader } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { CertApplyPluginNames } from "@certd/plugin-cert"; import { CertApplyPluginNames } from "@certd/plugin-cert";
@@ -117,7 +117,7 @@ export class AliyunDeployCertToNLB extends AbstractTaskPlugin {
async onInstance() {} async onInstance() {}
async getLBClient(access: AliyunAccess, region: string) { 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"; const version = "2022-04-30";
await client.init({ await client.init({
@@ -196,7 +196,7 @@ export class DeployCertToAliyunOSS extends AbstractTaskPlugin {
async getClient(access: AliyunAccess) { async getClient(access: AliyunAccess) {
// @ts-ignore // @ts-ignore
const OSS = await access.importRuntime("ali-oss"); const OSS = await this.importRuntime("ali-oss");
return new OSS.default({ return new OSS.default({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret, accessKeySecret: access.accessKeySecret,
@@ -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 { CertInfo } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { CertApplyPluginNames } from "@certd/plugin-cert"; import { CertApplyPluginNames } from "@certd/plugin-cert";
@@ -130,7 +130,7 @@ export class AliyunDeployCertToSLB extends AbstractTaskPlugin {
async onInstance() {} async onInstance() {}
async getLBClient(access: AliyunAccess, region: string) { 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"; const version = "2014-05-15";
await client.init({ await client.init({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
@@ -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 { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js";
@@ -103,7 +103,7 @@ export class AliyunDeployCertToWafCloud extends AbstractTaskPlugin {
async onInstance() {} async onInstance() {}
async getWafClient(access: AliyunAccess) { 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({ await client.init({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret, accessKeySecret: access.accessKeySecret,
@@ -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 { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js";
@@ -115,7 +115,7 @@ export class AliyunDeployCertToWaf extends AbstractTaskPlugin {
async onInstance() {} async onInstance() {}
async getWafClient(access: AliyunAccess) { 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({ await client.init({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret, accessKeySecret: access.accessKeySecret,
@@ -101,7 +101,7 @@ export class AliossAccess extends BaseAccess {
async getClient(access: AliyunAccess) { async getClient(access: AliyunAccess) {
// @ts-ignore // @ts-ignore
const OSS = await access.importRuntime("ali-oss"); const OSS = await this.importRuntime("ali-oss");
return new OSS.default({ return new OSS.default({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret, accessKeySecret: access.accessKeySecret,
@@ -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 { AliyunClientV2 } from "../lib/aliyun-client-v2.js";
import { AliyunSslClient } from "../lib/ssl-client.js"; import { AliyunSslClient } from "../lib/ssl-client.js";
@IsAccess({ @IsAccess({
@@ -1,5 +1,6 @@
import { ILogger } from "@certd/basic"; import { ILogger } from "@certd/basic";
import { AliyunAccess } from "../access/aliyun-access.js"; import { AliyunAccess } from "../access/aliyun-access.js";
import { importRuntime as importRuntimeDirect } from "@certd/pipeline";
export type AliyunClientV2Req = { export type AliyunClientV2Req = {
action: string; action: string;
@@ -26,11 +27,15 @@ export class AliyunClientV2 {
this.endpoint = opts.endpoint; this.endpoint = opts.endpoint;
} }
async importRuntime(name: string) {
return await importRuntimeDirect(name, this.logger);
}
async getClient() { async getClient() {
if (this.client) { if (this.client) {
return 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"); // const Credential = await import("@alicloud/credentials");
// //@ts-ignore // //@ts-ignore
// const credential = new Credential.default.default({ // const credential = new Credential.default.default({
@@ -52,9 +57,9 @@ export class AliyunClientV2 {
async doRequest(req: AliyunClientV2Req) { async doRequest(req: AliyunClientV2Req) {
const client = await this.getClient(); const client = await this.getClient();
const $OpenApi = await this.access.importRuntime("@alicloud/openapi-client"); const $OpenApi = await this.importRuntime("@alicloud/openapi-client");
const $Util = await this.access.importRuntime("@alicloud/tea-util"); const $Util = await this.importRuntime("@alicloud/tea-util");
const OpenApiUtil = await this.access.importRuntime("@alicloud/openapi-util"); const OpenApiUtil = await this.importRuntime("@alicloud/openapi-util");
const params = new $OpenApi.Params({ const params = new $OpenApi.Params({
// 接口名称 // 接口名称
action: req.action, action: req.action,
@@ -1,21 +1,24 @@
import { getGlobalAgents, ILogger } from "@certd/basic"; import { getGlobalAgents, ILogger } from "@certd/basic";
import { ImportRuntime } from "@certd/pipeline"; import { importRuntime as importRuntimeDirect } from "@certd/pipeline";
export class AliyunClient { export class AliyunClient {
client: any; client: any;
logger: ILogger; logger: ILogger;
agent: any; agent: any;
useROAClient: boolean; useROAClient: boolean;
importRuntime: ImportRuntime;
constructor(opts: { logger: ILogger; useROAClient?: boolean; importRuntime?: ImportRuntime }) { constructor(opts: { logger: ILogger; useROAClient?: boolean }) {
this.logger = opts.logger; this.logger = opts.logger;
this.useROAClient = opts.useROAClient || false; this.useROAClient = opts.useROAClient || false;
this.importRuntime = opts.importRuntime || (async (specifier: string) => await import(specifier));
const agents = getGlobalAgents(); const agents = getGlobalAgents();
this.agent = agents.httpsAgent; this.agent = agents.httpsAgent;
} }
async importRuntime(specifier: string) {
return await importRuntimeDirect(specifier, this.logger);
}
async getSdk() { async getSdk() {
if (this.useROAClient) { if (this.useROAClient) {
return await this.getROAClient(); return await this.getROAClient();
@@ -1,4 +1,4 @@
import { ILogger, utils } from "@certd/basic"; import { ILogger, utils } from "@certd/basic";
import { AliyunAccess } from "../access/index.js"; import { AliyunAccess } from "../access/index.js";
import { AliyunClient } from "./index.js"; import { AliyunClient } from "./index.js";
import { CertInfo, CertReader, SimpleCertDetail } from "@certd/plugin-lib"; import { CertInfo, CertReader, SimpleCertDetail } from "@certd/plugin-lib";
@@ -42,6 +42,7 @@ export type CasCertId = {
export class AliyunSslClient { export class AliyunSslClient {
opts: AliyunSslClientOpts; opts: AliyunSslClientOpts;
logger: ILogger; logger: ILogger;
constructor(opts: AliyunSslClientOpts) { constructor(opts: AliyunSslClientOpts) {
this.opts = opts; this.opts = opts;
this.logger = opts.logger; this.logger = opts.logger;
@@ -55,7 +56,7 @@ export class AliyunSslClient {
async getClient() { async getClient() {
const access = this.opts.access; 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"; let endpoint = this.opts.endpoint || "cas.aliyuncs.com";
if (this.opts.endpoint == null && this.opts.region) { if (this.opts.endpoint == null && this.opts.region) {
@@ -1,4 +1,4 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { TencentAccess, TencentCosAccess, TencentCosClient } from "../../tencent/index.js"; import { TencentAccess, TencentCosAccess, TencentCosClient } from "../../tencent/index.js";
import { BaseOssClient, OssFileItem } from "../api.js"; import { BaseOssClient, OssFileItem } from "../api.js";
@@ -109,7 +109,7 @@ export class TencentAccess extends BaseAccess {
} }
async getStsClient() { 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 StsClient = sdk.v20180813.Client;
const clientConfig = { const clientConfig = {
@@ -1,21 +1,23 @@
import { TencentAccess } from "../access.js"; import { TencentAccess } from "../access.js";
import { ILogger, safePromise } from "@certd/basic"; import { ILogger, safePromise } from "@certd/basic";
import { ImportRuntime } from "@certd/pipeline";
import fs from "fs"; import fs from "fs";
import { importRuntime as importRuntimeDirect } from "@certd/pipeline";
export class TencentCosClient { export class TencentCosClient {
access: TencentAccess; access: TencentAccess;
logger: ILogger; logger: ILogger;
region: string; region: string;
bucket: 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.access = opts.access;
this.logger = opts.logger; this.logger = opts.logger;
this.bucket = opts.bucket; this.bucket = opts.bucket;
this.region = opts.region; 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() { async getCosClient() {
@@ -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(", ")}`);
}
}
};
@@ -1,344 +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 { BaseAccess } = await import("@certd/pipeline");
const httpsModule = await import("node:https");
const { URL } = await import("node:url");
const axiosModule = await import("axios");
const formDataModule = await import("form-data");
const { authenticator } = await import("otplib");
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.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 {
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,
});
}
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);
}
};
@@ -142,7 +142,7 @@ export class DeployCertToTencentCosPlugin extends AbstractTaskPlugin {
async onGetDomainList(data: any) { async onGetDomainList(data: any) {
const access = await this.getAccess(this.accessId); 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({ const cos = new cosv5.default({
SecretId: access.secretId, SecretId: access.secretId,
SecretKey: access.secretKey, SecretKey: access.secretKey,
@@ -95,7 +95,7 @@ export class TencentDeployCertToLive extends AbstractTaskPlugin {
async getLiveClient() { async getLiveClient() {
const accessProvider = await this.getAccess<TencentAccess>(this.accessId); const accessProvider = await this.getAccess<TencentAccess>(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 CssClient = sdk.v20180801.Client;
const clientConfig = { const clientConfig = {
@@ -53,7 +53,7 @@ export class UploadCertToTencent extends AbstractTaskPlugin {
Client: any; Client: any;
async onInstance() { 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; this.Client = sdk.v20191205.Client;
} }
@@ -10,12 +10,15 @@ export class VolcengineCdnClient {
this.opts = opts; this.opts = opts;
} }
async importRuntime(packageName: string) {
return this.opts.access.importRuntime(packageName);
}
async getCdnClient() { async getCdnClient() {
if (this.service) { if (this.service) {
return this.service; return this.service;
} }
const importRuntime = this.opts.importRuntime || this.opts.access.importRuntime.bind(this.opts.access); const { cdn } = await this.importRuntime("@volcengine/openapi");
const { cdn } = await importRuntime("@volcengine/openapi");
const service = new cdn.CdnService(); const service = new cdn.CdnService();
// 设置ak、sk // 设置ak、sk
service.setAccessKeyId(this.opts.access.accessKeyId); service.setAccessKeyId(this.opts.access.accessKeyId);
@@ -19,10 +19,12 @@ export class VolcengineDnsClient {
constructor(opts: VolcengineOpts) { constructor(opts: VolcengineOpts) {
this.opts = opts; this.opts = opts;
} }
async importRuntime(packageName: string) {
return this.opts.access.importRuntime(packageName);
}
async doRequest(req: VolcengineReq) { async doRequest(req: VolcengineReq) {
const importRuntime = this.opts.importRuntime || this.opts.access.importRuntime.bind(this.opts.access); const { Signer } = await this.importRuntime("@volcengine/openapi");
const { Signer } = await importRuntime("@volcengine/openapi");
// http request data // http request data
const openApiRequestData: any = { const openApiRequestData: any = {
@@ -243,7 +243,7 @@ export class VolcengineDeployToVOD extends AbstractTaskPlugin {
return { return {
value: item.Domain, value: item.Domain,
label: item.Domain, label: item.Domain,
domain : item.Domain, domain: item.Domain,
}; };
}); });
return this.ctx.utils.options.buildGroupOptions(list, this.certDomains); return this.ctx.utils.options.buildGroupOptions(list, this.certDomains);
@@ -1,12 +1,10 @@
import { VolcengineAccess } from "./access.js"; import { VolcengineAccess } from "./access.js";
import { HttpClient, ILogger } from "@certd/basic"; import { HttpClient, ILogger } from "@certd/basic";
import { ImportRuntime } from "@certd/pipeline";
export type VolcengineOpts = { export type VolcengineOpts = {
access: VolcengineAccess; access: VolcengineAccess;
logger: ILogger; logger: ILogger;
http: HttpClient; http: HttpClient;
importRuntime?: ImportRuntime;
}; };
export class VolcengineClient { export class VolcengineClient {
@@ -17,6 +15,10 @@ export class VolcengineClient {
this.opts = opts; this.opts = opts;
} }
async importRuntime(packageName: string) {
return this.opts.access.importRuntime(packageName);
}
async getCertCenterService() { async getCertCenterService() {
const CommonService = await this.getServiceCls(); const CommonService = await this.getServiceCls();
@@ -142,8 +144,7 @@ export class VolcengineClient {
} }
async getTOSService(opts: { region?: string }) { async getTOSService(opts: { region?: string }) {
const importRuntime = this.opts.importRuntime || this.opts.access.importRuntime.bind(this.opts.access); const { TosClient } = await this.importRuntime("@volcengine/tos-sdk");
const { TosClient } = await importRuntime("@volcengine/tos-sdk");
const client = new TosClient({ const client = new TosClient({
accessKeyId: this.opts.access.accessKeyId, accessKeyId: this.opts.access.accessKeyId,
@@ -172,8 +173,7 @@ export class VolcengineClient {
if (this.CommonService) { if (this.CommonService) {
return this.CommonService; return this.CommonService;
} }
const importRuntime = this.opts.importRuntime || this.opts.access.importRuntime.bind(this.opts.access); const { Service } = await this.importRuntime("@volcengine/openapi");
const { Service } = await importRuntime("@volcengine/openapi");
class CommonService extends Service { class CommonService extends Service {
Generic: any; Generic: any;
@@ -22,7 +22,6 @@ export class VolcengineDnsProvider extends AbstractDnsProvider {
access: this.access, access: this.access,
logger: this.logger, logger: this.logger,
http: this.http, http: this.http,
importRuntime: this.importRuntime.bind(this),
}); });
} }
+1 -1
View File
@@ -1 +1 @@
23:48 02:48
+1 -1
View File
@@ -1 +1 @@
00:00 03:41