mirror of
https://github.com/certd/certd.git
synced 2026-07-19 21:27:35 +08:00
Merge remote-tracking branch 'origin/v2-dev' into v2_audit_log
This commit is contained in:
@@ -70,6 +70,8 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
|||||||
- 先读本文,再按任务读取具体代码或技能文件。
|
- 先读本文,再按任务读取具体代码或技能文件。
|
||||||
- PowerShell 读取中文、Markdown、locale、文档类文件时使用 `Get-Content -Raw -Encoding UTF8`;仍乱码时先执行 `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()`。
|
- PowerShell 读取中文、Markdown、locale、文档类文件时使用 `Get-Content -Raw -Encoding UTF8`;仍乱码时先执行 `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()`。
|
||||||
- PowerShell 中用 `rg` 搜索含引号、括号、反斜杠的 pattern 时,优先用单引号包裹整个 pattern,例如 `rg 'await import\("tencentcloud-sdk-nodejs' packages/ui/certd-server/src -g '*.ts'`。
|
- PowerShell 中用 `rg` 搜索含引号、括号、反斜杠的 pattern 时,优先用单引号包裹整个 pattern,例如 `rg 'await import\("tencentcloud-sdk-nodejs' packages/ui/certd-server/src -g '*.ts'`。
|
||||||
|
- 手工编辑或创建文件时优先使用 `apply_patch`。单个文件内有多处不连续改动时,拆成多个独立的 `*** Update File` 块,每块只改一处附近上下文;不要在同一个 update hunk 里强塞多个 `@@`。
|
||||||
|
- 只有真正机械化的大批量替换、格式化或生成任务才考虑脚本/工具。若必须使用临时脚本,应放在临时目录并在同一个受控步骤内完成创建、执行、删除;不要把临时脚本落在仓库里跨多步工具调用执行。
|
||||||
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
|
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
|
||||||
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
|
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
|
||||||
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
|
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
|
||||||
@@ -210,7 +212,7 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
|||||||
- 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules` 与 `src/controller`。
|
- 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules` 与 `src/controller`。
|
||||||
- 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`。
|
- 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`。
|
||||||
|
|
||||||
## 注意事项
|
## 其他注意事项
|
||||||
|
|
||||||
|
|
||||||
### 换行符(LF / CRLF)
|
### 换行符(LF / CRLF)
|
||||||
@@ -237,4 +239,4 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
|||||||
- 审计日志是 Plus 版功能,非 Plus 版不会写入。
|
- 审计日志是 Plus 版功能,非 Plus 版不会写入。
|
||||||
- Controller 继承 `BaseController`,通过 `this.auditLog({ content: "xxx" })` 记录日志。
|
- Controller 继承 `BaseController`,通过 `this.auditLog({ content: "xxx" })` 记录日志。
|
||||||
- `getAuditType()` 返回类型常量,中间件自动从 ctx.path 判定 scope(`/api/sys/` → system,其他 → user)。
|
- `getAuditType()` 返回类型常量,中间件自动从 ctx.path 判定 scope(`/api/sys/` → system,其他 → user)。
|
||||||
- 操作日志有系统级(scope=system)和用户级(scope=user)区分。
|
- 操作日志有系统级(scope=system)和用户级(scope=user)区分。
|
||||||
|
|||||||
@@ -107,11 +107,13 @@ https://certd.handfree.work/
|
|||||||
|
|
||||||
**镜像版本:**
|
**镜像版本:**
|
||||||
|
|
||||||
| 版本标签 | 基础系统 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
| 标签 | 指定版本 | 基础系统 | 说明 |
|
||||||
| `latest` / `[version]` | Alpine Linux | 默认版本,镜像体积小 |
|
| --- | --- | --- | --- |
|
||||||
| `slim` / `[version]-slim` | Debian slim | 基于glibc,dns解析兼容性好(可能需要配置security_opt -seccomp=unconfined) |
|
| `latest` | `[version]` | Alpine Linux | 默认版本,镜像体积小 |
|
||||||
| `armv7` / `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
|
| `slim` | `[version]-slim` | Debian slim | 基于glibc,dns解析兼容性好(可能需要配置security_opt -seccomp=unconfined) |
|
||||||
|
| `armv7` | `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
|
||||||
|
|
||||||
|
|
||||||
**镜像地址:**
|
**镜像地址:**
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
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";
|
||||||
@@ -34,6 +34,9 @@ describe("RuntimeDepsService", () => {
|
|||||||
async resolve() {
|
async resolve() {
|
||||||
return "https://registry.npmmirror.com";
|
return "https://registry.npmmirror.com";
|
||||||
},
|
},
|
||||||
|
async resolveOrdered() {
|
||||||
|
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[]) {
|
||||||
@@ -60,6 +63,9 @@ describe("RuntimeDepsService", () => {
|
|||||||
async resolve() {
|
async resolve() {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
async resolveOrdered() {
|
||||||
|
return [""];
|
||||||
|
},
|
||||||
} as any;
|
} as any;
|
||||||
service.commandRunner = {
|
service.commandRunner = {
|
||||||
async run(command: string, args: string[]) {
|
async run(command: string, args: string[]) {
|
||||||
@@ -100,6 +106,9 @@ describe("RuntimeDepsService", () => {
|
|||||||
async resolve() {
|
async resolve() {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
async resolveOrdered() {
|
||||||
|
return [""];
|
||||||
|
},
|
||||||
} as any;
|
} as any;
|
||||||
service.commandRunner = {
|
service.commandRunner = {
|
||||||
async run(command: string, args: string[]) {
|
async run(command: string, args: string[]) {
|
||||||
@@ -127,6 +136,9 @@ describe("RuntimeDepsService", () => {
|
|||||||
async resolve() {
|
async resolve() {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
async resolveOrdered() {
|
||||||
|
return [""];
|
||||||
|
},
|
||||||
} as any;
|
} as any;
|
||||||
service.commandRunner = {
|
service.commandRunner = {
|
||||||
async run(command: string, args: string[]) {
|
async run(command: string, args: string[]) {
|
||||||
@@ -167,6 +179,9 @@ describe("RuntimeDepsService", () => {
|
|||||||
async resolve() {
|
async resolve() {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
async resolveOrdered() {
|
||||||
|
return [""];
|
||||||
|
},
|
||||||
} as any;
|
} as any;
|
||||||
service.commandRunner = {
|
service.commandRunner = {
|
||||||
async run(command: string, args: string[]) {
|
async run(command: string, args: string[]) {
|
||||||
@@ -211,6 +226,9 @@ describe("RuntimeDepsService", () => {
|
|||||||
async resolve() {
|
async resolve() {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
async resolveOrdered() {
|
||||||
|
return [""];
|
||||||
|
},
|
||||||
} as any;
|
} as any;
|
||||||
service.commandRunner = {
|
service.commandRunner = {
|
||||||
async run(command: string, args: string[]) {
|
async run(command: string, args: string[]) {
|
||||||
@@ -279,6 +297,9 @@ describe("RuntimeDepsService", () => {
|
|||||||
async resolve() {
|
async resolve() {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
async resolveOrdered() {
|
||||||
|
return [""];
|
||||||
|
},
|
||||||
} as any;
|
} 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 }) {
|
||||||
@@ -315,7 +336,7 @@ describe("RuntimeDepsService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("NpmRegistryResolver", () => {
|
describe("NpmRegistryResolver", () => {
|
||||||
it("chooses the fastest successful registry in auto mode", async () => {
|
it("returns the fastest successful registry via resolve()", async () => {
|
||||||
const resolver = new NpmRegistryResolver({
|
const resolver = new NpmRegistryResolver({
|
||||||
mode: "auto",
|
mode: "auto",
|
||||||
candidates: ["https://slow.example.com", "https://fast.example.com"],
|
candidates: ["https://slow.example.com", "https://fast.example.com"],
|
||||||
@@ -341,4 +362,60 @@ describe("NpmRegistryResolver", () => {
|
|||||||
const result = await resolver.resolve();
|
const result = await resolver.resolve();
|
||||||
assert.equal(result, "https://registry.example.com");
|
assert.equal(result, "https://registry.example.com");
|
||||||
});
|
});
|
||||||
|
it("returns ordered list via resolveOrdered (fastest first)", async () => {
|
||||||
|
const resolver = new NpmRegistryResolver({
|
||||||
|
mode: "auto",
|
||||||
|
candidates: ["https://slow.example.com", "https://fast.example.com"],
|
||||||
|
probeTimeoutMs: 100,
|
||||||
|
cacheTtlMs: 1000,
|
||||||
|
});
|
||||||
|
resolver.probe = async (registryUrl: string) => ({
|
||||||
|
registryUrl,
|
||||||
|
ok: true,
|
||||||
|
elapsedMs: registryUrl.includes("fast") ? 10 : 50,
|
||||||
|
});
|
||||||
|
const result = await resolver.resolveOrdered();
|
||||||
|
assert.deepEqual(result, ["https://fast.example.com", "https://slow.example.com"]);
|
||||||
|
});
|
||||||
|
it("includes failed registries at the end of resolveOrdered", async () => {
|
||||||
|
const resolver = new NpmRegistryResolver({
|
||||||
|
mode: "auto",
|
||||||
|
candidates: ["https://good.example.com", "https://bad.example.com"],
|
||||||
|
probeTimeoutMs: 100,
|
||||||
|
cacheTtlMs: 1000,
|
||||||
|
});
|
||||||
|
resolver.probe = async (registryUrl: string) => {
|
||||||
|
if (registryUrl.includes("bad")) {
|
||||||
|
return { registryUrl, ok: false, elapsedMs: 200 };
|
||||||
|
}
|
||||||
|
return { registryUrl, ok: true, elapsedMs: 30 };
|
||||||
|
};
|
||||||
|
const result = await resolver.resolveOrdered();
|
||||||
|
assert.deepEqual(result, ["https://good.example.com", "https://bad.example.com"]);
|
||||||
|
});
|
||||||
|
it("returns empty ordered list when no candidates", async () => {
|
||||||
|
const resolver = new NpmRegistryResolver({ mode: "auto", candidates: [] });
|
||||||
|
const result = await resolver.resolveOrdered();
|
||||||
|
assert.deepEqual(result, []);
|
||||||
|
const single = await resolver.resolve();
|
||||||
|
assert.equal(single, "");
|
||||||
|
});
|
||||||
|
it("re-validates cached URL on resolveOrdered call", async () => {
|
||||||
|
let probeCount = 0;
|
||||||
|
const resolver = new NpmRegistryResolver({
|
||||||
|
mode: "auto",
|
||||||
|
candidates: ["https://mirror.example.com"],
|
||||||
|
cacheTtlMs: 60000,
|
||||||
|
});
|
||||||
|
resolver.probe = async (registryUrl: string) => {
|
||||||
|
probeCount++;
|
||||||
|
return { registryUrl, ok: true, elapsedMs: 10 };
|
||||||
|
};
|
||||||
|
const first = await resolver.resolveOrdered();
|
||||||
|
assert.deepEqual(first, ["https://mirror.example.com"]);
|
||||||
|
assert.equal(probeCount, 1);
|
||||||
|
const second = await resolver.resolveOrdered();
|
||||||
|
assert.deepEqual(second, ["https://mirror.example.com"]);
|
||||||
|
assert.equal(probeCount, 2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
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";
|
||||||
@@ -89,7 +89,7 @@ export type RegistryProbeResult = {
|
|||||||
|
|
||||||
export class NpmRegistryResolver {
|
export class NpmRegistryResolver {
|
||||||
config: NpmRegistryResolverConfig;
|
config: NpmRegistryResolverConfig;
|
||||||
private cache?: { registryUrl: string; expiresAt: number };
|
private cache?: { orderedUrls: string[]; expiresAt: number };
|
||||||
|
|
||||||
constructor(config?: NpmRegistryResolverConfig) {
|
constructor(config?: NpmRegistryResolverConfig) {
|
||||||
this.config = config || {};
|
this.config = config || {};
|
||||||
@@ -105,21 +105,67 @@ export class NpmRegistryResolver {
|
|||||||
}
|
}
|
||||||
const cached = this.cache;
|
const cached = this.cache;
|
||||||
if (cached && cached.expiresAt > Date.now()) {
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
return cached.registryUrl;
|
const fastUrl = cached.orderedUrls[0];
|
||||||
|
if (fastUrl) {
|
||||||
|
const probeResult = await this.probe(fastUrl);
|
||||||
|
if (probeResult.ok) {
|
||||||
|
return cached.orderedUrls[0] || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cache = undefined;
|
||||||
}
|
}
|
||||||
const candidates = (config?.candidates || []).filter(Boolean);
|
const candidates = (config?.candidates || []).filter(Boolean);
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const probes = await Promise.allSettled(candidates.map(registryUrl => this.probe(registryUrl)));
|
const orderedUrls = await this.internalProbeAll(candidates);
|
||||||
const okList = probes.map(item => (item.status === "fulfilled" ? item.value : null)).filter((item): item is RegistryProbeResult => !!item && item.ok);
|
this.cache = { orderedUrls, expiresAt: Date.now() + (config?.cacheTtlMs ?? 300_000) };
|
||||||
if (okList.length > 0) {
|
return orderedUrls[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) };
|
async resolveOrdered(): Promise<string[]> {
|
||||||
return best;
|
const config = this.config;
|
||||||
|
if (config?.mode === "fixed" && config.fixedUrl) {
|
||||||
|
return [config.fixedUrl];
|
||||||
}
|
}
|
||||||
return "";
|
if (config?.mode === "system") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const cached = this.cache;
|
||||||
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
|
const fastUrl = cached.orderedUrls[0];
|
||||||
|
if (fastUrl) {
|
||||||
|
const probeResult = await this.probe(fastUrl);
|
||||||
|
if (probeResult.ok) {
|
||||||
|
return cached.orderedUrls;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cache = undefined;
|
||||||
|
}
|
||||||
|
const candidates = (config?.candidates || []).filter(Boolean);
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const orderedUrls = await this.internalProbeAll(candidates);
|
||||||
|
this.cache = { orderedUrls, expiresAt: Date.now() + (config?.cacheTtlMs ?? 300_000) };
|
||||||
|
return orderedUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async internalProbeAll(candidates: string[]): Promise<string[]> {
|
||||||
|
const probes = await Promise.allSettled(candidates.map(registryUrl => this.probe(registryUrl)));
|
||||||
|
const okList: RegistryProbeResult[] = [];
|
||||||
|
const failList: RegistryProbeResult[] = [];
|
||||||
|
for (const item of probes) {
|
||||||
|
const result = item.status === "fulfilled" ? item.value : null;
|
||||||
|
if (result && result.ok) {
|
||||||
|
okList.push(result);
|
||||||
|
} else if (result) {
|
||||||
|
failList.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
okList.sort((a, b) => a.elapsedMs - b.elapsedMs);
|
||||||
|
failList.sort((a, b) => a.elapsedMs - b.elapsedMs);
|
||||||
|
return [...okList.map(r => r.registryUrl), ...failList.map(r => r.registryUrl)];
|
||||||
}
|
}
|
||||||
|
|
||||||
async probe(registryUrl: string): Promise<RegistryProbeResult> {
|
async probe(registryUrl: string): Promise<RegistryProbeResult> {
|
||||||
@@ -368,6 +414,7 @@ export class RuntimeDepsService {
|
|||||||
await this.ensureLazyDependency(packageName, logger);
|
await this.ensureLazyDependency(packageName, logger);
|
||||||
return this.resolveRuntimeSpecifier(specifier).resolved;
|
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||||
} catch (lazyError: any) {
|
} catch (lazyError: any) {
|
||||||
|
logger?.error?.(`动态依赖安装失败: ${lazyError.message}`);
|
||||||
return this.resolveProjectSpecifier(specifier, lazyError).resolved;
|
return this.resolveProjectSpecifier(specifier, lazyError).resolved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,30 +555,37 @@ export class RuntimeDepsService {
|
|||||||
const env = this.buildChildEnv(registryUrl);
|
const env = this.buildChildEnv(registryUrl);
|
||||||
const command = this.getPnpmCommand();
|
const command = this.getPnpmCommand();
|
||||||
const pnpmVersion = await this.getPnpmVersion(command, env);
|
const pnpmVersion = await this.getPnpmVersion(command, env);
|
||||||
const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--no-frozen-lockfile", "--reporter=append-only"];
|
const allRegistryUrls = await this.registryResolver.resolveOrdered();
|
||||||
if (registryUrl) {
|
const urlsToTry = allRegistryUrls.length > 0 ? allRegistryUrls : [""];
|
||||||
args.push(`--registry=${registryUrl}`);
|
let lastError: string | undefined;
|
||||||
|
for (const tryUrl of urlsToTry) {
|
||||||
|
const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--no-frozen-lockfile", "--reporter=append-only"];
|
||||||
|
if (tryUrl) {
|
||||||
|
args.push(`--registry=${tryUrl}`);
|
||||||
|
}
|
||||||
|
const tryEnv = tryUrl ? this.buildChildEnv(tryUrl) : env;
|
||||||
|
log.info(`开始安装第三方依赖: ${Object.keys(dependencies).join(", ")}${tryUrl ? `,镜像: ${tryUrl}` : ""}`);
|
||||||
|
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
|
||||||
|
if (result.code === 0) {
|
||||||
|
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
|
||||||
|
log.info("第三方依赖安装完成");
|
||||||
|
return { registryUrl: tryUrl, packageJsonPath };
|
||||||
|
}
|
||||||
|
lastError = result.stderr || result.stdout || "unknown error";
|
||||||
|
log.warn?.(`镜像 ${tryUrl || "默认"} 安装失败${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
|
||||||
}
|
}
|
||||||
log.info(`开始安装第三方依赖: ${Object.keys(dependencies).join(", ")}`);
|
this.writeInstallState(statePath, {
|
||||||
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env });
|
...currentState,
|
||||||
if (result.code !== 0) {
|
installedAt: currentState?.installedAt,
|
||||||
const message = result.stderr || result.stdout || "unknown error";
|
failedAt: new Date().toISOString(),
|
||||||
this.writeInstallState(statePath, {
|
registryUrl: urlsToTry[0],
|
||||||
...currentState,
|
dependenciesHash,
|
||||||
installedAt: currentState?.installedAt,
|
nodeVersion: process.version,
|
||||||
failedAt: new Date().toISOString(),
|
pnpmVersion,
|
||||||
registryUrl,
|
lockFileExists: fs.existsSync(lockPath),
|
||||||
dependenciesHash,
|
lastError,
|
||||||
nodeVersion: process.version,
|
});
|
||||||
pnpmVersion,
|
throw new Error(`动态依赖安装失败: ${lastError}`);
|
||||||
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 };
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user