From 9decb49d048272fdf1c265e8cf006ef0d4b897d9 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sat, 29 Aug 2026 02:48:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=90=8C=E6=AD=A5=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=8F=92=E4=BB=B6=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86?= =?UTF-8?q?=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 调整插件同步时的字段处理逻辑,减少冗余数据存储 2. 提前查询已有插件数据,避免重复创建和更新 3. 拆分插件同步代码结构,提升可读性和可维护性 4. 注释掉下载次数同步逻辑,暂时关闭该功能 5. 简化插件编辑接口的字段处理逻辑 6. 移除插件列表页的复制按钮,新增插件导出功能 7. 更新文档和镜像配置,修正Docker Hub镜像地址 8. 调整文档章节顺序,新增版本号徽章展示 --- .github/workflows/release-image.yml | 34 +++-- README.md | 7 +- docker/run/docker-compose.yaml | 2 +- docs/guide/install/docker/index.md | 46 +++++-- .../plugin/components/plugin-item-card.vue | 35 +++-- .../src/views/sys/plugin/index.vue | 36 +---- .../plugin/service/plugin-service.test.ts | 6 + .../modules/plugin/service/plugin-service.ts | 128 +++++++----------- 8 files changed, 139 insertions(+), 155 deletions(-) diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml index 6e8962fe2..6d6ae61cf 100644 --- a/.github/workflows/release-image.yml +++ b/.github/workflows/release-image.yml @@ -111,8 +111,8 @@ jobs: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim registry.cn-shenzhen.aliyuncs.com/certd/certd:slim registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-slim - certd/certd:slim - certd/certd:${{steps.get_certd_version.outputs.result}}-slim + greper/certd:slim + greper/certd:${{steps.get_certd_version.outputs.result}}-slim ghcr.io/${{ github.repository }}:slim ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim @@ -129,22 +129,26 @@ jobs: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7 registry.cn-shenzhen.aliyuncs.com/certd/certd:armv7 registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-armv7 - certd/certd:armv7 - certd/certd:${{steps.get_certd_version.outputs.result}}-armv7 + greper/certd:armv7 + greper/certd:${{steps.get_certd_version.outputs.result}}-armv7 ghcr.io/${{ github.repository }}:armv7 ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7 - # - name: Build agent - # uses: docker/build-push-action@v6 - # with: - # platforms: linux/amd64,linux/arm64 - # push: true - # context: ./packages/ui/agent/ - # tags: | - # registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:latest - # registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:${{steps.get_certd_version.outputs.result}} - # greper/certd-agent:latest - # greper/certd-agent:${{steps.get_certd_version.outputs.result}} + - name: Login to Certd Docker Hub account + uses: docker/login-action@v3 + with: + username: ${{ secrets.dockerhub_username2 }} + password: ${{ secrets.dockerhub_password2 }} + + - name: Publish to certd Docker Hub repository + run: | + docker buildx imagetools create --tag certd/certd:latest greper/certd:latest + docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }} greper/certd:${{ steps.get_certd_version.outputs.result }} + docker buildx imagetools create --tag certd/certd:slim greper/certd:slim + docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }}-slim greper/certd:${{ steps.get_certd_version.outputs.result }}-slim + docker buildx imagetools create --tag certd/certd:armv7 greper/certd:armv7 + docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }}-armv7 greper/certd:${{ steps.get_certd_version.outputs.result }}-armv7 + - name: deploy-certd-doc uses: tyrrrz/action-http-request@prime with: diff --git a/README.md b/README.md index de6b24e86..688519405 100644 --- a/README.md +++ b/README.md @@ -130,12 +130,12 @@ registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable] | slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | | armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | -##### 2. 镜像地址: +##### 3. 镜像地址: | 镜像仓库 | 最新预览版 | slim | armv7 | | --- | --- | --- | --- | | 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` | -| Docker Hub | `greper/certd:latest` | `certd:slim` | +| Docker Hub | `certd/certd:latest` | `certd:slim` | | GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` | @@ -144,6 +144,9 @@ registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable] > 2. 稳定版在后面加 `-stable` 即可。 > 3. 如需指定具体的版本号,在冒号后面加 `version-`即可,例如 `certd:1.42.1-stable`。 +最新版本号: +![GitHub release (latest by date)](https://img.shields.io/github/v/release/certd/certd?label=Latest%20Release&sort=semver) + ##### 3. 镜像构建说明: diff --git a/docker/run/docker-compose.yaml b/docker/run/docker-compose.yaml index 5e43f6da8..f665d54e2 100644 --- a/docker/run/docker-compose.yaml +++ b/docker/run/docker-compose.yaml @@ -4,7 +4,7 @@ services: # 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0 image: registry.cn-shenzhen.aliyuncs.com/certd/certd:latest # image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源 - # image: greper/certd:latest + # image: certd/certd:latest # --------- 生产建议使用稳定版, latest改成stable即可 # image: registry.cn-shenzhen.aliyuncs.com/certd/certd:stable diff --git a/docs/guide/install/docker/index.md b/docs/guide/install/docker/index.md index e627bece4..55ec3fbe4 100644 --- a/docs/guide/install/docker/index.md +++ b/docs/guide/install/docker/index.md @@ -6,22 +6,48 @@ Certd 提供多种 Docker 镜像版本,您可以根据需要选择: -**最新版本:** +#### 1. 镜像地址格式: + +``` +registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable] +------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型 +``` +#### 2. 版本标签: + +**最新版本标签:** | 版本 | 标签 | 说明 | | --- | --- | --- | -| 预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 | -| 稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 | +| 最新预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 | +| 最新稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 | -**系统版本分支:** +**系统分支版本:** -| 分支版本标签 | 基础系统 | 说明 | 指定版本 | 稳定版 | -| --- | --- | --- | --- | --- | -| `certd:latest` 【默认】 | Alpine Linux | 默认版本,镜像体积小 | `certd:[version]` | `certd:[version-]stable` | -| `certd:slim` | Debian slim | glibc版本,dns解析兼容性更好(可能需要配置security_opt -seccomp=unconfined)| `certd:[version-]slim` | `certd:[version-]slim-stable` | -| `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:[version]-armv7` | `certd:[version-]armv7-ststable` | +> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的) + +| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | +| --- | --- | --- | --- | --- | +| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` | +| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | +| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | + +#### 3. 镜像地址: + +| 镜像仓库 | 最新预览版 | slim | armv7 | +| --- | --- | --- | +| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` | +| Docker Hub | `certd/certd:latest` | `certd:slim` | `certd:armv7` | +| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` | + + +> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。 +> 2. 稳定版在后面加 `-stable` 即可。 +> 3. 如需指定具体的版本号,在冒号后面加 `version-`即可,例如 `certd:1.42.1-stable`。 +> 4. 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。 + +最新版本号: +![GitHub release (latest by date)](https://img.shields.io/github/v/release/certd/certd?label=Latest%20Release&sort=semver) -> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。 ### 一键脚本安装(推荐) diff --git a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-item-card.vue b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-item-card.vue index a42c7493c..d6d150655 100644 --- a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-item-card.vue +++ b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-item-card.vue @@ -29,7 +29,7 @@
-
+
- - + + @@ -160,11 +160,9 @@ const props = withDefaults( simple?: boolean; showConfig?: boolean; editable?: boolean; - copyHandler?: (plugin: any) => void | Promise; }>(), { source: "market", - copyHandler: undefined, } ); @@ -183,7 +181,7 @@ const { resolveOnlinePluginDependencies, installOnlinePluginChain, openDependenc const actionLoading = ref(""); const editDialogBodyRef = ref(); -type PluginCardAction = "edit" | "copy" | "publish" | "config" | "install" | "uninstall" | "remove" | "toggle"; +type PluginCardAction = "edit" | "export" | "publish" | "config" | "install" | "uninstall" | "remove" | "toggle"; const pluginCardState = computed(() => { const isLocal = props.source === "local"; @@ -200,7 +198,7 @@ const pluginCardState = computed(() => { canEditPlugin, isAvailableLocally, canManagePlugin, - canCopyPlugin: canManagePlugin, + canExportPlugin: canManagePlugin, showEditButton: canManagePlugin, showConfigButton: !!props.showConfig, isInstalled: isCloudPlugin && !!props.plugin.installed, @@ -354,16 +352,26 @@ async function editPlugin() { }); } -async function copyPlugin() { - if (!props.copyHandler) { +async function exportPlugin() { + if (!pluginCardState.value.canExportPlugin || !props.plugin.id) { return; } await runAction( - "copy", + "export", async () => { - await props.copyHandler?.(props.plugin); + const content = await api.ExportPlugin(props.plugin.id); + if (!content) { + return; + } + const blob = new Blob([content], { type: "text/yaml;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `${props.plugin.name || props.plugin.fullName || "plugin"}.yaml`; + link.click(); + URL.revokeObjectURL(url); }, - { emitChanged: true } + { emitChanged: false } ); } @@ -1052,5 +1060,4 @@ function handleVersionClick() { } } } - diff --git a/packages/ui/certd-client/src/views/sys/plugin/index.vue b/packages/ui/certd-client/src/views/sys/plugin/index.vue index 4a20efdea..39b215321 100644 --- a/packages/ui/certd-client/src/views/sys/plugin/index.vue +++ b/packages/ui/certd-client/src/views/sys/plugin/index.vue @@ -9,17 +9,7 @@
- +
@@ -80,30 +70,8 @@ async function handleDetailInstalled() { crudExpose.doRefresh(); } -async function copyPlugin(row: any) { - const copyRow = { ...row }; - delete copyRow.fullName; - delete copyRow.id; - delete copyRow.developerId; - delete copyRow.appId; - delete copyRow.latest; - delete copyRow.status; - delete copyRow.downloadCount; - delete copyRow.score; - await crudExpose.openCopy( - { - row: copyRow, - }, - { - async onSuccess() { - crudExpose.doRefresh(); - }, - } - ); -} - async function handlePluginChanged(payload: { action: string }) { - if (payload.action === "install" || payload.action === "uninstall" || payload.action === "remove" || payload.action === "copy") { + if (payload.action === "install" || payload.action === "uninstall" || payload.action === "remove") { await pluginStore.reload(); } crudExpose.doRefresh(); diff --git a/packages/ui/certd-server/src/modules/plugin/service/plugin-service.test.ts b/packages/ui/certd-server/src/modules/plugin/service/plugin-service.test.ts index b4e0ab460..8690dc697 100644 --- a/packages/ui/certd-server/src/modules/plugin/service/plugin-service.test.ts +++ b/packages/ui/certd-server/src/modules/plugin/service/plugin-service.test.ts @@ -13,6 +13,7 @@ type RepoOptions = { function createPluginRepositoryMock(options: RepoOptions = {}) { const state = { savedRows: [] as any[], + saveCalls: [] as any[][], updates: [] as any[], findCalls: 0, queryRows: options.queryRows || options.marketRows || [], @@ -198,6 +199,7 @@ function createPluginRepositoryMock(options: RepoOptions = {}) { }, async save(records: any[] | any) { const list = Array.isArray(records) ? records : [records]; + state.saveCalls.push(list); state.savedRows.push(...list); return records; }, @@ -538,6 +540,10 @@ describe("PluginService online plugins", () => { const res = await service.syncOnlinePluginList(); assert.equal(requestCalls.length, 2); + assert.deepEqual( + repository.state.saveCalls.map(records => records.length), + [200, 1] + ); assert.equal(res.length, 201); assert.equal(repository.state.savedRows.length, 201); assert.equal(repository.state.savedRows[0].fullName, "developer/plugin-1"); diff --git a/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts b/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts index 385f6bf0f..8b3ff8af8 100644 --- a/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts +++ b/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts @@ -795,13 +795,6 @@ export class PluginService extends BaseService { extra: Object.keys(extra).length > 0 ? yaml.dump(extra) : null, syncTime, type: old?.type || "store", - disabled: old?.disabled ?? false, - installed: old?.installed ?? false, - setting: old?.setting, - sysSetting: old?.sysSetting, - content: old?.content, - metadata: old?.metadata, - version: old?.version, updateTime: new Date(), }); } @@ -971,8 +964,20 @@ export class PluginService extends BaseService { await this.plusService.register(); // 只同步市场列表元数据,插件 YAML 内容仍然在安装时按版本下载。 - const pluginMap = new Map(); const createdAtLt = Date.now(); + const existingList = await this.find({ + where: { + type: "store", + }, + }); + const existingMap = new Map(); + for (const item of existingList) { + const fullName = item.fullName || this.getOnlinePluginFullName(item as any); + if (fullName) { + existingMap.set(fullName, item); + } + } + const syncTime = new Date().getTime(); let pageStart = 0; while (true) { const res = await this.plusService.request({ @@ -993,6 +998,7 @@ export class PluginService extends BaseService { }, }); const list = res?.list || []; + const pluginMap = new Map(); for (const item of list) { const record = { ...item }; const fullName = this.getOnlinePluginFullName(record); @@ -1002,6 +1008,15 @@ export class PluginService extends BaseService { record.fullName = fullName; pluginMap.set(fullName, record); } + const records = Array.from(pluginMap.values()).map(item => { + return this.normalizeOnlinePluginRecord(item, syncTime, existingMap.get(item.fullName)); + }); + if (records.length > 0) { + await this.addOrUpdate(records); + for (const record of records) { + existingMap.set(record.fullName, record); + } + } const total = Number(res?.page?.total || 0); if (total > 0) { pageStart += ONLINE_PLUGIN_SYNC_PAGE_SIZE; @@ -1015,23 +1030,6 @@ export class PluginService extends BaseService { } pageStart += ONLINE_PLUGIN_SYNC_PAGE_SIZE; } - const existingList = await this.find({ - where: { - type: "store", - }, - }); - const existingMap = new Map(); - for (const item of existingList) { - const fullName = item.fullName || this.getOnlinePluginFullName(item as any); - if (fullName) { - existingMap.set(fullName, item); - } - } - const syncTime = Date.now(); - const records = Array.from(pluginMap.values()).map(item => { - return this.normalizeOnlinePluginRecord(item, syncTime, existingMap.get(item.fullName)); - }); - await this.addOrUpdate(records); await this.saveOnlinePluginSyncTime(syncTime); return await this.onlinePluginList({}); } @@ -1074,7 +1072,7 @@ export class PluginService extends BaseService { override: true, type: "store", }); - await this.refreshOnlinePluginDownloadCount(res.fullName || fullName, res.plugin?.downloadCount); + // await this.refreshOnlinePluginDownloadCount(res.fullName || fullName, res.plugin?.downloadCount); return { ...importRes, fullName: res.fullName || fullName, @@ -1289,20 +1287,20 @@ export class PluginService extends BaseService { await this.sysSettingsService.saveSetting(setting); } - private async refreshOnlinePluginDownloadCount(fullName: string, downloadCount?: number) { - if (!fullName || downloadCount == null) { - return; - } - await this.updateWhere( - { - type: "store", - fullName, - }, - { - downloadCount, - } - ); - } + // private async refreshOnlinePluginDownloadCount(fullName: string, downloadCount?: number) { + // if (!fullName || downloadCount == null) { + // return; + // } + // await this.updateWhere( + // { + // type: "store", + // fullName, + // }, + // { + // downloadCount, + // } + // ); + // } async unRegisterById(id: any) { const item = await this.info(id); @@ -1346,50 +1344,27 @@ export class PluginService extends BaseService { throw new Error("id 不能为空"); } // 编辑接口只允许修改插件自身内容,市场同步字段始终以数据库记录为准。 - const current = this.repository?.findOneBy ? await this.info(param.id) : null; - const source = { ...(current || {}), ...param }; - source.type = normalizePluginSourceType(current?.type || param.type); - if (source.type === "store") { - this.normalizeStorePluginAuthor(source); - } + const source = { ...param }; + source.type = normalizePluginSourceType(source.type); + this.normalizeStorePluginAuthor(source); let old: PluginEntity | null; - if (source.type === "store" && source.fullName) { - old = await this.findOne({ - where: { - type: "store", - fullName: source.fullName, - }, - }); - } else { - old = await this.findOne({ - where: { - name: source.name, - author: source.author, - }, - }); - } + old = await this.findOne({ + where: { + type: "store", + fullName: source.fullName, + }, + }); if (old && old.id !== param.id) { throw new Error(`插件${source.author}/${source.name}已存在`); } - const editableFields = ["name", "author", "fullName", "icon", "title", "group", "desc", "setting", "sysSetting", "content", "version", "pluginType", "metadata", "extra", "dependPlugins", "vip", "disabled"]; - const updateData: any = { id: param.id }; - for (const field of editableFields) { - if (Object.prototype.hasOwnProperty.call(param, field)) { - updateData[field] = param[field]; - } - } + const updateData: any = {...source, id: param.id }; this.normalizePluginYamlParam(updateData, "extra"); this.normalizeDependPluginsParam(updateData); - updateData.type = source.type; - updateData.author = source.author; - updateData.name = source.name; - updateData.fullName = source.fullName; await this.unRegisterById(param.id); const res = await super.update(updateData); - await this.registerById(param.id); return res; } @@ -1647,17 +1622,12 @@ export class PluginService extends BaseService { extra: yaml.dump(extra), content: loaded.content, installed: true, - latest: old?.latest ?? loaded.latest, - status: old?.status ?? loaded.status, - downloadCount: old?.downloadCount ?? loaded.downloadCount, - score: old?.score ?? loaded.score, - syncTime: old?.syncTime ?? loaded.syncTime, + syncTime: new Date().getTime(), disabled: old?.disabled ?? false, }; if (!pluginEntity.pluginType) { throw new Error(`插件类型不能为空`); } - if (!old) { //add const { id } = await this.add(pluginEntity);