From 6eb519d47d433e3127936d4a811a94bc6f05d575 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Sun, 1 Sep 2024 23:55:51 +0800 Subject: [PATCH] docs: update documentation for Halo 2.18 Signed-off-by: Ryan Wang --- docs/developer-guide/core/build.md | 2 +- .../getting-started/install/docker-compose.md | 12 +- docs/getting-started/install/docker.md | 6 +- docs/getting-started/install/jar-file.md | 4 +- docs/getting-started/install/other/traefik.md | 2 +- docs/getting-started/install/podman.md | 12 +- .../install/slots/_docker-registry-list.md | 8 +- docs/intro.md | 2 +- docs/user-guide/faq.md | 4 +- docusaurus.config.js | 49 +- i18n/zh-Hans/code.json | 62 +- .../current.json | 2 +- .../version-2.19.json | 78 +++ .../docusaurus-theme-classic/footer.json | 6 +- versioned_docs/version-2.19/about.md | 16 + .../version-2.19/contribution/issue.md | 28 + .../version-2.19/contribution/pr.md | 110 ++++ .../version-2.19/contribution/sponsor.md | 26 + .../developer-guide/annotations-form.md | 89 +++ .../developer-guide/core/build.md | 84 +++ .../developer-guide/core/code-style.md | 30 + .../developer-guide/core/prepare.md | 25 + .../version-2.19/developer-guide/core/run.md | 122 ++++ .../developer-guide/core/structure.md | 36 ++ .../developer-guide/form-schema.md | 559 +++++++++++++++++ .../api-reference/server/extension-client.md | 222 +++++++ .../api-reference/server/extension-getter.md | 72 +++ .../extension-points/additional-webfilter.md | 76 +++ .../server/extension-points/attachment.md | 55 ++ .../authentication-webfilter.md | 81 +++ .../extension-points/comment-subject.md | 83 +++ .../server/extension-points/comment-widget.md | 41 ++ .../server/extension-points/index.md | 65 ++ .../server/extension-points/notifier.md | 60 ++ .../server/extension-points/post-content.md | 43 ++ .../extension-points/singlepage-content.md | 38 ++ .../template-footer-processor.md | 77 +++ .../template-head-processor.md | 87 +++ ...sername-password-authentication-manager.md | 30 + .../plugin/api-reference/server/extension.md | 305 +++++++++ .../api-reference/server/finder-for-theme.md | 60 ++ .../server/login-handler-enhancer.md | 63 ++ .../plugin/api-reference/server/reconciler.md | 365 +++++++++++ .../api-reference/server/reverseproxy.md | 36 ++ .../api-reference/server/role-template.md | 307 ++++++++++ .../api-reference/server/setting-fetcher.md | 139 +++++ .../server/template-for-theme.md | 90 +++ .../plugin/api-reference/server/websocket.md | 46 ++ .../plugin/api-reference/ui/api-request.md | 70 +++ .../ui/components/annotations-form.md | 55 ++ .../components/attachment-file-type-icon.md | 25 + .../components/attachment-selector-modal.md | 50 ++ .../ui/components/filter-clean-button.md | 18 + .../ui/components/filter-dropdown.md | 48 ++ .../ui/components/has-permission.md | 26 + .../api-reference/ui/components/index.md | 42 ++ .../ui/components/plugin-detail-modal.md | 30 + .../ui/components/search-input.md | 33 + .../ui/components/uppy-upload.md | 47 ++ .../ui/components/v-codemirror.md | 36 ++ .../ui/components/v-permission.md | 18 + .../api-reference/ui/components/v-tooltip.md | 18 + .../attachment-list-item-operation-create.md | 98 +++ .../attachment-selector-create.md | 146 +++++ .../backup-list-item-operation-create.md | 41 ++ .../ui/extension-points/backup-tabs-create.md | 36 ++ .../comment-list-item-operation-create.md | 81 +++ .../comment-subject-ref-create.md | 114 ++++ .../default-editor-extension-create.md | 479 +++++++++++++++ .../ui/extension-points/editor-create.md | 188 ++++++ .../ui/extension-points/index.md | 14 + .../extension-points/interface/Attachment.md | 25 + .../interface/ListedComment.md | 64 ++ .../extension-points/interface/ListedPost.md | 119 ++++ .../extension-points/interface/ListedReply.md | 49 ++ .../interface/OperationItem.md | 12 + .../ui/extension-points/interface/Plugin.md | 50 ++ .../ui/extension-points/interface/Theme.md | 63 ++ .../plugin-installation-tabs-create.md | 44 ++ .../plugin-list-item-field-create.md | 84 +++ .../plugin-list-item-operation-create.md | 55 ++ .../plugin-self-tabs-create.md | 90 +++ .../post-list-item-field-create.md | 80 +++ .../post-list-item-operation-create.md | 92 +++ .../reply-list-item-operation-create.md | 81 +++ .../theme-list-item-operation-create.md | 91 +++ .../theme-list-tabs-create.md | 44 ++ .../uc-user-profile-tabs-create.md | 41 ++ .../user-detail-tabs-create.md | 41 ++ .../plugin/api-reference/ui/route.md | 120 ++++ .../developer-guide/plugin/appendices.md | 4 + .../developer-guide/plugin/basics/devtools.md | 124 ++++ .../plugin/basics/framework.md | 96 +++ .../developer-guide/plugin/basics/manifest.md | 102 ++++ .../plugin/basics/server/lifecycle.md | 46 ++ .../plugin/basics/server/object-management.md | 57 ++ .../plugin/basics/structure.md | 69 +++ .../developer-guide/plugin/basics/ui/entry.md | 126 ++++ .../developer-guide/plugin/basics/ui/intro.md | 12 + .../plugin/examples/todolist.md | 577 ++++++++++++++++++ .../developer-guide/plugin/hello-world.md | 145 +++++ .../developer-guide/plugin/introduction.md | 6 + .../developer-guide/plugin/prepare.md | 15 + .../developer-guide/plugin/publish.md | 160 +++++ .../developer-guide/theme/annotations.md | 64 ++ .../developer-guide/theme/code-snippets.md | 47 ++ .../developer-guide/theme/config.md | 95 +++ .../developer-guide/theme/finder-apis.md | 10 + .../theme/finder-apis/category.md | 216 +++++++ .../theme/finder-apis/comment.md | 155 +++++ .../theme/finder-apis/contributor.md | 64 ++ .../developer-guide/theme/finder-apis/menu.md | 87 +++ .../theme/finder-apis/plugin.md | 63 ++ .../developer-guide/theme/finder-apis/post.md | 460 ++++++++++++++ .../theme/finder-apis/single-page.md | 131 ++++ .../theme/finder-apis/site-stats.md | 45 ++ .../developer-guide/theme/finder-apis/tag.md | 139 +++++ .../theme/finder-apis/theme.md | 119 ++++ .../developer-guide/theme/global-variables.md | 58 ++ .../theme/image-optimization.md | 80 +++ .../developer-guide/theme/prepare.md | 111 ++++ .../developer-guide/theme/settings.md | 139 +++++ .../developer-guide/theme/static-resources.md | 55 ++ .../developer-guide/theme/structure.md | 33 + .../theme/template-route-mapping.md | 87 +++ .../developer-guide/theme/template-tag.md | 56 ++ .../theme/template-variables.md | 7 + .../theme/template-variables/archives.md | 108 ++++ .../theme/template-variables/author.md | 104 ++++ .../theme/template-variables/categories.md | 54 ++ .../theme/template-variables/category.md | 105 ++++ .../theme/template-variables/error.md | 45 ++ .../theme/template-variables/index_.md | 98 +++ .../theme/template-variables/page.md | 53 ++ .../theme/template-variables/post.md | 65 ++ .../theme/template-variables/tag.md | 105 ++++ .../theme/template-variables/tags.md | 39 ++ .../theme/vo/_CategoryTreeVo.md | 33 + .../developer-guide/theme/vo/_CategoryVo.md | 31 + .../developer-guide/theme/vo/_CommentVo.md | 53 ++ .../developer-guide/theme/vo/_ContentVo.md | 6 + .../theme/vo/_ContributorVo.md | 19 + .../developer-guide/theme/vo/_ListedPostVo.md | 65 ++ .../theme/vo/_ListedSinglePageVo.md | 57 ++ .../developer-guide/theme/vo/_MenuItemVo.md | 44 ++ .../developer-guide/theme/vo/_MenuVo.md | 21 + .../developer-guide/theme/vo/_PostVo.md | 66 ++ .../developer-guide/theme/vo/_ReplyVo.md | 42 ++ .../developer-guide/theme/vo/_SinglePageVo.md | 58 ++ .../theme/vo/_SiteSettingVo.md | 26 + .../developer-guide/theme/vo/_TagVo.md | 26 + .../developer-guide/theme/vo/_ThemeVo.md | 36 ++ .../developer-guide/theme/vo/_UserVo.md | 28 + .../getting-started/first-post.md | 32 + .../getting-started/install/1panel.md | 78 +++ .../install/cloud/tencent-cloud-lighthouse.md | 71 +++ .../getting-started/install/docker-compose.md | 333 ++++++++++ .../getting-started/install/docker.md | 79 +++ .../getting-started/install/helm.md | 142 +++++ .../getting-started/install/jar-file.md | 291 +++++++++ .../getting-started/install/offline.md | 106 ++++ .../install/other/nginxproxymanager.md | 164 +++++ .../getting-started/install/other/traefik.md | 117 ++++ .../getting-started/install/podman.md | 224 +++++++ .../install/slots/_docker-args.md | 16 + .../install/slots/_docker-registry-list.md | 15 + .../getting-started/migrate-from-1.x.md | 80 +++ .../version-2.19/getting-started/prepare.md | 124 ++++ .../version-2.19/getting-started/setup.md | 18 + versioned_docs/version-2.19/intro.md | 78 +++ .../version-2.19/user-guide/app-store.md | 70 +++ .../version-2.19/user-guide/attachments.md | 108 ++++ .../version-2.19/user-guide/backup.md | 60 ++ .../version-2.19/user-guide/common.md | 67 ++ versioned_docs/version-2.19/user-guide/faq.md | 134 ++++ .../version-2.19/user-guide/pages.md | 20 + .../version-2.19/user-guide/plugins.md | 87 +++ .../version-2.19/user-guide/posts.md | 139 +++++ .../version-2.19/user-guide/settings.md | 113 ++++ .../version-2.19/user-guide/themes.md | 98 +++ .../version-2.19/user-guide/user-center.md | 61 ++ .../version-2.19/user-guide/users.md | 118 ++++ versioned_sidebars/version-2.19-sidebars.json | 336 ++++++++++ versions.json | 1 + 184 files changed, 15316 insertions(+), 84 deletions(-) create mode 100644 i18n/zh-Hans/docusaurus-plugin-content-docs/version-2.19.json create mode 100644 versioned_docs/version-2.19/about.md create mode 100644 versioned_docs/version-2.19/contribution/issue.md create mode 100644 versioned_docs/version-2.19/contribution/pr.md create mode 100644 versioned_docs/version-2.19/contribution/sponsor.md create mode 100644 versioned_docs/version-2.19/developer-guide/annotations-form.md create mode 100644 versioned_docs/version-2.19/developer-guide/core/build.md create mode 100644 versioned_docs/version-2.19/developer-guide/core/code-style.md create mode 100644 versioned_docs/version-2.19/developer-guide/core/prepare.md create mode 100644 versioned_docs/version-2.19/developer-guide/core/run.md create mode 100644 versioned_docs/version-2.19/developer-guide/core/structure.md create mode 100644 versioned_docs/version-2.19/developer-guide/form-schema.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-client.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-getter.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/additional-webfilter.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/attachment.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/authentication-webfilter.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/comment-subject.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/comment-widget.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/index.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/notifier.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/post-content.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/singlepage-content.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/template-footer-processor.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/template-head-processor.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/username-password-authentication-manager.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/finder-for-theme.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/login-handler-enhancer.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/reconciler.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/reverseproxy.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/role-template.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/setting-fetcher.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/template-for-theme.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/websocket.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/api-request.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/annotations-form.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/attachment-file-type-icon.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/attachment-selector-modal.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/filter-clean-button.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/filter-dropdown.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/has-permission.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/index.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/plugin-detail-modal.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/search-input.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/uppy-upload.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/v-codemirror.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/v-permission.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/v-tooltip.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/attachment-list-item-operation-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/attachment-selector-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/backup-list-item-operation-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/backup-tabs-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/comment-list-item-operation-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/comment-subject-ref-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/default-editor-extension-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/editor-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/index.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/interface/Attachment.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/interface/ListedComment.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/interface/ListedPost.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/interface/ListedReply.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/interface/OperationItem.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/interface/Plugin.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/interface/Theme.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/plugin-installation-tabs-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/plugin-list-item-field-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/plugin-list-item-operation-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/plugin-self-tabs-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/post-list-item-field-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/post-list-item-operation-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/reply-list-item-operation-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/theme-list-item-operation-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/theme-list-tabs-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/uc-user-profile-tabs-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/extension-points/user-detail-tabs-create.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/route.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/appendices.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/basics/devtools.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/basics/framework.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/basics/manifest.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/basics/server/lifecycle.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/basics/server/object-management.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/basics/structure.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/basics/ui/entry.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/basics/ui/intro.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/examples/todolist.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/hello-world.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/introduction.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/prepare.md create mode 100644 versioned_docs/version-2.19/developer-guide/plugin/publish.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/annotations.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/code-snippets.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/config.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis/category.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis/comment.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis/contributor.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis/menu.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis/plugin.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis/post.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis/single-page.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis/site-stats.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis/tag.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/finder-apis/theme.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/global-variables.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/image-optimization.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/prepare.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/settings.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/static-resources.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/structure.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-route-mapping.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-tag.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables/archives.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables/author.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables/categories.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables/category.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables/error.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables/index_.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables/page.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables/post.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables/tag.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/template-variables/tags.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_CategoryTreeVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_CategoryVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_CommentVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_ContentVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_ContributorVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_ListedPostVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_ListedSinglePageVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_MenuItemVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_MenuVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_PostVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_ReplyVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_SinglePageVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_SiteSettingVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_TagVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_ThemeVo.md create mode 100644 versioned_docs/version-2.19/developer-guide/theme/vo/_UserVo.md create mode 100644 versioned_docs/version-2.19/getting-started/first-post.md create mode 100644 versioned_docs/version-2.19/getting-started/install/1panel.md create mode 100644 versioned_docs/version-2.19/getting-started/install/cloud/tencent-cloud-lighthouse.md create mode 100644 versioned_docs/version-2.19/getting-started/install/docker-compose.md create mode 100644 versioned_docs/version-2.19/getting-started/install/docker.md create mode 100644 versioned_docs/version-2.19/getting-started/install/helm.md create mode 100644 versioned_docs/version-2.19/getting-started/install/jar-file.md create mode 100644 versioned_docs/version-2.19/getting-started/install/offline.md create mode 100644 versioned_docs/version-2.19/getting-started/install/other/nginxproxymanager.md create mode 100644 versioned_docs/version-2.19/getting-started/install/other/traefik.md create mode 100644 versioned_docs/version-2.19/getting-started/install/podman.md create mode 100644 versioned_docs/version-2.19/getting-started/install/slots/_docker-args.md create mode 100644 versioned_docs/version-2.19/getting-started/install/slots/_docker-registry-list.md create mode 100644 versioned_docs/version-2.19/getting-started/migrate-from-1.x.md create mode 100644 versioned_docs/version-2.19/getting-started/prepare.md create mode 100644 versioned_docs/version-2.19/getting-started/setup.md create mode 100644 versioned_docs/version-2.19/intro.md create mode 100644 versioned_docs/version-2.19/user-guide/app-store.md create mode 100644 versioned_docs/version-2.19/user-guide/attachments.md create mode 100644 versioned_docs/version-2.19/user-guide/backup.md create mode 100644 versioned_docs/version-2.19/user-guide/common.md create mode 100644 versioned_docs/version-2.19/user-guide/faq.md create mode 100644 versioned_docs/version-2.19/user-guide/pages.md create mode 100644 versioned_docs/version-2.19/user-guide/plugins.md create mode 100644 versioned_docs/version-2.19/user-guide/posts.md create mode 100644 versioned_docs/version-2.19/user-guide/settings.md create mode 100644 versioned_docs/version-2.19/user-guide/themes.md create mode 100644 versioned_docs/version-2.19/user-guide/user-center.md create mode 100644 versioned_docs/version-2.19/user-guide/users.md create mode 100644 versioned_sidebars/version-2.19-sidebars.json diff --git a/docs/developer-guide/core/build.md b/docs/developer-guide/core/build.md index 110cf87e..ce5578e8 100644 --- a/docs/developer-guide/core/build.md +++ b/docs/developer-guide/core/build.md @@ -33,7 +33,7 @@ git checkout ${branch_name} ## 构建 Fat Jar -构建之前需要修改 `gradle.properties` 中的 `version` 属性(推荐遵循 [SemVer 规范](https://semver.org/)),例如:`version=2.18.0` +构建之前需要修改 `gradle.properties` 中的 `version` 属性(推荐遵循 [SemVer 规范](https://semver.org/)),例如:`version=2.19.0` ```bash cd path/to/halo diff --git a/docs/getting-started/install/docker-compose.md b/docs/getting-started/install/docker-compose.md index cb0c615c..5dd9ec85 100644 --- a/docs/getting-started/install/docker-compose.md +++ b/docs/getting-started/install/docker-compose.md @@ -52,7 +52,7 @@ import DockerRegistryList from "./slots/_docker-registry-list.md" services: halo: - image: registry.fit2cloud.com/halo/halo:2.18 + image: registry.fit2cloud.com/halo/halo:2.19 restart: on-failure:3 depends_on: halodb: @@ -108,7 +108,7 @@ import DockerRegistryList from "./slots/_docker-registry-list.md" services: halo: - image: registry.fit2cloud.com/halo/halo:2.18 + image: registry.fit2cloud.com/halo/halo:2.19 restart: on-failure:3 depends_on: halodb: @@ -175,7 +175,7 @@ import DockerRegistryList from "./slots/_docker-registry-list.md" services: halo: - image: registry.fit2cloud.com/halo/halo:2.18 + image: registry.fit2cloud.com/halo/halo:2.19 restart: on-failure:3 volumes: - ./halo2:/root/.halo2 @@ -198,7 +198,7 @@ import DockerRegistryList from "./slots/_docker-registry-list.md" services: halo: - image: registry.fit2cloud.com/halo/halo:2.18 + image: registry.fit2cloud.com/halo/halo:2.19 restart: on-failure:3 network_mode: "host" volumes: @@ -249,7 +249,7 @@ import DockerRegistryList from "./slots/_docker-registry-list.md" ```yaml {3} services: halo: - image: registry.fit2cloud.com/halo/halo:2.18 + image: registry.fit2cloud.com/halo/halo:2.19 ``` ```bash @@ -313,7 +313,7 @@ networks: services: halo: - image: registry.fit2cloud.com/halo/halo:2.18 + image: registry.fit2cloud.com/halo/halo:2.19 restart: on-failure:3 volumes: - ./halo2:/root/.halo2 diff --git a/docs/getting-started/install/docker.md b/docs/getting-started/install/docker.md index f17c75b2..d6f34e04 100644 --- a/docs/getting-started/install/docker.md +++ b/docs/getting-started/install/docker.md @@ -31,7 +31,7 @@ import DockerRegistryList from "./slots/_docker-registry-list.md" 1. 创建容器 ```bash - docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 registry.fit2cloud.com/halo/halo:2.18 + docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 registry.fit2cloud.com/halo/halo:2.19 ``` :::info @@ -60,7 +60,7 @@ import DockerRegistryList from "./slots/_docker-registry-list.md" 2. 拉取新版本镜像 ```bash - docker pull registry.fit2cloud.com/halo/halo:2.18 + docker pull registry.fit2cloud.com/halo/halo:2.19 ``` 3. 停止运行中的容器 @@ -75,5 +75,5 @@ import DockerRegistryList from "./slots/_docker-registry-list.md" 修改版本号后,按照最初安装的方式,重新创建容器即可。 ```bash - docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 registry.fit2cloud.com/halo/halo:2.18 + docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 registry.fit2cloud.com/halo/halo:2.19 ``` diff --git a/docs/getting-started/install/jar-file.md b/docs/getting-started/install/jar-file.md index 5aea244f..595966bf 100644 --- a/docs/getting-started/install/jar-file.md +++ b/docs/getting-started/install/jar-file.md @@ -53,7 +53,7 @@ title: 使用 JAR 文件部署 3. 下载运行包 ```bash - wget https://dl.halo.run/release/halo-2.18.0.jar -O halo.jar + wget https://dl.halo.run/release/halo-2.19.0.jar -O halo.jar ``` :::info @@ -244,7 +244,7 @@ journalctl -n 20 -u halo 3. 下载新版本的 Halo 运行包,覆盖原有的运行包 ```bash - wget https://dl.halo.run/release/halo-2.18.0.jar -O /home/halo/app/halo.jar + wget https://dl.halo.run/release/halo-2.19.0.jar -O /home/halo/app/halo.jar ``` 4. 启动 Halo 服务 diff --git a/docs/getting-started/install/other/traefik.md b/docs/getting-started/install/other/traefik.md index f45b0b50..70b82d2e 100644 --- a/docs/getting-started/install/other/traefik.md +++ b/docs/getting-started/install/other/traefik.md @@ -96,7 +96,7 @@ networks: services: halo: - image: registry.fit2cloud.com/halo/halo:2.18 + image: registry.fit2cloud.com/halo/halo:2.19 container_name: halo restart: on-failure:3 volumes: diff --git a/docs/getting-started/install/podman.md b/docs/getting-started/install/podman.md index f1498287..2c54f301 100644 --- a/docs/getting-started/install/podman.md +++ b/docs/getting-started/install/podman.md @@ -57,7 +57,7 @@ Podman 采用无守护进程的包容性架构,因此可以更安全、更简 ```bash mkdir -p ~/.halo2 - podman run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 registry.fit2cloud.com/halo/halo:2.18 + podman run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 registry.fit2cloud.com/halo/halo:2.19 ``` :::info @@ -86,7 +86,7 @@ Podman 采用无守护进程的包容性架构,因此可以更安全、更简 2. 拉取新版本镜像 ```bash - podman pull registry.fit2cloud.com/halo/halo:2.18 + podman pull registry.fit2cloud.com/halo/halo:2.19 ``` 3. 停止运行中的容器 @@ -101,7 +101,7 @@ Podman 采用无守护进程的包容性架构,因此可以更安全、更简 修改版本号后,按照最初安装的方式,重新创建容器即可。 ```bash - podman run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 registry.fit2cloud.com/halo/halo:2.18 + podman run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 registry.fit2cloud.com/halo/halo:2.19 ``` ## 使用 [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) @@ -137,7 +137,7 @@ Environment=SPRING_CONFIG_LOCATION="optional:classpath:/;optional:file:/.halo/" Environment=TZ=Asia/Shanghai Volume=/opt/podman-data/halo:/.halo PublishPort=127.0.0.1:8090:8090 -Image=ghcr.io/halo-dev/halo:2.18 +Image=ghcr.io/halo-dev/halo:2.19 Exec=--halo.external-url=https://localhost:8090 --spring.sql.init.platform=postgresql --spring.r2dbc.url=r2dbc:pool:postgresql://127.0.0.1:5432/my-db --spring.r2dbc.username=my-user --spring.r2dbc.password=my-password [Service] @@ -166,7 +166,7 @@ Podman Quadlet 解析: `[Container]` 部分: -- `AutoUpdate=registry`指定了自动拉取容器。假设后续Halo镜像支持了`latest`标签,你需要`systemctl enable --now podman-auto-update.timer`以启用容器自动更新。本文示例`ghcr.io/halo-dev/halo:2.18`,将会自动更新适用与`2.18`版本的patch,例如您创建容器时是`2.18.1`,在官方发布`2.18.2`版本时,容器会自动更新到`2.18.2`。 +- `AutoUpdate=registry`指定了自动拉取容器。假设后续Halo镜像支持了`latest`标签,你需要`systemctl enable --now podman-auto-update.timer`以启用容器自动更新。本文示例`ghcr.io/halo-dev/halo:2.19`,将会自动更新适用与`2.19`版本的patch,例如您创建容器时是`2.19.1`,在官方发布`2.19.2`版本时,容器会自动更新到`2.19.2`。 - `ContainerName=`指定了 systemd 将生成的服务名称。 - `User=60000 Group=60000 UserNS=keep-id:uid=60000,gid=60000` 限制容器以 id 60000 的用户运行,提高安全性。注意这个id 60000请根据你实际想要运行的用户名来修改,可通过`id user`获得你的用户的id. - `Environment=`字段指定了容器的环境变量,其中你需要注意的是`Environment=HALO_WORK_DIR="/.halo"` `Environment=SPRING_CONFIG_LOCATION="optional:classpath:/;optional:file:/.halo/"`这两个变量中的`/.halo`路径。 @@ -209,7 +209,7 @@ AutoUpdate=registry ContainerName=halo Volume=/opt/podman-data/halo:/root/.halo PublishPort=127.0.0.1:8090:8090 -Image=ghcr.io/halo-dev/halo:2.18 +Image=ghcr.io/halo-dev/halo:2.19 Exec=--halo.external-url=https://localhost:8090 --spring.sql.init.platform=postgresql --spring.r2dbc.url=r2dbc:pool:postgresql://127.0.0.1:5432/my-db --spring.r2dbc.username=my-user --spring.r2dbc.password=my-password [Service] diff --git a/docs/getting-started/install/slots/_docker-registry-list.md b/docs/getting-started/install/slots/_docker-registry-list.md index db8d88e0..a163d46e 100644 --- a/docs/getting-started/install/slots/_docker-registry-list.md +++ b/docs/getting-started/install/slots/_docker-registry-list.md @@ -5,11 +5,11 @@ - [ghcr.io/halo-dev/halo](https://github.com/halo-dev/halo/pkgs/container/halo) :::info 注意 -目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `2.18` 或者 `2.18.0`。 +目前 Halo 2 并未更新 Docker 的 latest 标签镜像,主要因为 Halo 2 不兼容 1.x 版本,防止使用者误操作。我们推荐使用固定版本的标签,比如 `2.19` 或者 `2.19.0`。 - `registry.fit2cloud.com/halo/halo:2`:表示最新的 2.x 版本,即每次发布新版本都会更新此镜像。 -- `registry.fit2cloud.com/halo/halo:2.18`:表示最新的 2.18.x 版本,即每次发布 patch 版本都会同时更新此镜像。 -- `registry.fit2cloud.com/halo/halo:2.18.0`:表示一个具体的版本。 +- `registry.fit2cloud.com/halo/halo:2.19`:表示最新的 2.19.x 版本,即每次发布 patch 版本都会同时更新此镜像。 +- `registry.fit2cloud.com/halo/halo:2.19.0`:表示一个具体的版本。 -后续文档以 `registry.fit2cloud.com/halo/halo:2.18` 为例。 +后续文档以 `registry.fit2cloud.com/halo/halo:2.19` 为例。 ::: diff --git a/docs/intro.md b/docs/intro.md index 8c9c7dcc..0f9a1e78 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -34,7 +34,7 @@ slug: / ## 快速开始 ```bash -docker run -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.18 +docker run -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.19 ``` 以上仅作为体验使用,详细部署文档请查阅:[https://docs.halo.run/getting-started/install/docker-compose](https://docs.halo.run/getting-started/install/docker-compose) diff --git a/docs/user-guide/faq.md b/docs/user-guide/faq.md index f67e8a90..766aa373 100644 --- a/docs/user-guide/faq.md +++ b/docs/user-guide/faq.md @@ -93,7 +93,7 @@ server { --name halo-1 \ -p 8090:8090 \ -v ~/.halo2:/root/.halo2 \ - registry.fit2cloud.com/halo/halo:2.18 \ + registry.fit2cloud.com/halo/halo:2.19 \ # 第二个 Halo 容器 docker run \ @@ -101,7 +101,7 @@ server { --name halo-2 \ -p 8091:8090 \ -v ~/.halo2_2:/root/.halo2 \ - registry.fit2cloud.com/halo/halo:2.18 \ + registry.fit2cloud.com/halo/halo:2.19 \ ``` 更多 Docker 相关的教程请参考:[使用 Docker 部署 Halo](../getting-started/install/docker.md) diff --git a/docusaurus.config.js b/docusaurus.config.js index b06a86fd..f5bdad7d 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -28,11 +28,11 @@ const config = { routeBasePath: "/", showLastUpdateTime: true, showLastUpdateAuthor: true, - lastVersion: "2.18", + lastVersion: "2.19", versions: { current: { - label: "2.19.0-SNAPSHOT", - path: "2.19.0-SNAPSHOT", + label: "2.20.0-SNAPSHOT", + path: "2.20.0-SNAPSHOT", }, }, }, @@ -44,13 +44,15 @@ const config = { changefreq: "weekly", priority: 0.5, ignorePatterns: [ - "/2.0/**", - "/2.1/**", - "/2.2/**", - "/2.3/**", - "/2.4/**", - "/2.5/**", - "/2.6/**", + "/2.10/**", + "/2.11/**", + "/2.12/**", + "/2.13/**", + "/2.14/**", + "/2.15/**", + "/2.16/**", + "/2.17/**", + "/2.18/**", ], }, googleAnalytics: { @@ -147,11 +149,11 @@ const config = { items: [ { label: "官网", - href: "https://halo.run", + href: "https://www.halo.run", }, { label: "应用市场", - href: "https://halo.run/store/apps", + href: "https://www.halo.run/store/apps", }, { label: "GitHub 组织", @@ -176,7 +178,7 @@ const config = { }, { label: "微信公众号", - href: "https://halo.run/upload/2021/03/B3C27F16-4890-4633-81CC-20BA4B28F94F-2415126255c749b290312ca22d9bdeb0.jpeg", + href: "https://www.halo.run/upload/2021/03/B3C27F16-4890-4633-81CC-20BA4B28F94F-2415126255c749b290312ca22d9bdeb0.jpeg", }, { label: "GitHub Issues", @@ -235,17 +237,18 @@ const config = { }, ], createRedirects(existingPath) { - if (existingPath.startsWith("/2.19.0-SNAPSHOT/")) { + if (existingPath.startsWith("/2.20.0-SNAPSHOT/")) { return [ - existingPath.replace("/2.19.0-SNAPSHOT/", "/2.10.0-SNAPSHOT/"), - existingPath.replace("/2.19.0-SNAPSHOT/", "/2.11.0-SNAPSHOT/"), - existingPath.replace("/2.19.0-SNAPSHOT/", "/2.12.0-SNAPSHOT/"), - existingPath.replace("/2.19.0-SNAPSHOT/", "/2.13.0-SNAPSHOT/"), - existingPath.replace("/2.19.0-SNAPSHOT/", "/2.14.0-SNAPSHOT/"), - existingPath.replace("/2.19.0-SNAPSHOT/", "/2.15.0-SNAPSHOT/"), - existingPath.replace("/2.19.0-SNAPSHOT/", "/2.16.0-SNAPSHOT/"), - existingPath.replace("/2.19.0-SNAPSHOT/", "/2.17.0-SNAPSHOT/"), - existingPath.replace("/2.19.0-SNAPSHOT/", "/2.18.0-SNAPSHOT/"), + existingPath.replace("/2.20.0-SNAPSHOT/", "/2.10.0-SNAPSHOT/"), + existingPath.replace("/2.20.0-SNAPSHOT/", "/2.11.0-SNAPSHOT/"), + existingPath.replace("/2.20.0-SNAPSHOT/", "/2.12.0-SNAPSHOT/"), + existingPath.replace("/2.20.0-SNAPSHOT/", "/2.13.0-SNAPSHOT/"), + existingPath.replace("/2.20.0-SNAPSHOT/", "/2.14.0-SNAPSHOT/"), + existingPath.replace("/2.20.0-SNAPSHOT/", "/2.15.0-SNAPSHOT/"), + existingPath.replace("/2.20.0-SNAPSHOT/", "/2.16.0-SNAPSHOT/"), + existingPath.replace("/2.20.0-SNAPSHOT/", "/2.17.0-SNAPSHOT/"), + existingPath.replace("/2.20.0-SNAPSHOT/", "/2.18.0-SNAPSHOT/"), + existingPath.replace("/2.20.0-SNAPSHOT/", "/2.19.0-SNAPSHOT/"), ]; } return undefined; diff --git a/i18n/zh-Hans/code.json b/i18n/zh-Hans/code.json index d8cfbaa6..8a6169d0 100644 --- a/i18n/zh-Hans/code.json +++ b/i18n/zh-Hans/code.json @@ -69,10 +69,6 @@ "message": "浅色模式", "description": "The name for the light color mode" }, - "theme.docs.breadcrumbs.navAriaLabel": { - "message": "页面路径", - "description": "The ARIA label for the breadcrumbs" - }, "theme.docs.DocCard.categoryDescription.plurals": { "message": "{count} 个项目", "description": "The default description for a category card in the generated index about how many items this category includes" @@ -89,6 +85,10 @@ "message": "下一页", "description": "The label used to navigate to the next doc" }, + "theme.docs.breadcrumbs.navAriaLabel": { + "message": "页面路径", + "description": "The ARIA label for the breadcrumbs" + }, "theme.docs.tagDocListPageTitle.nDocsTagged": { "message": "{count} 篇文档带有标签", "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" @@ -116,14 +116,14 @@ "message": "最新版本", "description": "The label used for the latest version suggestion link label" }, - "theme.common.editThisPage": { - "message": "编辑此页", - "description": "The link label to edit the current page" - }, "theme.common.headingLinkTitle": { "message": "{heading}的直接链接", "description": "Title for link to heading" }, + "theme.common.editThisPage": { + "message": "编辑此页", + "description": "The link label to edit the current page" + }, "theme.lastUpdated.atDate": { "message": "于 {date} ", "description": "The words used to describe on which date a page has been last updated" @@ -136,22 +136,18 @@ "message": "最后{byUser}{atDate}更新", "description": "The sentence used to display when a page has been last updated, and by who" }, - "theme.NotFound.title": { - "message": "找不到页面", - "description": "The title of the 404 page" - }, "theme.navbar.mobileVersionsDropdown.label": { "message": "选择版本", "description": "The label for the navbar versions dropdown on mobile view" }, + "theme.NotFound.title": { + "message": "找不到页面", + "description": "The title of the 404 page" + }, "theme.tags.tagsListLabel": { "message": "标签:", "description": "The label alongside a tag list" }, - "theme.AnnouncementBar.closeButtonAriaLabel": { - "message": "关闭", - "description": "The ARIA label for close button of announcement bar" - }, "theme.admonition.caution": { "message": "警告", "description": "The default label used for the Caution admonition (:::caution)" @@ -176,6 +172,10 @@ "message": "注意", "description": "The default label used for the Warning admonition (:::warning)" }, + "theme.AnnouncementBar.closeButtonAriaLabel": { + "message": "关闭", + "description": "The ARIA label for close button of announcement bar" + }, "theme.blog.sidebar.navAriaLabel": { "message": "最近博文导航", "description": "The ARIA label for recent posts in the blog sidebar" @@ -216,10 +216,6 @@ "message": "请联系原始链接来源网站的所有者,并告知他们链接已损坏。", "description": "The 2nd paragraph of the 404 page" }, - "theme.navbar.mobileLanguageDropdown.label": { - "message": "选择语言", - "description": "The label for the mobile language switcher dropdown" - }, "theme.TOCCollapsible.toggleButtonLabel": { "message": "本页总览", "description": "The label used by the button on the collapsible TOC component" @@ -228,17 +224,9 @@ "message": "阅读需 {readingTime} 分钟", "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" }, - "theme.blog.post.readMore": { - "message": "阅读更多", - "description": "The label used in blog post item excerpts to link to full blog posts" - }, - "theme.blog.post.readMoreLabel": { - "message": "阅读 {title} 的全文", - "description": "The ARIA label for the link to full blog posts from excerpts" - }, - "theme.docs.breadcrumbs.home": { - "message": "主页面", - "description": "The ARIA label for the home page in the breadcrumbs" + "theme.navbar.mobileLanguageDropdown.label": { + "message": "选择语言", + "description": "The label for the mobile language switcher dropdown" }, "theme.docs.sidebar.collapseButtonTitle": { "message": "收起侧边栏", @@ -248,10 +236,22 @@ "message": "收起侧边栏", "description": "The title attribute for collapse button of doc sidebar" }, + "theme.blog.post.readMore": { + "message": "阅读更多", + "description": "The label used in blog post item excerpts to link to full blog posts" + }, + "theme.blog.post.readMoreLabel": { + "message": "阅读 {title} 的全文", + "description": "The ARIA label for the link to full blog posts from excerpts" + }, "theme.docs.sidebar.navAriaLabel": { "message": "文档侧边栏", "description": "The ARIA label for the sidebar navigation" }, + "theme.docs.breadcrumbs.home": { + "message": "主页面", + "description": "The ARIA label for the home page in the breadcrumbs" + }, "theme.docs.sidebar.closeSidebarButtonAriaLabel": { "message": "关闭导航栏", "description": "The ARIA label for close button of mobile sidebar" diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current.json b/i18n/zh-Hans/docusaurus-plugin-content-docs/current.json index f6c1821d..37127b30 100644 --- a/i18n/zh-Hans/docusaurus-plugin-content-docs/current.json +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current.json @@ -1,6 +1,6 @@ { "version.label": { - "message": "2.19.0-SNAPSHOT", + "message": "2.20.0-SNAPSHOT", "description": "The label for version current" }, "sidebar.tutorial.category.入门": { diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/version-2.19.json b/i18n/zh-Hans/docusaurus-plugin-content-docs/version-2.19.json new file mode 100644 index 00000000..2899a4a2 --- /dev/null +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/version-2.19.json @@ -0,0 +1,78 @@ +{ + "version.label": { + "message": "2.19", + "description": "The label for version 2.19" + }, + "sidebar.tutorial.category.入门": { + "message": "入门", + "description": "The label for category 入门 in sidebar tutorial" + }, + "sidebar.tutorial.category.安装指南": { + "message": "安装指南", + "description": "The label for category 安装指南 in sidebar tutorial" + }, + "sidebar.tutorial.category.云平台": { + "message": "云平台", + "description": "The label for category 云平台 in sidebar tutorial" + }, + "sidebar.tutorial.category.其他指南": { + "message": "其他指南", + "description": "The label for category 其他指南 in sidebar tutorial" + }, + "sidebar.tutorial.category.用户指南": { + "message": "用户指南", + "description": "The label for category 用户指南 in sidebar tutorial" + }, + "sidebar.tutorial.category.参与贡献": { + "message": "参与贡献", + "description": "The label for category 参与贡献 in sidebar tutorial" + }, + "sidebar.developer.category.系统开发": { + "message": "系统开发", + "description": "The label for category 系统开发 in sidebar developer" + }, + "sidebar.developer.category.插件开发": { + "message": "插件开发", + "description": "The label for category 插件开发 in sidebar developer" + }, + "sidebar.developer.category.基础": { + "message": "基础", + "description": "The label for category 基础 in sidebar developer" + }, + "sidebar.developer.category.服务端": { + "message": "服务端", + "description": "The label for category 服务端 in sidebar developer" + }, + "sidebar.developer.category.UI": { + "message": "UI", + "description": "The label for category UI in sidebar developer" + }, + "sidebar.developer.category.API 参考": { + "message": "API 参考", + "description": "The label for category API 参考 in sidebar developer" + }, + "sidebar.developer.category.扩展点": { + "message": "扩展点", + "description": "The label for category 扩展点 in sidebar developer" + }, + "sidebar.developer.category.组件": { + "message": "组件", + "description": "The label for category 组件 in sidebar developer" + }, + "sidebar.developer.category.案例和最佳实践": { + "message": "案例和最佳实践", + "description": "The label for category 案例和最佳实践 in sidebar developer" + }, + "sidebar.developer.category.主题开发": { + "message": "主题开发", + "description": "The label for category 主题开发 in sidebar developer" + }, + "sidebar.developer.category.模板变量": { + "message": "模板变量", + "description": "The label for category 模板变量 in sidebar developer" + }, + "sidebar.developer.category.Finder API": { + "message": "Finder API", + "description": "The label for category Finder API in sidebar developer" + } +} diff --git a/i18n/zh-Hans/docusaurus-theme-classic/footer.json b/i18n/zh-Hans/docusaurus-theme-classic/footer.json index c1251bce..40771fc3 100644 --- a/i18n/zh-Hans/docusaurus-theme-classic/footer.json +++ b/i18n/zh-Hans/docusaurus-theme-classic/footer.json @@ -9,11 +9,11 @@ }, "link.item.label.官网": { "message": "官网", - "description": "The label of footer link with label=官网 linking to https://halo.run" + "description": "The label of footer link with label=官网 linking to https://www.halo.run" }, "link.item.label.应用市场": { "message": "应用市场", - "description": "The label of footer link with label=应用市场 linking to https://halo.run/store/apps" + "description": "The label of footer link with label=应用市场 linking to https://www.halo.run/store/apps" }, "link.item.label.GitHub 组织": { "message": "GitHub 组织", @@ -33,7 +33,7 @@ }, "link.item.label.微信公众号": { "message": "微信公众号", - "description": "The label of footer link with label=微信公众号 linking to https://halo.run/upload/2021/03/B3C27F16-4890-4633-81CC-20BA4B28F94F-2415126255c749b290312ca22d9bdeb0.jpeg" + "description": "The label of footer link with label=微信公众号 linking to https://www.halo.run/upload/2021/03/B3C27F16-4890-4633-81CC-20BA4B28F94F-2415126255c749b290312ca22d9bdeb0.jpeg" }, "link.item.label.GitHub Issues": { "message": "GitHub Issues", diff --git a/versioned_docs/version-2.19/about.md b/versioned_docs/version-2.19/about.md new file mode 100644 index 00000000..d0eb7313 --- /dev/null +++ b/versioned_docs/version-2.19/about.md @@ -0,0 +1,16 @@ +--- +title: 关于文档 +description: 关于本文档站点的一些说明 +--- + +:::note +此文档使用 [Docusaurus](https://docusaurus.io/) 搭建,感谢 [Docusaurus](https://github.com/facebook/docusaurus) 社区所做的贡献。 +::: + +## 参与贡献 + +:::tip +如果你发现文档中有不正确或者需要添加的内容,非常欢迎参与到文档编辑当中。 +::: + +当前文档的仓库地址为 [halo-dev/docs](https://github.com/halo-dev/docs) ,所以你可以 fork 此仓库,修改之后提交 `Pull request` 等待我们合并即可。 diff --git a/versioned_docs/version-2.19/contribution/issue.md b/versioned_docs/version-2.19/contribution/issue.md new file mode 100644 index 00000000..f1184855 --- /dev/null +++ b/versioned_docs/version-2.19/contribution/issue.md @@ -0,0 +1,28 @@ +--- +title: 问题反馈 +description: 问题反馈渠道及指南 +--- + +:::info +如果您在使用过程中遇到了什么问题,您可以通过下面的方式反馈,但请尽量按照要求提出反馈。 +::: + +## GitHub Issues + +链接:[https://github.com/halo-dev/halo/issues](https://github.com/halo-dev/halo/issues) + +如果你在使用过程中,遇到了一些 bug 或者需要添加某些新特性,请尽量在 GitHub Issues 进行反馈,这非常有助于我们跟踪解决此问题,您也可以很方便的接收到处理状态。 + +建议步骤: + +1. 在 [Issues 列表](https://github.com/halo-dev/halo/issues) 搜索相关问题,看看是否有其他人已经提到了此问题。 +2. 如果当前还没有人遇到您类似的问题,那么请点击右上角的 `New issue` 按钮创建新的 issue。 +3. 选择正确的反馈类型。 +4. 请尽可能详细的按照模板填写内容。 +5. 点击 `Submit new issue` 提交 issue。 + +## Halo 官方社区 + +链接:[https://bbs.halo.run](https://bbs.halo.run) + +此平台主要目的用于与其他 Halo 用户进行交流。但如果您对 GitHub 不是很熟悉或者没有账号,您也可以在此平台进行反馈。 diff --git a/versioned_docs/version-2.19/contribution/pr.md b/versioned_docs/version-2.19/contribution/pr.md new file mode 100644 index 00000000..10d26ffb --- /dev/null +++ b/versioned_docs/version-2.19/contribution/pr.md @@ -0,0 +1,110 @@ +--- +title: 代码贡献 +description: 代码贡献指南 +--- + +欢迎关注并有想法参与 Halo 的开发,以下是关于如何参与到 Halo 项目的指南,仅供参考。 + +## 发现 Issue + +所有的代码尽可能都有依据(Issue),不是凭空产生。 + +### 寻找一个 Good First Issue + +> 这个步骤非常适合首次贡献者。 + +在 [halo-dev](https://github.com/halo-dev) 和 [halo-sigs](https://github.com/halo-sigs) 组织下,有非常多的仓库。每个仓库下都有可能包含一些“首次贡献者”友好的 Issue,主要是为了给贡献者提供一个友好的体验。 该类 Issue +一般会用 `good-first-issue` 标签标记。标签 `good-first-issue` 表示该 Issue 不需要对 Halo 有深入的理解也能够参与。 + +请点击:[good-first-issue](https://github.com/issues?q=org%3Ahalo-dev+is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+no%3Aassignee+) +查看关于 Halo 的 Good First Issue。 + +### 认领 Issue + +若对任何一个 Issue 感兴趣,请尝试在 Issue 进行回复,讨论解决 Issue 的思路。确定后可直接通过 `/assign` 或者 `/assign @GitHub 用户名` 认领这个 +Issue。这样可避免两位贡献者在同一个问题上花时间。 + +## 代码贡献步骤 + +1. Fork 此仓库 + + 点击 Halo 仓库主页右上角的 `Fork` 按钮即可。 + +2. Clone 仓库到本地 + + ```bash + git clone https://github.com/{YOUR_USERNAME}/halo --recursive + # 或者 git clone git@github.com:{YOUR_USERNAME}/halo.git --recursive + ``` + +3. 添加主仓库 + + 添加主仓库方便未来同步主仓库最新的 commits 以及创建新的分支。 + + ```bash + git remote add upstream https://github.com/halo-dev/halo.git + # 或者 git remote add upstream git@github.com:halo-dev/halo.git + git fetch upstream main + ``` + +4. 创建新的开发分支 + + 我们需要从主仓库的主分支创建一个新的开发分支。 + + ```bash + git checkout upstream/main + git checkout -b {BRANCH_NAME} + ``` + +5. 提交代码 + + ```bash + git add . + git commit -s -m "Fix a bug king" + git push origin {BRANCH_NAME} + ``` + +6. 合并主分支 + + 在提交 Pull Request 之前,尽量保证当前分支和主分支的代码尽可能同步,这时需要我们手动操作。示例: + + ```bash + git fetch upstream/main + git merge upstream/main + git push origin {BRANCH_NAME} + ``` + +## Pull Request + +进入此阶段说明已经完成了代码的编写,测试和自测,并且准备好接受 Code Review。 + +### 创建 Pull Request + +回到自己的仓库页面,选择 `New pull request` 按钮,创建 `Pull request` 到原仓库的 `main` 分支。 +然后等待我们 Review 即可,如有 `Change Request`,再本地修改之后再次提交即可。 + +提交 Pull Request 的注意事项: + +- 提交 Pull Request 请充分自测。 +- 每个 Pull Request 尽量只解决一个 Issue,特殊情况除外。 +- 应尽可能多的添加单元测试,其他测试(集成测试和 E2E 测试)可看情况添加。 +- 不论需要解决的 Issue 发生在哪个版本,提交 Pull Request 的时候,请将主仓库的主分支设置为 `main`。例如:即使某个 Bug 于 Halo 2.0.x 被发现,但是提交 Pull Request 仍只针对 + `main` 分支,等待 Pull Request 合并之后,我们会通过 `/cherrypick release-2.0` 或者 `/cherry-pick release-2.1` 指令将此 Pull Request + 的修改应用到 `release-2.0` 和 `release-2.1` 分支上。 + +### 更新 commits + +Code Review 阶段可能需要 Pull Request 作者重新修改代码,请直接在当前分支 commit 并 push 即可,无需关闭并重新提交 Pull Request。示例: + +```bash +git add . +git commit -s -m "Refactor some code according code review" +git push origin bug/king +``` + +同时,若已经进入 Code Review 阶段,请不要强制推送 commits 到当前分支。否则 Reviewers 需要从头开始 Code Review。 + +### 开发规范 + +请参考 [https://docs.halo.run/developer-guide/core/code-style](https://docs.halo.run/developer-guide/core/code-style) +,请确保所有代码格式化之后再提交。 diff --git a/versioned_docs/version-2.19/contribution/sponsor.md b/versioned_docs/version-2.19/contribution/sponsor.md new file mode 100644 index 00000000..eede34e3 --- /dev/null +++ b/versioned_docs/version-2.19/contribution/sponsor.md @@ -0,0 +1,26 @@ +--- +title: 赞助我们 +description: 如果 Halo 对你有帮助,不妨赞助我们 +--- + +## 为什么需要赞助 + +我们花费了大量的精力来维护这个软件,并且也提供了不少资金来维护服务器,域名等。因此我们需要赞助来节省部分开发成本。你的赞助不仅仅会被我们用来支付一些开发成本(比如服务器,OSS,域名等),还会让我们有更多的信心和精力投入到这个开源项目的开发中。从而让项目保持更加健康的成长以及迭代。 + +## 赞助形式 + +:::info +你可以通过任何形式对我们赞助,不限于资金。 +::: + +### 资金赞助 + +- 爱发电:[https://afdian.com/a/halo-dev](https://afdian.com/a/halo-dev) + +### 通过我们的推广链接购买服务器 + +如果你当前还没有购买服务器,可以考虑通过以下链接购买,这会为我们带来一部分收益。 + +- 阿里云:[https://www.aliyun.com/daily-act/ecs/activity_selection?userCode=j57gyupo](https://www.aliyun.com/daily-act/ecs/activity_selection?userCode=j57gyupo) +- 阿里云新人专享:[https://www.aliyun.com/minisite/goods?userCode=j57gyupo](https://www.aliyun.com/minisite/goods?userCode=j57gyupo) +- 腾讯云:[https://curl.qcloud.com/9Ogon25Y](https://curl.qcloud.com/9Ogon25Y) diff --git a/versioned_docs/version-2.19/developer-guide/annotations-form.md b/versioned_docs/version-2.19/developer-guide/annotations-form.md new file mode 100644 index 00000000..0fa3d8d3 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/annotations-form.md @@ -0,0 +1,89 @@ +--- +title: 元数据表单定义 +--- + +在 Halo 2.0,所有的模型都包含了 `metadata.annotations` 字段,用于存储元数据信息。元数据信息可以用于存储一些自定义的信息,可以等同于扩展字段。此文档主要介绍如何在 Halo 中为具体的模型定义元数据编辑表单,至于如何在插件或者主题模板中使用,请看插件或者主题的文档。 + +定义元数据编辑表单同样使用 `FormKit Schema`,但和主题或插件的定义方式稍有不同,其中输入组件类型可参考 [表单定义](./form-schema.md)。 + +:::info 提示 +因为 `metadata.annotations` 是一个键值都为字符串类型的对象,所以表单项的值必须为字符串类型。这就意味着,FormKit 的 `number`、`group`、`repeater` 等类型的输入组件都不能使用。 +::: + +## AnnotationSetting 资源定义方式 + +```yaml title="annotation-setting.yaml" +apiVersion: v1alpha1 +kind: AnnotationSetting +metadata: + name: my-annotation-setting +spec: + targetRef: + group: content.halo.run + kind: Post + formSchema: + - $formkit: "text" + name: "download" + label: "下载地址" + - $formkit: "text" + name: "version" + label: "版本" +``` + +以上定义为文章模型添加了两个元数据字段,分别为 `download` 和 `version`,分别对应了下载地址和版本号,最终效果: + +![Annotation Setting Preview](/img/annotation-setting/annotation-setting-preview.png) + +字段说明: + +1. `metadata.name`:唯一标识,命名规范可参考 [metadata name](./plugin/api-reference/server/extension.md#metadata-name),为了尽可能避免冲突,建议自定义前缀以及追加随机字符串,如:`theme-earth-post-wanfs5`。 +2. `spec.targetRef`:模型的关联,即为哪个模型添加元数据表单,目前支持的模型可查看下方的列表。 +3. `spec.formSchema`:表单的定义,使用 FormKit Schema 来定义。虽然我们使用的 YAML,但与 FormKit Schema 完全一致。 + +targetRef 支持列表: + +| 对应模型 | group | kind | +| ---------- | ---------------- | ---------- | +| 文章 | content.halo.run | Post | +| 自定义页面 | content.halo.run | SinglePage | +| 文章分类 | content.halo.run | Category | +| 文章标签 | content.halo.run | Tag | +| 菜单项 | `""` | MenuItem | +| 用户 | `""` | User | + +## 为多个模型定义表单 + +考虑到某些情况可能会同时为多个模型添加元数据表单,推荐在一个 `yaml` 文件中使用 `---` 来分割多个资源定义,如下: + +```yaml title="annotation-setting.yaml" +apiVersion: v1alpha1 +kind: AnnotationSetting +metadata: + name: my-annotation-setting +spec: + targetRef: + group: content.halo.run + kind: Post + formSchema: + - $formkit: "text" + name: "download" + label: "下载地址" + - $formkit: "text" + name: "version" + label: "版本" + +--- + +apiVersion: v1alpha1 +kind: AnnotationSetting +metadata: + name: my-annotation-setting +spec: + targetRef: + group: "" + kind: MenuItem + formSchema: + - $formkit: "text" + name: "icon" + label: "图标" +``` diff --git a/versioned_docs/version-2.19/developer-guide/core/build.md b/versioned_docs/version-2.19/developer-guide/core/build.md new file mode 100644 index 00000000..ce5578e8 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/core/build.md @@ -0,0 +1,84 @@ +--- +title: 构建 +description: 构建为可执行 JAR 和 Docker 镜像的文档 +--- + +:::info +在此之前,我们推荐你先阅读[《准备工作》](./prepare),检查本地环境是否满足要求。 +::: + +一般情况下,为了保证版本一致性和可维护性,我们并不推荐自行构建和二次开发。 + +## 克隆项目 + +如果你已经 Fork 了相关仓库,请将以下命令中的 `halo-dev` 替换为你的 GitHub 用户名。 + +```bash +git clone https://github.com/halo-dev/halo + +# 或者使用 ssh 的方式 clone(推荐) +# git clone git@github.com:halo-dev/halo.git + +# 或者使用 GitHub CLI 克隆(推荐) +# gh repo clone halo-dev/halo + +# 或者使用 GitHub CLI Fork(推荐) +# gh repo fork halo-dev/halo + +cd halo + +# 切换到特定的分支或标签,请替换 ${branch_name} +git checkout ${branch_name} +``` + +## 构建 Fat Jar + +构建之前需要修改 `gradle.properties` 中的 `version` 属性(推荐遵循 [SemVer 规范](https://semver.org/)),例如:`version=2.19.0` + +```bash +cd path/to/halo +``` + +下载预设插件(可选): + +```bash +# Windows +./gradlew.bat downloadPluginPresets + +# macOS / Linux +./gradlew downloadPluginPresets +``` + +构建: + +```bash +# Windows +./gradlew.bat clean build -x check + +# macOS / Linux +./gradlew clean build -x check +``` + +构建完成之后,在 Halo 项目下产生的 `application/build/libs/halo-${version}.jar` 即为构建完成的文件。 + +最终部署文档可参考:[使用 JAR 文件部署](../../getting-started/install/jar-file.md)。 + +## 构建 Docker 镜像 + +在此之前,请确认已经构建好了 Fat Jar。 + +```bash +cd path/to/halo +``` + +```bash +# 请替换 ${tag_name} +docker build -t halo-dev/halo:${tag_name} . +``` + +```bash +# 插件构建完成的版本 +docker images | grep halo +``` + +最终部署文档可参考:[使用 Docker Compose 部署](../../getting-started/install/docker-compose.md)。 diff --git a/versioned_docs/version-2.19/developer-guide/core/code-style.md b/versioned_docs/version-2.19/developer-guide/core/code-style.md new file mode 100644 index 00000000..d422826a --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/core/code-style.md @@ -0,0 +1,30 @@ +--- +title: 代码风格 +description: 代码风格的相关配置说明 +--- + +Halo 添加了 checkstyle 插件,来保证每位提交者代码的风格保持一致,减少无效代码的修改。本篇文章主要讲解如何在 IDEA 中添加 CheckStyle 插件,并引入项目所提供的 checkstyle.xml 配置。 + +## 安装 CheckStyle-IDEA + +- 进入 IDEA 插件市场。 +- 搜索 CheckStyle-IDEA,点击安装即可。 + +## 配置 CheckStyle + +- 进入 CheckStyle 配置(File | Settings | Tools | Checkstyle)。 +- 选择 Checkstyle 版本:9.3(以文件 `application/build.gradle` 中指定的版本为准)。 +- 在配置文件中点击添加按钮,配置描述可随便填写(推荐 Halo Checks),选择 ./config/checkstyle/checkstyle.xml,点击下一步和完成。 +- 勾选刚刚创建的配置文件。 + +## 配置 Editor + +- 进入编辑器配置(File | Settings | Editor | Code Style) + +- 导入 checkstyle.xm 配置: + +![image.png](https://halo.run/upload/2020/2/image-0c7a018e73f74634a534fa3ba8806628.png) + +- 选择 `./config/checkstyle/checkstyle.xml` 配置文件,点击确定即可。 + +至此,有关代码风格检查工具和格式化配置已经完成。 diff --git a/versioned_docs/version-2.19/developer-guide/core/prepare.md b/versioned_docs/version-2.19/developer-guide/core/prepare.md new file mode 100644 index 00000000..ca917bc2 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/core/prepare.md @@ -0,0 +1,25 @@ +--- +title: 准备工作 +description: 开发环境的准备工作 +--- + +## 环境要求 + +- [OpenJDK 17 LTS](https://github.com/openjdk/jdk) +- [Node.js 20 LTS](https://nodejs.org) +- [pnpm 9](https://pnpm.io/) +- [IntelliJ IDEA](https://www.jetbrains.com/idea/) +- [Git](https://git-scm.com/) +- [Docker](https://www.docker.com/)(可选) + +## 名词解释 + +### 工作目录 + +指 Halo 所依赖的工作目录,在 Halo 运行的时候会在系统当前用户目录下产生一个 halo-next 的文件夹,绝对路径为 ~/halo-next。里面通常包含下列目录或文件: + +1. `db`:存放 H2 Database 的物理文件,如果你使用其他数据库,那么不会存在这个目录。 +2. `themes`:里面包含用户所安装的主题。 +2. `plugins`:里面包含用户所安装的插件。 +5. `attachments`:附件目录。 +4. `logs`:运行日志目录。 diff --git a/versioned_docs/version-2.19/developer-guide/core/run.md b/versioned_docs/version-2.19/developer-guide/core/run.md new file mode 100644 index 00000000..21b000e2 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/core/run.md @@ -0,0 +1,122 @@ +--- +title: 开发环境运行 +description: 开发环境运行的指南 +--- + +:::info +在此之前,我们推荐你先阅读[《准备工作》](./prepare),检查本地环境是否满足要求。 +::: + +## 项目结构说明 + +目前如果需要完整的运行 Halo,总共需要三个部分: + +1. Halo 主项目([halo-dev/halo](https://github.com/halo-dev/halo)) +2. UI,包括 Console 控制台和 UC 个人中心(托管在 Halo 主项目) +3. 主题(Halo 主项目内已包含默认主题) + +:::info 说明 +从 Halo 2.11 开始,Halo 项目的 `ui` 目录同时包含了 Console(管理控制台)和 UC(个人中心),以下统称为 UI。 + +当前 Halo 主项目并不会将 UI 的构建资源托管到 Git 版本控制,所以在开发环境是需要同时运行 UI 项目的。当然,在我们的最终发布版本的时候会在 CI 中自动构建 UI 到 Halo 主项目。 +::: + +## 克隆项目 + +如果你已经 Fork 了相关仓库,请将以下命令中的 `halo-dev` 替换为你的 GitHub 用户名。 + +```bash +git clone https://github.com/halo-dev/halo + +# 或者使用 ssh 的方式 clone(推荐) +# git clone git@github.com:halo-dev/halo.git + +# 或者使用 GitHub CLI 克隆(推荐) +# gh repo clone halo-dev/halo + +# 或者使用 GitHub CLI Fork(推荐) +# gh repo fork halo-dev/halo +``` + +## 运行 UI 服务 + +```bash +cd path/to/halo/ui +pnpm install +pnpm build:packages +pnpm dev +``` + +最终控制台打印了如下信息即代表运行正常: + +```bash +VITE v4.2.3 ready in 638 ms + +# Console 控制台服务 +➜ Local: http://localhost:3000/console/ + +# UC 个人中心服务 +➜ Local: http://localhost:4000/uc/ +``` + +:::info 提示 +请不要直接使用 UI 的运行端口(3000 / 4000)访问,会因为跨域问题导致无法正常登录,建议按照后续的步骤以 dev 的配置文件运行 Halo,在 dev 的配置文件中,我们默认代理了 UI 页面的访问地址,所以后续访问 UI 页面使用 `http://localhost:8090/console` 和 `http://localhost:8090/uc` 访问即可,代理的相关配置: + +```yaml +halo: + console: + proxy: + endpoint: http://localhost:3000/ + enabled: true + uc: + proxy: + endpoint: http://localhost:4000/ + enabled: true +``` + +::: + +## 运行 Halo + +1. 在 IntelliJ IDEA 中打开 Halo 项目,等待 Gradle 初始化和依赖下载完成。 + +2. 下载预设插件(可选) + + ```bash + # Windows + ./gradlew.bat downloadPluginPresets + + # macOS / Linux + ./gradlew downloadPluginPresets + ``` + +3. 修改 IntelliJ IDEA 的运行配置 + + - Windows + + 将 Active Profiles 改为 `dev,win`,如图所示: + + ![IntelliJ IDEA Profiles](/img/developer-run/IntelliJ-IDEA-Profiles-Win.png) + + - macOS / Linux + + 将 Active Profiles 改为 `dev`,如图所示: + + ![IntelliJ IDEA Profiles](/img/developer-run/IntelliJ-IDEA-Profiles-macOS.png) + +4. 点击 IntelliJ IDEA 的运行按钮,等待项目启动完成。 + +5. 或者使用 Gradle 运行 + + ```bash + # macOS / Linux + ./gradlew bootRun --args="--spring.profiles.active=dev" + + # Windows + gradlew.bat bootRun --args="--spring.profiles.active=dev,win" + ``` + +6. 最终提供以下访问地址: + 1. 网站首页:[http://localhost:8090](http://localhost:8090) + 2. Console 控制台:[http://localhost:8090/console](http://localhost:8090/console) + 3. UC 个人中心:[http://localhost:8090/uc](http://localhost:8090/uc) diff --git a/versioned_docs/version-2.19/developer-guide/core/structure.md b/versioned_docs/version-2.19/developer-guide/core/structure.md new file mode 100644 index 00000000..cf76cd78 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/core/structure.md @@ -0,0 +1,36 @@ +--- +title: 系统结构 +description: Halo 项目的构成 +--- + +[Halo](https://github.com/halo-dev/halo) 博客系统分为以下四个部分: + +| 项目名称 | 简介 | +| :------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------- | +| [halo](https://github.com/halo-dev/halo) | 提供整个系统的服务,采用 [Spring Boot](https://spring.io/) 开发 | +| [halo-admin](https://github.com/halo-dev/halo-admin) | 负责后台管理的渲染,采用 [Vue](https://vuejs.org/) 开发,已集成在 Halo 运行包内,无需独立部署。 | +| [halo-comment](https://github.com/halo-dev/halo-comment) | 评论插件,采用 [Vue](https://vuejs.org/) 开发,在主题中运行方式引入构建好的 `JavaScript` 文件即可 | +| [halo-theme-\*](https://github.com/halo-dev) | 主题项目集,采用 [FreeMarker](https://freemarker.apache.org/) 模板引擎编写,需要包含一些特殊的配置才能够被 halo 所使用 | + +## 自定义配置 + +> 为什么要提前讲自定义配置呢?是因为在这里让大家了解到 `Halo` 的`配置方式`,以及`配置优先级`,不至于未来运行项目的时候不知道如何优雅地修改配置。 + +`Halo` 配置目录优先级如下(从上到下优先级越来越小,上层的配置将会覆盖下层): + +- `Halo` 自定义配置 + - file:~/.halo/ + - file:~/.halo-dev/ +- `Spring Boot` 默认配置 + - file:./config/ + - file:./ + - classpath:/config/ + - classpath:/ + +> 参考: [Application Property Files](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-application-property-files) + +在开发的时候,希望大家能够在 `~/halo-dev/application.yml` 中进行添加自定义配置。当然后面也会讲到如何用`运行参数` 和 `VM options` 进行控制配置,届时可根据具体情况进行选择。 + +:::warning +开发的时候,我们不建议直接更改`项目源码`中的所包含的`配置文件`,包括 `application.yml`、`application-dev.yml`、`application-test.yml` 和 `application-user.yml`。 +::: diff --git a/versioned_docs/version-2.19/developer-guide/form-schema.md b/versioned_docs/version-2.19/developer-guide/form-schema.md new file mode 100644 index 00000000..d51ac6a9 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/form-schema.md @@ -0,0 +1,559 @@ +--- +title: 表单定义 +--- + +在 Halo 2.0,在 Console 端的所有表单我们都使用了 [FormKit](https://github.com/formkit/formkit) 的方案。FormKit 不仅支持使用 Vue 组件的形式来构建表单,同时支持使用 Schema 的形式来构建。因此,我们的 [Setting](https://github.com/halo-dev/halo/blob/87ccd61ae5cd35a38324c30502d4e9c0ced41c6a/src/main/java/run/halo/app/core/extension/Setting.java#L20) 资源中的表单定义,都是使用 FormKit Schema 来定义的,最常用的场景即主题和插件的设置表单定义。当然,如果要在 Halo 2.0 的插件中使用,也可以参考 FormKit 的文档使用 Vue 组件的形式使用,但不需要在插件中引入 FormKit。 + +此文档将不会介绍 FormKit 的具体使用教程,因为我们已经很好的集成了 FormKit,并且使用方式基本无异。此文章将介绍 Halo 2.0 中表单定义的一些规范,以及额外的一些输入组件。 + +FormKit 相关文档: + +- Form Schema: [https://formkit.com/essentials/schema](https://formkit.com/essentials/schema) +- FormKit Inputs: [https://formkit.com/inputs](https://formkit.com/inputs) + +:::tip +目前不支持 FormKit Pro 中的输入组件,但 Halo 额外提供了部分输入组件,将在下面文档列出。 +::: + +## Setting 资源定义方式 + +```yaml title="settings.yaml" +apiVersion: v1alpha1 +kind: Setting +metadata: + name: foo-setting +spec: + forms: + - group: group_1 + label: 分组 1 + formSchema: + - $formkit: radio + name: color_scheme + label: 默认配色 + value: system + options: + - label: 跟随系统 + value: system + - label: 深色 + value: dark + - label: 浅色 + value: light + + - group: group_2 + label: 分组 2 + formSchema: + - $formkit: text + name: username + label: 用户名 + value: "" + - $formkit: password + name: password + label: 密码 + value: "" +``` + +:::tip +需要注意的是,FormKit Schema 本身应该是 JSON 格式的,但目前我们定义一个表单所使用的是 YAML,可能在参考 FormKit 写法时需要手动转换一下。 +::: + +字段说明: + +1. `metadata.name`:设置资源的名称,建议以 `-setting` 结尾。 +2. `spec.forms`:表单定义,可以定义多个表单,每个表单都有一个 `group` 字段,用于区分不同的表单。 +3. `spec.forms[].label`:表单的标题。 +4. `spec.forms[].formSchema`:表单的定义,使用 FormKit Schema 来定义。虽然我们使用的 YAML,但与 FormKit Schema 完全一致。 + +## 组件类型 + +除了 FormKit 官方提供的常用输入组件之外,Halo 还额外提供了一些输入组件,这些输入组件可以在 Form Schema 中使用。 + +### select + +#### 描述 + +自定义的选择器组件,支持静态和动态数据源,支持多选等功能。 + +#### 参数 + +- `options`:静态数据源。当 `action` 存在时,此参数无效。 +- `action`:远程动态数据源的接口地址。 +- `requestOption`: 动态数据源的请求参数,可以通过此参数来指定如何获取数据,适配不同的接口。当 `action` 存在时,此参数有效。 +- `remoteOptimize`:是否开启远程数据源优化,默认为 `true`。开启后,将会对远程数据源进行优化,减少请求次数。仅在动态数据源下有效。 +- `allowCreate`:是否允许创建新选项,默认为 `false`。仅在静态数据源下有效。 +- `clearable`:是否允许清空选项,默认为 `false`。 +- `multiple`:是否多选,默认为 `false`。 +- `maxCount`:多选时最大可选数量,默认为 `Infinity`。仅在多选时有效。 +- `sortable`:是否支持拖动排序,默认为 `false`。仅在多选时有效。 +- `searchable`: 是否支持搜索,默认为 `false`。 + +#### 参数类型定义 + +```ts +{ + options?: Array< + Record & { + label: string; + value: string; + } + >; + action?: string; + requestOption?: { + method?: "GET" | "POST"; + + /** + * 请求结果中 page 的字段名,默认为 `page`。 + */ + pageField?: PropertyPath; + + /** + * 请求结果中 size 的字段名,默认为 `size`。 + */ + sizeField?: PropertyPath; + + /** + * 请求结果中 total 的字段名,默认为 `total`。 + */ + totalField?: PropertyPath; + + /** + * 从请求结果中解析数据的字段名,默认为 `items`。 + */ + itemsField?: PropertyPath; + + /** + * 从 items 中解析出 label 的字段名,默认为 `label`。 + */ + labelField?: PropertyPath; + + /** + * 从 items 中解析出 value 的字段名,默认为 `value`。 + */ + valueField?: PropertyPath; + }; + remoteOptimize?: boolean; + allowCreate?: boolean; + clearable?: boolean; + multiple?: boolean; + maxCount?: number; + sortable?: boolean; + searchable?: boolean; +} +``` + +#### 静态数据示例 + +```yaml +- $formkit: select + name: countries + label: What country makes the best food? + sortable: true + multiple: true + clearable: true + searchable: true + placeholder: Select a country + options: + - label: China + value: cn + - label: France + value: fr + - label: Germany + value: de + - label: Spain + value: es + - label: Italy + value: ie + - label: Greece + value: gr +``` + +#### 远程动态数据示例 + +支持远程动态数据源,通过 `action` 和 `requestOption` 参数来指定如何获取数据。 + +请求的接口将会自动拼接 `page`、`size` 与 `keyword` 参数,其中 `keyword` 为搜索关键词。 + +```yaml +- $formkit: select + name: postName + label: Choose an post + clearable: true + action: /apis/api.console.halo.run/v1alpha1/posts + requestOption: + method: GET + pageField: page + sizeField: size + totalField: total + itemsField: items + labelField: post.spec.title + valueField: post.metadata.name +``` + +:::tip +当远程数据具有分页时,可能会出现默认选项不在第一页的情况,此时 Select 组件将会发送另一个查询请求,以获取默认选项的数据。此接口会携带如下参数: + +```ts +fieldSelector: `${requestOption.valueField}=(value1,value2,value3)` +``` + +其中,value1, value2, value3 为默认选项的值。返回值与查询一致,通过 `requestOption` 解析。 +::: + +### list + +#### 描述 + +列表类型的输入组件,支持动态添加、删除数据项。 + +#### 参数 + +- `item-type`:数据项的数据类型,用于初始化数据。可选参数 `string`, `number`, `boolean`, `object`,默认为 `string` +- `min`:数组最小要求数量,默认为 `0` +- `max`:数组最大容量,默认为 `Infinity`,即无限制 +- `addButton`:是否显示添加按钮 +- `addLabel`:添加按钮的文本 +- `upControl`:是否显示上移按钮 +- `downControl`:是否显示下移按钮 +- `insertControl`:是否显示插入按钮 +- `removeControl`:是否显示移除按钮 + +#### 示例 + +```yaml +- $formkit: list + name: socials + label: 社交账号 + addLabel: 添加账号 + min: 1 + max: 5 + itemType: string + children: + - $formkit: text + index: "$index" + validation: required +``` + +:::tip +`list` 组件有且只有一个子节点,并且必须为子节点传递 `index` 属性。若想提供多个字段,则建议使用 `group` 组件包裹,并将 itemType 改为 object。 +::: + +最终保存表单之后得到的值为以下形式: + +```json +{ + "socials": [ + "GitHub", + "Twitter" + ] +} +``` + +### verificationForm + +#### 描述 + +用于远程验证一组数据是否符合要求的组件。 + +#### 参数 + +- `action`:对目标数据进行验证的接口地址 +- `label`:验证按钮文本 +- `submitAttrs`:验证按钮的额外属性 + +#### 示例 + +```yaml +- $formkit: verificationForm + action: /apis/console.api.halo.run/v1alpha1/verify/verify-password + label: 账户校验 + children: + - $formkit: text + label: "用户名" + name: username + validation: required + - $formkit: password + label: "密码" + name: password + validation: required +``` + +:::tip +尽管 `verificationForm` 本身是一个输入组件,但与其他输入组件不同的是,它仅仅用于包装待验证的数据,所以并不会破坏原始数据的格式。例如上述示例中的值在保存后为: + +```json +{ + "username": "admin", + "password": "admin" +} +``` + +而不是 + +```json +{ + "verificationForm": { + "username": "admin", + "password": "admin" + } +} +``` + +::: + +示例中发送至验证地址的值为如下格式: + +```json +{ + "username": "admin", + "password": "admin" +} +``` + +当验证接口返回成功响应时,则验证通过,否则验证失败。 + +若用户在验证失败时想显示错误信息,可以在验证接口返回错误信息,该错误信息的结构定义需遵循 [RFC 7807 - Problem Details for HTTP APIs](https://datatracker.ietf.org/doc/html/rfc7807.html) 。例如: + +```json +{ + "title": "无效凭据", + "status": 401, + "detail": "用户名或密码错误。" +} +``` + +UI 效果: + + + +### repeater + +#### 描述 + +一组重复的输入组件,可以用于定义一组数据,最终得到的数据为一个对象的数组,可以方便地让使用者对其进行增加、移除、排序等操作。 + +#### 参数 + +- `min`:数组最小要求数量,默认为 `0` +- `max`:数组最大容量,默认为 `Infinity`,即无限制 +- `addButton`:是否显示添加按钮 +- `addLabel`:添加按钮的文本 +- `upControl`:是否显示上移按钮 +- `downControl`:是否显示下移按钮 +- `insertControl`:是否显示插入按钮 +- `removeControl`:是否显示移除按钮 + +#### 示例 + +```yaml +- $formkit: repeater + name: socials + label: 社交账号 + value: [] + max: 5 + min: 1 + children: + - $formkit: select + name: enabled + id: enabled + label: 是否启用 + options: + - label: 是 + value: true + - label: 否 + value: false + - $formkit: text + # 在 Repeater 中进行条件判断的方式,当 enabled 为 true 时才显示 + if: "$value.enabled === true", + name: name + label: 名称 + value: "" + - $formkit: text + if: "$value.enabled === true", + name: url + label: 地址 + value: "" +``` + +:::tip +使用 `repeater` 类型时,一定要设置默认值,如果不需要默认有任何元素,可以设置为 `[]`。 +::: + +其中 `name` 和 `url` 即数组对象的属性,最终保存表单之后得到的值为以下形式: + +```json +{ + "socials": [ + { + "name": "GitHub", + "url": "https://github.com/halo-dev" + } + ] +} +``` + +UI 效果: + + + +### attachment + +#### 描述 + +附件类型的输入框,支持直接调用附件库弹框选择附件。 + +#### 参数 + +- `accepts`:文件类型,数据类型为 `string[]`。 + +#### 示例 + +```yaml +- $formkit: attachment + name: logo + label: Logo + accepts: + - "image/png" + - "video/mp4" + - "audio/*" + value: "" +``` + +### code + +#### 描述 + +代码编辑器的输入组件,集成了 [Codemirror](https://codemirror.net/)。 + +#### 参数 + +- `language`:代码语言,目前支持 `yaml` `html` `javascript` `css` `json`。 +- `height`:代码编辑器的高度。 + +#### 示例 + +```yaml +- $formkit: code + name: footer_code + label: 页脚代码注入 + value: "" + language: yaml +``` + +### menuCheckbox + +#### 描述 + +菜单复选框,用于选择系统内的导航菜单。其中选择的值为菜单资源 `metadata.name` 的集合。 + +#### 示例 + +```yaml +- $formkit: menuCheckbox + name: menus + label: 菜单 + value: [] +``` + +### menuRadio + +#### 描述 + +菜单单选框,用于选择系统内的导航菜单。其中选择的值为菜单资源 `metadata.name`。 + +#### 示例 + +```yaml +- $formkit: menuRadio + name: menu + label: 菜单 + value: "" +``` + +### postSelect + +#### 描述 + +文章选择器,用于选择系统内的文章。其中选择的值为文章资源 `metadata.name`。 + +#### 示例 + +```yaml +- $formkit: postSelect + name: post + label: 文章 + value: "" +``` + +### singlePageSelect + +#### 描述 + +单页选择器,用于选择系统内的独立页面。其中选择的值为独立页面资源 `metadata.name`。 + +#### 示例 + +```yaml +- $formkit: singlePageSelect + name: singlePage + label: 单页 + value: "" +``` + +### categorySelect + +#### 描述 + +文章分类选择器,用于选择系统内的文章分类。其中选择的值为文章分类资源 `metadata.name`。 + +#### 示例 + +```yaml +- $formkit: categorySelect + name: category + label: 分类 + value: "" +``` + +### categoryCheckbox + +#### 描述 + +文章分类复选框,用于选择系统内的文章分类。其中选择的值为文章分类资源 `metadata.name` 的集合。 + +#### 示例 + +```yaml +- $formkit: categoryCheckbox + name: categories + label: 分类 + value: [] +``` + +### tagSelect + +#### 描述 + +文章标签选择器,用于选择系统内的文章标签。其中选择的值为文章标签资源 `metadata.name`。 + +#### 示例 + +```yaml +- $formkit: tagSelect + name: tag + label: 标签 + value: "" +``` + +### tagCheckbox + +#### 描述 + +文章标签复选框,用于选择系统内的文章标签。其中选择的值为文章标签资源 `metadata.name` 的集合。 + +#### 示例 + +```yaml +- $formkit: tagCheckbox + name: tags + label: 标签 + value: [] +``` diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-client.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-client.md new file mode 100644 index 00000000..ce6e994a --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-client.md @@ -0,0 +1,222 @@ +--- +title: 与自定义模型交互 +description: 了解如何通过代码的方式操作数据 +--- + +Halo 提供了两个类用于与自定义模型对象交互 `ExtensionClient` 和 `ReactiveExtensionClient`。 + +它们提供了对自定义模型对象的增删改查操作,`ExtensionClient` 是阻塞式的用于后台任务如控制器中操作数据,而 `ReactiveExtensionClient` 返回值都是 Mono 或 Flux 是反应式非阻塞的,它们由 [reactor](https://projectreactor.io/) 提供。 + +```java +public interface ReactiveExtensionClient { + + // 已经过时,建议使用 listBy 或 listAll 代替 + Flux list(Class type, Predicate predicate, + Comparator comparator); + + // 已经过时,建议使用 listBy 或 listAll 代替 + Mono> list(Class type, Predicate predicate, + Comparator comparator, int page, int size); + + Flux listAll(Class type, ListOptions options, Sort sort); + + Mono> listBy(Class type, ListOptions options, + PageRequest pageable); + + /** + * Fetches Extension by its type and name. + * + * @param type is Extension type. + * @param name is Extension name. + * @param is Extension type. + * @return an optional Extension. + */ + Mono fetch(Class type, String name); + + Mono fetch(GroupVersionKind gvk, String name); + + Mono get(Class type, String name); + + /** + * Creates an Extension. + * + * @param extension is fresh Extension to be created. Please make sure the Extension name does + * not exist. + * @param is Extension type. + */ + Mono create(E extension); + + /** + * Updates an Extension. + * + * @param extension is an Extension to be updated. Please make sure the resource version is + * latest. + * @param is Extension type. + */ + Mono update(E extension); + + /** + * Deletes an Extension. + * + * @param extension is an Extension to be deleted. Please make sure the resource version is + * latest. + * @param is Extension type. + */ + Mono delete(E extension); +} +``` + +### 示例 + +如果你想在插件中根据 name 参数查询获取到 Person 自定义模型的数据,则可以这样写: + +```java +@Service +@RequiredArgsConstructor +public PersonService { + private final ReactiveExtensionClient client; + + Mono getPerson(String name) { + return client.fetch(Person.class, name); + } +} +``` + +或者使用阻塞式 Client + +```java +@Service +@RequiredArgsConstructor +public PersonService { + private final ExtensionClient client; + + Optional getPerson(String name) { + return client.fetch(Person.class, name); + } +} +``` + +注意:非阻塞线程中不能调用阻塞式方法。 + +我们建议你更多的使用响应式的 `ReactiveExtensionClient` 去替代 `ExtensionClient`。 + +### 查询 + +`ReactiveExtensionClient` 提供了两个方法用于查询数据,`listBy` 和 `listAll`。 + +`listBy` 方法用于分页查询数据,`listAll` 方法用于查询所有数据,它们都需要一个 `ListOptions` 参数,用于传递查询条件: + +```java +public class ListOptions { + private LabelSelector labelSelector; + private FieldSelector fieldSelector; +} +``` + +其中 `LabelSelector` 用于传递标签查询条件,`FieldSelector` 用于传递字段查询条件。 + +`FieldSelector` 支持比自动生成的 APIs 中更多的查询条件,可以通过 `run.halo.app.extension.index.query.QueryFactory` 来构建。 + +```java +ListOptions.builder() + .fieldQuery(QueryFactory.and( + QueryFactory.equal("name", "test"), + QueryFactory.equal("age", 18) + )) + .build(); +``` + +支持的查询条件如下: + +| 方法 | 说明 | 示例 | +| ---------------------------- | ---------------- | ----------------------------------------------------------------------------- | +| equal | 等于 | equal("name", "test"), name 是字段名,test 是字段值 | +| equalOtherField | 等于其他字段 | equalOtherField("name", "otherName"), name 是字段名,otherName 是另一个字段名 | +| notEqual | 不等于 | notEqual("name", "test") | +| notEqualOtherField | 不等于其他字段 | notEqualOtherField("name", "otherName") | +| greaterThan | 大于 | greaterThan("age", 18) | +| greaterThanOtherField | 大于其他字段 | greaterThanOtherField("age", "otherAge") | +| greaterThanOrEqual | 大于等于 | greaterThanOrEqual("age", 18) | +| greaterThanOrEqualOtherField | 大于等于其他字段 | greaterThanOrEqualOtherField("age", "otherAge") | +| lessThan | 小于 | lessThan("age", 18) | +| lessThanOtherField | 小于其他字段 | lessThanOtherField("age", "otherAge") | +| lessThanOrEqual | 小于等于 | lessThanOrEqual("age", 18) | +| lessThanOrEqualOtherField | 小于等于其他字段 | lessThanOrEqualOtherField("age", "otherAge") | +| in | 在范围内 | in("age", 18, 19, 20) | +| and | 且 | and(equal("name", "test"), equal("age", 18)) | +| or | 或 | or(equal("name", "test"), equal("age", 18)) | +| between | 在范围内 | between("age", 18, 20), 包含 18 和 20 | +| betweenExclusive | 在范围内 | betweenExclusive("age", 18, 20), 不包含 18 和 20 | +| betweenLowerExclusive | 在范围内 | betweenLowerExclusive("age", 18, 20), 不包含 18,包含 20 | +| betweenUpperExclusive | 在范围内 | betweenUpperExclusive("age", 18, 20), 包含 18,不包含 20 | +| startsWith | 以指定字符串开头 | startsWith("name", "test") | +| endsWith | 以指定字符串结尾 | endsWith("name", "test") | +| contains | 包含指定字符串 | contains("name", "test") | +| all | 指定字段的所有值 | all("age") | + +在 `FieldSelector` 中使用的所有字段都必须添加为索引,否则会抛出异常表示不支持该字段。关于如何使用索引请参考 [自定义模型使用索引](./extension.md#using-indexes)。 + +可以通过 `and` 和 `or` 方法组合和嵌套查询条件: + +```java +import static run.halo.app.extension.index.query.QueryFactory.and; +import static run.halo.app.extension.index.query.QueryFactory.equal; +import static run.halo.app.extension.index.query.QueryFactory.greaterThan; +import static run.halo.app.extension.index.query.QueryFactory.or; + +Query query = and( + or(equal("dept", "A"), equal("dept", "B")), + or(equal("age", "19"), equal("age", "18")) +); +ListOptions.builder() + .fieldQuery(query) + .build(); +``` + +### 构建 ListOptions + +ListOptions 提供了 `builder` 方法用于构建查询条件,`fieldQuery` 方法用于传递字段查询条件,`labelSelector` 方法用于传递标签查询条件。 + +```java +ListOptions.builder() + .labelSelector() + .eq("key-1", "value-1") + .end() + .fieldQuery(QueryFactory.equal("key-2", "value-2")) + .build(); +``` + +- `labelSelector` 之后使用 `end` 方法结束标签查询条件的构建。 +- `andQuery` 和 `orQuery` 用于组合多个 `FieldSelector` 查询条件。 + +### 排序 + +`listBy` 和 `listAll` 方法都支持传递 `Sort` 参数,用于传递排序条件。 + +```java +import org.springframework.data.domain.Sort; + +Sort.by(Sort.Order.asc("metadata.name")) +``` + +通过 `Sort.by` 方法可以构建排序条件,`Sort.Order` 用于指定排序字段和排序方式,`asc` 表示升序,`desc` 表示降序。 + +排序中使用的字段必须是添加为索引的字段,否则会抛出异常表示不支持该字段。关于如何使用索引请参考 [自定义模型使用索引](./extension.md#using-indexes)。 + +### 分页 + +`listBy` 方法支持传递 `PageRequest` 参数,用于传递分页条件。 + +```java +import run.halo.app.extension.PageRequestImpl; + +PageRequestImpl.of(1, 10); + +PageRequestImpl.of(1, 10, Sort.by(Sort.Order.asc("metadata.name")); + +PageRequestImpl.ofSize(10); +``` + +通过 `PageRequestImpl.of` 方法可以构建分页条件,具有两个参数的方法用于指定页码和每页数量,具有三个参数的方法用于指定页码、每页数量和排序条件。 + +`ofSize` 方法用于指定每页数量,页码默认为 1。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-getter.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-getter.md new file mode 100644 index 00000000..a8c44d5a --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-getter.md @@ -0,0 +1,72 @@ +--- +title: 获取扩展 +description: 了解如何在插件中使用 `ExtensionGetter` 获取扩展 +--- + +`ExtensionGetter` 用于获取和管理 Halo 或其他插件提供的扩展。它提供了多种方法来根据扩展点获取扩展,确保插件能够灵活地集成和使用各种扩展功能。 + +`ExtensionGetter` 接口的定义如下: + +```java +public interface ExtensionGetter { + + /** + * Get only one enabled extension from system configuration. + * + * @param extensionPoint is extension point class. + * @return implementation of the corresponding extension point. If no configuration is found, + * we will use the default implementation from application context instead. + */ + Mono getEnabledExtension(Class extensionPoint); + + /** + * Get the extension(s) according to the {@link ExtensionPointDefinition} queried + * by incoming extension point class. + * + * @param extensionPoint extension point class + * @return implementations of the corresponding extension point. + * @throws IllegalArgumentException if the incoming extension point class does not have + * the {@link ExtensionPointDefinition}. + */ + Flux getEnabledExtensions(Class extensionPoint); + + /** + * Get all extensions according to extension point class. + * + * @param extensionPointClass extension point class + * @param type of extension point + * @return a bunch of extension points. + */ + Flux getExtensions(Class extensionPointClass); +} +``` + +包含以下方法: + +1. `getEnabledExtension(Class extensionPoint)`: 获取一个在扩展设置中已启用的扩展。如果没有找到对应配置,将使用 Halo 中的默认扩展,如果 Halo 没有提供默认实现则找到一个由**已启用插件**提供的可用扩展。 +2. `getEnabledExtensions(Class extensionPoint)`: 根据传入的扩展点类获取所有已启用扩展。如果没有在扩展设置页面配置过则会返回所有可用的扩展。 +3. `getExtensions(Class extensionPointClass)`: 获取所有与扩展点类相关的扩展,无论是否在扩展设置中启用它。 + +:::tip Note +使用 `getEnabledExtension` 方法或者 `getEnabledExtensions` 方法取决于扩展点声明的 `type` 是 `SINGLETON` 还是 `MULTI_INSTANCE`。 + +通过使用 `ExtensionGetter`,开发者可以轻松地在插件中访问和管理各种扩展点,提升插件的功能和灵活性。 + +如果想了解 Halo 提供的扩展点请参考:[扩展点](./extension-points/index.md)。 +::: + +### 示例 + +如果你想在插件中获取已启用的搜索引擎扩展,可以使用 `ExtensionGetter` 来获取: + +```java +@Service +@RequiredArgsConstructor +public class SearchService { + private final ExtensionGetter extensionGetter; + + Mono getSearchEngine() { + return extensionGetter.getEnabledExtension(SearchEngine.class) + } +} +``` diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/additional-webfilter.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/additional-webfilter.md new file mode 100644 index 00000000..916ac347 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/additional-webfilter.md @@ -0,0 +1,76 @@ +--- +title: Web 过滤器 +description: 为 Web 请求提供过滤器扩展点,可用于对请求进行拦截、修改等操作。 +--- + +在现代的 Web 应用开发中,过滤器(Filter)是一个非常重要的概念。你可以使用 `run.halo.app.security.AdditionalWebFilter` 在服务器处理请求之前或之后执行特定的任务。 + +通过实现这个接口,开发者可以自定义过滤逻辑,用于处理进入和离开应用程序的 HTTP 请求和响应。 + +AdditionalWebFilter 能做什么? + +1. 认证与授权: AdditionalWebFilter 可以用来检查用户是否登录,或者是否有权限访问某个资源。 +2. 日志记录与审计: 在请求处理之前或之后记录日志,帮助了解应用程序的使用情况。 +3. 请求重构: 修改请求数据,例如添加、删除或修改请求头或请求参数。 +4. 响应处理: 修改响应,例如设置通用的响应头。 +5. 性能监控: 记录处理请求所需的时间,用于性能分析。 +6. 异常处理: 统一处理请求过程中抛出的异常。 +7. ...... + +## 使用示例 + +以下是一个使用 `AdditionalWebFilter` 来拦截 `/login` 请求实现用户名密码登录的示例: + +```java +@Component +public class UsernamePasswordAuthenticator implements AdditionalWebFilter { + final ServerWebExchangeMatcher requiresMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login"); + + @Override + @NonNull + public Mono filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) { + return this.requiresAuthenticationMatcher.matches(exchange) + .filter((matchResult) -> { + return matchResult.isMatch(); + }).flatMap((matchResult) -> { + return this.authenticationConverter.convert(exchange); + }).switchIfEmpty(chain.filter(exchange) + .then(Mono.empty())) + .flatMap((token) -> { + return this.authenticate(exchange, chain, token); + }).onErrorResume(AuthenticationException.class, (ex) -> { + return this.authenticationFailureHandler.onAuthenticationFailure(new WebFilterExchange(exchange, chain), ex); + }); + } + + @Override + public int getOrder() { + return SecurityWebFiltersOrder.FORM_LOGIN.getOrder(); + } +} +``` + +1. `filter` 方法中的 `chain.filter(exchange)` 表示继续执行后续的过滤器,如果不调用这个方法,请求将不会继续执行后续的过滤器或目标处理程序。 +2. `getOrder` 方法用于指定过滤器的执行顺序,比如 `SecurityWebFiltersOrder.FORM_LOGIN.getOrder()` 表示在 Spring Security 的表单登录过滤器之前执行,参考:[SecurityWebFiltersOrder](https://github.com/spring-projects/spring-security/blob/main/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java)。 + +`AdditionalWebFilter` 对应的 `ExtensionPointDefinition` 如下: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: additional-webfilter +spec: + className: run.halo.app.security.AdditionalWebFilter + displayName: AdditionalWebFilter + type: MULTI_INSTANCE + description: "Contract for interception-style, chained processing of Web requests that may be used to + implement cross-cutting, application-agnostic requirements such as security, timeouts, and others." +``` + +即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `additional-webfilter`。 + +以下是一些可以参考的项目示例: + +- [OAuth2 第三方登录插件](https://github.com/halo-sigs/plugin-oauth2) +- [Halo 用户名密码登录](https://github.com/halo-dev/halo/blob/main/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticator.java) diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/attachment.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/attachment.md new file mode 100644 index 00000000..f6bb260f --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/attachment.md @@ -0,0 +1,55 @@ +--- +title: 附件存储 +description: 为附件存储方式提供的扩展点,可用于自定义附件存储方式。 +--- +附件存储策略扩展点支持扩展附件的上传和存储方式,如将附件存储到第三方云存储服务中。 + +扩展点接口如下: + +```java +public interface AttachmentHandler extends ExtensionPoint { + + Mono upload(UploadContext context); + + Mono delete(DeleteContext context); + + default Mono getSharedURL(Attachment attachment, + Policy policy, + ConfigMap configMap, + Duration ttl) { + return Mono.empty(); + } + + default Mono getPermalink(Attachment attachment, + Policy policy, + ConfigMap configMap) { + return Mono.empty(); + } +``` + +- `upload` 方法用于上传附件,返回值为 `Mono`,其中 `Attachment` 为上传成功后的附件对象。 +- `delete` 方法用于删除附件,返回值为 `Mono`,其中 `Attachment` 为删除后的附件对象。 +- `getSharedURL` 方法用于获取附件的共享链接,返回值为 `Mono`,其中 `URI` 为附件的共享链接。 +- `getPermalink` 方法用于获取附件的永久链接,返回值为 `Mono`,其中 `URI` 为附件的永久链接。 + +`AttachmentHandler` 对应的 `ExtensionPointDefinition` 如下: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: attachment-handler +spec: + className: run.halo.app.core.extension.attachment.endpoint.AttachmentHandler + displayName: AttachmentHandler + type: MULTI_INSTANCE + description: "Provide extension points for attachment storage strategies" +``` + +即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `attachment-handler`。 + +可以参考以下项目示例: + +- [S3 对象存储协议的存储插件](https://github.com/halo-dev/plugin-s3) +- [阿里云 OSS 的存储策略插件](https://github.com/halo-sigs/plugin-alioss) +- [又拍云 OSS 的存储策略](https://github.com/AirboZH/plugin-uposs) diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/authentication-webfilter.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/authentication-webfilter.md new file mode 100644 index 00000000..f54e8057 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/authentication-webfilter.md @@ -0,0 +1,81 @@ +--- +title: 认证安全过滤器 +description: 提供 Security WebFilter 扩展点,插件可实现自定义认证逻辑,例如:用户名密码认证,JWT 认证,匿名认证。 +--- + +此前,Halo 提供了 AdditionalWebFilter 作为扩展点供插件扩展认证相关的功能。但是近期我们明确了 AdditionalWebFilter +的使用用途,故不再作为认证的扩展点。 + +目前,Halo 提供了三种认证扩展点:表单登录认证、普通认证和匿名认证。 + +## 表单登录(FormLogin) + +示例如下: + +```java +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; +import run.halo.app.security.FormLoginSecurityWebFilter; + +@Component +public class MyFormLoginSecurityWebFilter implements FormLoginSecurityWebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + // Do your logic here + return chain.filter(exchange); + } +} + +``` + +## 普通认证(Authentication) + +示例如下: + +```java +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; +import run.halo.app.security.AuthenticationSecurityWebFilter; + +@Component +public class MyAuthenticationSecurityWebFilter implements AuthenticationSecurityWebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + // Do your logic here + return chain.filter(exchange); + } +} +``` + +## 匿名认证(Anonymous Authentication) + +示例如下: + +```java +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; +import run.halo.app.security.AnonymousAuthenticationSecurityWebFilter; + +@Component +public class MyAnonymousAuthenticationSecurityWebFilter + implements AnonymousAuthenticationSecurityWebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + // Do your logic here + return chain.filter(exchange); + } +} +``` + +我们在实现扩展点的时候需要注意:如果当前请求不满足认证条件,请一定要调用 `chain.filter(exchange)`,给其他 filter 留下机会。 + +后续会根据需求实现其他认证相关的扩展点。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/comment-subject.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/comment-subject.md new file mode 100644 index 00000000..4542cdcc --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/comment-subject.md @@ -0,0 +1,83 @@ +--- +title: 评论主体展示 +description: 用于在管理端评论列表中展示评论的主体内容。 +--- + +评论主体扩展点用于在管理端评论列表中展示评论的主体内容,评论自定义模型是 Halo 主应用提供的,如果你需要使用评论自定义模型,那么评论会统一 +展示在管理后台的评论列表中,这时就需要通过评论主体扩展点来展示评论的主体内容便于跳转到对应的页面,如果没有实现该扩展点,那么评论列表中对应的评论的主体会显示为“未知”。 + +```java +public interface CommentSubject extends ExtensionPoint { + + Mono get(String name); + + default Mono getSubjectDisplay(String name) { + return Mono.empty(); + } + + boolean supports(Ref ref); + + record SubjectDisplay(String title, String url, String kindName) { + } +} +``` + +- `get` 方法用于获取评论主体,参数 `name` 是评论主体的自定义模型对象的名称,返回值为 `Mono`,其中 `T` 为评论主体对象,它是使用评论的那个自定义模型。 +- `getSubjectDisplay` 方法用于获取评论主体的展示信息,返回值为 `Mono`,其中 `SubjectDisplay` 为评论主体的展示信息,包含标题、链接和类型名称,用于在主题端展示评论主体的信息。 +- `supports` 方法用于判断是否支持该评论主体,返回值为 `boolean`,如果支持则返回 `true`,否则返回 `false`。 + +实现该扩展点后,评论列表会通过 `get` 方法将主体的自定义模型对象带到评论列表中,可以配置前端的扩展点来决定如何展示评论主体的信息,参考:[UI 评论来源显示](../../ui/extension-points//comment-subject-ref-create.md) + +例如对于文章是支持评论的,所以文章的评论主体扩展点实现如下: + +```java +public class PostCommentSubject implements CommentSubject { + + private final ReactiveExtensionClient client; + private final ExternalLinkProcessor externalLinkProcessor; + + @Override + public Mono get(String name) { + return client.fetch(Post.class, name); + } + + @Override + public Mono getSubjectDisplay(String name) { + return get(name) + .map(post -> { + var url = externalLinkProcessor + .processLink(post.getStatusOrDefault().getPermalink()); + return new SubjectDisplay(post.getSpec().getTitle(), url, "文章"); + }); + } + + @Override + public boolean supports(Ref ref) { + Assert.notNull(ref, "Subject ref must not be null."); + GroupVersionKind groupVersionKind = + new GroupVersionKind(ref.getGroup(), ref.getVersion(), ref.getKind()); + return GroupVersionKind.fromExtension(Post.class).equals(groupVersionKind); + } +} +``` + +`CommentSubject` 对应的 `ExtensionPointDefinition` 如下: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: comment-subject +spec: + className: run.halo.app.content.comment.CommentSubject + displayName: CommentSubject + type: MULTI_INSTANCE + description: "Provide extension points for comment subject display" +``` + +即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `comment-subject`。 + +可以参考其他使用该扩展点的项目示例: + +- [Halo 自定义页面评论主体](https://github.com/halo-dev/halo/blob/main/application/src/main/java/run/halo/app/content/comment/SinglePageCommentSubject.java) +- [瞬间的评论主体](https://github.com/halo-sigs/plugin-moments/blob/096b1b3e4a2ca44b6f858ba1181b62eeff64a139/src/main/java/run/halo/moments/MomentCommentSubject.java#L25) diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/comment-widget.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/comment-widget.md new file mode 100644 index 00000000..6bdde216 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/comment-widget.md @@ -0,0 +1,41 @@ +--- +title: 评论组件 +description: 用于自定义评论组件,可在主题端使用其他评论组件。 +--- +评论组件扩展点用于自定义主题端使用的评论组件,Halo 通过插件提供了一个默认的评论组件,如果你需要使用其他的评论组件,那么可以通过实现该扩展点来自定义评论组件。 + +```java +public interface CommentWidget extends ExtensionPoint { + + String ENABLE_COMMENT_ATTRIBUTE = CommentWidget.class.getName() + ".ENABLE"; + + void render(ITemplateContext context, IProcessableElementTag tag, + IElementTagStructureHandler structureHandler); +} +``` + +其中 `render` 方法用于在主题端模板中渲染评论组件,参数: + +- `context` 为模板上下文,包含执行模板的上下文:变量、模板数据等。 +- 参数 `tag` 为 `` 标签它包含元素的名称及其属性 +- `structureHandler` 是一个特殊的对象,它允许 `CommentWidget` 向引擎发出指令,指示模板引擎应根据处理器的执行而执行哪些操作。 + +`CommentWidget` 对应的 `ExtensionPointDefinition` 如下: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: comment-widget +spec: + className: run.halo.app.theme.dialect.CommentWidget + displayName: CommentWidget + type: SINGLETON + description: "Provides an extension point for the comment widget on the theme-side." +``` + +即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `comment-widget`。 + +参考:[Thymeleaf IElementTagProcessor 文档](https://www.thymeleaf.org/doc/tutorials/3.1/extendingthymeleaf.html#element-tag-processors-ielementtagprocessor)。 + +参考:[Halo 默认评论组件的实现](https://github.com/halo-dev/plugin-comment-widget/blob/main/src/main/java/run/halo/comment/widget/DefaultCommentWidget.java)。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/index.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/index.md new file mode 100644 index 00000000..2cb89985 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/index.md @@ -0,0 +1,65 @@ +--- +title: 扩展点 +description: Halo 服务端为插件提供的扩展点接口 +--- + +术语: + +- 扩展点 + - 由 Halo 定义的用于添加特定功能的接口。 + - 扩展点应该在服务的核心功能和它所认为的集成之间的交叉点上。 + - 扩展点是对服务的扩充,但不是影响服务的核心功能:区别在于,如果没有其核心功能,服务就无法运行,而扩展点对于特定的配置可能至关重要该服务最终是可选的。 + - 扩展点应该小且可组合,并且在相互配合使用时,可为 Halo 提供比其各部分总和更大的价值。 +- 扩展 + - 扩展点的一种具体实现。 + +使用 Halo 扩展点的必要步骤是: + +1. 实现扩展点接口,然后标记上 `@Component` 注解。 +2. 对该扩展点的实现类进行 `ExtensionDefinition` 自定义模型对象的声明。 + +例如: 实现一个通知器的扩展,首先 `implements` ReactiveNotifier 扩展点接口: + +```java +@Component +public class EmailNotifier implements ReactiveNotifier { + @Override + public Mono notify(NotificationContext context) { + // Send notification + } +} +``` + +然后声明一个 `ExtensionDefinition` 自定义模型对象: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionDefinition +metadata: + name: halo-email-notifier # 指定一个扩展定义的名称 +spec: + # 扩展的全限定类名 + className: run.halo.app.notification.EmailNotifier + # 所实现的扩展点定义的自定义模型对象名称 + extensionPointName: reactive-notifier + # 扩展名称用于展示给用户 + displayName: "EmailNotifier" + # 扩展的简要描述,用于让用户了解该扩展的作用 + description: "Support sending notifications to users via email" +``` + +:::tip Note +单实例或多实例的扩展点需要声明对应的 `ExtensionPointDefinition` 自定义模型对象被称之为扩展点定义,用于描述该扩展点的信息,例如:扩展点的名称、描述、扩展点的类型等。 + +单实例或多实例扩展点的实现也必须声明一个对应的 `ExtensionDefinition` 自定义模型对象被称之为扩展定义,用于描述该扩展的信息,例如:扩展的名称、描述、对应扩展点的对象名称等。 +::: + +关于如何在插件中声明自定义模型对象请参考:[自定义模型](../../server/extension.md#declare-extension-object) + +以下是目前已支持的扩展点列表: + +```mdx-code-block +import DocCardList from '@theme/DocCardList'; + + +``` diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/notifier.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/notifier.md new file mode 100644 index 00000000..5fbd45fa --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/notifier.md @@ -0,0 +1,60 @@ +--- +title: 通知器 +description: 为以何种方式向用户发送通知提供的扩展点。 +--- + +通知器扩展点是用于扩展为 Halo 通知系统提供更多通知方式的扩展点,例如:邮件、短信、WebHook 等。 + +```java +public interface ReactiveNotifier extends ExtensionPoint { + + Mono notify(NotificationContext context); +} +``` + +`notify` 方法用于发送通知,参数:context 为通知上下文,包含通知的内容、接收者、通知配置等信息。 + +除了实现该扩展点并声明 `ExtensionDefinition` 自定义模型对象外,还需要声明一个 `NotifierDescriptor` 自定义模型对象用于描述通知器,例如: + +```yaml +apiVersion: notification.halo.run/v1alpha1 +kind: NotifierDescriptor +metadata: + name: default-email-notifier +spec: + displayName: '邮件通知' + description: '通过邮件将通知发送给用户' + notifierExtName: 'halo-email-notifier' + senderSettingRef: + name: 'notifier-setting-for-email' + group: 'sender' + #receiverSettingRef: + # name: '' + # group: '' +``` + +- `notifierExtName` 为通知器扩展的自定义模型对象名称 +- `senderSettingRef` 用于声明通知器的发送者配置,例如:邮件通知器的发送者配置为:SMTP 服务器地址、端口、用户名、密码等,如果没有可以不配置,参考:[表单定义](../../../../form-schema.md) + - `name` 为发送者配置的名称,它是一个 `Setting` 自定义模型对象的名称。 + - `group` 用于引用到一个具体的配置 Schema 组,它是一个 `Setting` 自定义模型对象中描述的 `formSchema` 的 `group`,由于 `Setting` 可以声明多个配置分组但通知器的发送者配置只能有在一个组,因此需要指定一个组。 +- `receiverSettingRef` 用于声明通知器的接收者配置,例如:邮件通知器的接收者配置为:接收者邮箱地址,如果没有可以不配置,`name` 和 `group` 配置同 `senderSettingRef`。 + +当配置了 `senderSettingRef` 后,触发通知时 `notify` 方法的 `context` 参数中会包含 `senderConfig` 即为发送者配置的值,`receiverConfig` 同理。 + +`ReactiveNotifier` 对应的 `ExtensionPointDefinition` 如下: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: reactive-notifier +spec: + className: run.halo.app.notification.ReactiveNotifier + displayName: Notifier + type: MULTI_INSTANCE + description: "Provides a way to extend the notifier to send notifications to users." +``` + +即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `reactive-notifier`。 + +使用案例可以参考:[Halo 邮件通知器](https://github.com/halo-dev/halo/blob/main/application/src/main/java/run/halo/app/notification/EmailNotifier.java) diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/post-content.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/post-content.md new file mode 100644 index 00000000..089815a6 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/post-content.md @@ -0,0 +1,43 @@ +--- +title: 主题端文章内容处理 +description: 提供扩展主题端文章内容处理的方法,干预文章内容的渲染。 +--- + +主题端文章内容处理扩展点用于干预文章内容的渲染,例如:在文章内容中添加广告、添加版权信息等。 + +```java +public interface ReactivePostContentHandler extends ExtensionPoint { + + Mono handle(@NonNull PostContentContext postContent); + + @Data + @Builder + class PostContentContext { + private Post post; + private String content; + private String raw; + private String rawType; + } +} +``` + +`handle` 方法用于处理文章内容,参数 `postContent` 为文章内容上下文,包含文章自定义模型对象、文章 html 内容、原始内容、原始内容类型等信息。 + +`ReactivePostContentHandler` 对应的 `ExtensionPointDefinition` 如下: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: reactive-post-content-handler +spec: + className: run.halo.app.theme.ReactivePostContentHandler + displayName: ReactivePostContentHandler + type: MULTI_INSTANCE + description: "Provides a way to extend the post content to be displayed on the theme-side." +``` + +即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `reactive-post-content-handler`。 + +使用案例可以参考:[WebP Cloud 插件](https://github.com/webp-sh/halo-plugin-webp-cloud/blob/a6069dfa78931de0d5b5dfe98fdd18a0da75b09f/src/main/java/se/webp/plugin/WebpCloudPostContentHandler.java#L17) +它的作用是处理主题端文章内容中的所有图片的地址,将其替换为一个 WebP Cloud 的代理地址,从而实现文章内容中的图片都使用 WebP 格式。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/singlepage-content.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/singlepage-content.md new file mode 100644 index 00000000..b2aad2c9 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/singlepage-content.md @@ -0,0 +1,38 @@ +--- +title: 主题端自定义页面内容处理 +description: 提供扩展主题端自定义页面内容处理的方法,干预自定义页面内容的渲染。 +--- + +主题端自定义页面内容处理扩展点,作用同 [主题端文章内容处理](./post-content.md) 扩展点,只是作用于自定义页面。 + +```java +public interface ReactiveSinglePageContentHandler extends ExtensionPoint { + + Mono handle(@NonNull SinglePageContentContext singlePageContent); + + @Data + @Builder + class SinglePageContentContext { + private SinglePage singlePage; + private String content; + private String raw; + private String rawType; + } +} +``` + +`ReactiveSinglePageContentHandler` 对应的 `ExtensionPointDefinition` 如下: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: reactive-singlepage-content-handler +spec: + className: run.halo.app.theme.ReactiveSinglePageContentHandler + displayName: ReactiveSinglePageContentHandler + type: MULTI_INSTANCE + description: "Provides a way to extend the single page content to be displayed on the theme-side." +``` + +即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `reactive-singlepage-content-handler`。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/template-footer-processor.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/template-footer-processor.md new file mode 100644 index 00000000..7035c99f --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/template-footer-processor.md @@ -0,0 +1,77 @@ +--- +title: 主题端 Halo Footer 标签处理 +description: 提供扩展主题端 HTML 页面中的 标签内容处理的方法。 +--- + +Halo 为主题端模板提供了自定义标签 `` 的处理扩展点,以便可以添加额外的页脚内容如版权信息、备案号等。 + +## 使用场景 + +- 添加备案号 +- 添加版权信息 +- 添加统计代码 +- 添加自定义脚本 +- 添加自定义链接 + +## 扩展点定义 + +扩展 `` 标签的扩展点定义为 `TemplateFooterProcessor`,对应的 `ExtensionPoint` 类型为 `MULTI_INSTANCE`,即可以有多个实现类。 + +```java +public interface TemplateFooterProcessor extends ExtensionPoint { + + Mono process(ITemplateContext context, IProcessableElementTag tag, + IElementTagStructureHandler structureHandler, IModel model); +} +``` + +`TemplateFooterProcessor` 对应的 `ExtensionPointDefinition` 资源描述如下: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: template-footer-processor +spec: + className: run.halo.app.theme.dialect.TemplateFooterProcessor + displayName: 页脚标签内容处理器 + type: MULTI_INSTANCE + description: "提供用于扩展 标签内容的扩展方式。" +``` + +即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `template-footer-processor`。 + +## 示例实现 + +以下是一个简单的 TemplateFooterProcessor 插件实现示例: + +```java +@Component +public class FakeFooterCodeInjection implements TemplateFooterProcessor { + + @Override + public Mono process(ITemplateContext context, IProcessableElementTag tag, + IElementTagStructureHandler structureHandler, IModel model) { + var factory = context.getModelFactory(); + // regular footer text + var copyRight = factory.createText("
© 2024 Halo
"); + model.add(copyRight); + return Mono.empty(); + } +} +``` + +声明 ExtensionDefinition 自定义模型对象时对应的 extensionPointName 为 template-footer-processor。 + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionDefinition +metadata: + name: custom-footer-extension +spec: + extensionPointName: template-footer-processor + className: com.example.FakeFooterCodeInjection + displayName: "Custom Footer Extension" + description: "Adds custom footer content." + icon: 'some-icon' +``` diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/template-head-processor.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/template-head-processor.md new file mode 100644 index 00000000..1a74b8eb --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/template-head-processor.md @@ -0,0 +1,87 @@ +--- +title: 主题端 HTML Head 标签处理 +description: 提供扩展主题端 HTML 页面中的 Head 标签内容处理的方法,干预 HTML 页面的 Head 标签内容。 +--- + +主题端 HTML Head 标签处理扩展点的作用是干预 HTML 页面中的 Head 标签内容,可以添加自定义的 CSS、JS 及 meta 标签等,以满足特定的定制化需求。 + +## 使用场景 + +- **添加自定义样式或脚本**:在 HTML Head 中插入额外的 CSS 文件或 JavaScript 脚本文件,以增强页面的交互性或样式。 +- **定制 Meta 标签**:为特定页面添加或修改 meta 标签,如描述、作者、关键词等,以提高 SEO 和页面信息的完整性。 +- **引入第三方库**:引入第三方库(如 Google Fonts、Font Awesome 等),以满足页面的特殊功能或风格需求。 +- **定制 Open Graph 等社交媒体标签**:为社交媒体分享优化页面标签内容。 + +## 扩展点定义 + +主题端 HTML Head 标签处理的扩展点定义为 `TemplateHeadProcessor`,对应的 `ExtensionPoint` 类型为 `MULTI_INSTANCE`,即可以有多个实现类。 + +```java +@FunctionalInterface +public interface TemplateHeadProcessor extends ExtensionPoint { + + Mono process(ITemplateContext context, IModel model, + IElementModelStructureHandler structureHandler); +} +``` + +`TemplateHeadProcessor` 对应的 `ExtensionPointDefinition` 资源描述如下: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: template-head-processor +spec: + className: run.halo.app.theme.dialect.TemplateHeadProcessor + displayName: TemplateHeadProcessor + type: MULTI_INSTANCE + description: "Provides a way to extend the head tag content in the theme-side HTML page." +``` + +即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `template-head-processor`。 + +## 示例实现 + +以下是一个简单的 TemplateHeadProcessor 插件实现示例: + +```java +@Component +public class CustomHeadProcessor implements TemplateHeadProcessor { + + @Override + public Mono process(ITemplateContext context, IModel model, + IElementModelStructureHandler structureHandler) { + // 添加自定义 CSS 文件 + model.add(context.createStandaloneElementTag("link", + "rel", "stylesheet", + "href", "/custom/styles.css")); + + // 添加自定义 Meta 标签 + model.add(context.createStandaloneElementTag("meta", + "name", "author", + "content", "Your Name")); + return Mono.empty(); + } +} +``` + +声明 ExtensionDefinition 自定义模型对象时对应的 extensionPointName 为 template-head-processor。 + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionDefinition +metadata: + name: custom-head-extension +spec: + extensionPointName: template-head-processor + className: com.example.CustomHeadProcessor + displayName: "Custom Head Extension" + description: "Adds custom CSS and meta tags to the head section." +``` + +## 使用此扩展点的插件 + +- [集成 highlight.js 为文章提供代码块高亮渲染](https://github.com/halo-sigs/plugin-highlightjs) +- [集成 lightgallery.js,支持在内容页面放大显示图片](https://github.com/halo-sigs/plugin-lightgallery) +- [Halo 2.0 对 Umami 的集成](https://github.com/halo-sigs/plugin-umami) diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/username-password-authentication-manager.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/username-password-authentication-manager.md new file mode 100644 index 00000000..94087043 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension-points/username-password-authentication-manager.md @@ -0,0 +1,30 @@ +--- +title: 用户名密码认证管理器 +description: 提供扩展用户名密码的身份验证的方法 +--- + +用户名密码认证管理器扩展点用于替换 Halo 默认的用户名密码认证管理器实现,例如:使用第三方的身份验证服务,一个例子是 LDAP。 + +```java +public interface UsernamePasswordAuthenticationManager extends ExtensionPoint { + Mono authenticate(Authentication authentication); +} +``` + +`UsernamePasswordAuthenticationManager` 对应的 `ExtensionPointDefinition` 如下: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: username-password-authentication-manager +spec: + className: run.halo.app.security.authentication.login.UsernamePasswordAuthenticationManager + displayName: Username password authentication manager + type: SINGLETON + description: "Provides a way to extend the username password authentication." +``` + +即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `username-password-authentication-manager`。 + +可以参考的实现示例 [TOTP 认证](https://github.com/halo-dev/halo/blob/86e688a15d05c084021b6ba5e75d56a8813c3813/application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpAuthenticationFilter.java#L84) diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension.md new file mode 100644 index 00000000..76309314 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/extension.md @@ -0,0 +1,305 @@ +--- +title: 自定义模型 +description: 了解什么是自定义模型及如何创建 +--- + +Halo 自定义模型主要参考自 [Kubernetes CRD](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/) 。自定义模型遵循 [OpenAPI v3](https://spec.openapis.org/oas/v3.1.0)。设计目的在于提供一种灵活可扩展的数据存储和使用方式,便于为插件提供自定义数据支持。 +比如某插件需要存储自定义数据,同时也想读取和操作自定义数据。更多细节请参考 [自定义模型设计](https://github.com/halo-dev/rfcs/tree/main/extension)。 + +一个典型的自定义模型 `Java` 代码示例如下: + +```java +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import run.halo.app.extension.AbstractExtension; +import run.halo.app.extension.GVK; +import run.halo.app.extension.GroupKind; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@GVK(group = "my-plugin.halo.run", + version = "v1alpha1", + kind = "Person", + plural = "persons", + singular = "person") +public class Person extends AbstractExtension { + + @Schema(requireMode = Schema.RequireMode.REQUIRED) + private Spec spec; + + @Schema(name="PersonSpec") + public static class Spec { + @Schema(description = "The description on name field", maxLength = 100) + private String name; + + @Schema(description = "The description on age field", maximum = "150", minimum = "0") + private Integer age; + + @Schema(description = "The description on gender field") + private Gender gender; + + private Person otherPerson; + } + + public enum Gender { + MALE, FEMALE, + } +} +``` + +要创建一个自定义模型需要三步: + +1. 创建一个类继承 `run.halo.app.extension.AbstractExtension`。 +2. 使用 `GVK` 注解。 +3. 在插件 `start()` 生命周期方法中注册自定义模型: + +```java +@Autowired +private SchemeManager schemeManager; + +@Override +public void start() { + schemeManager.register(Person.class); +} +``` + +:::tip 释义 + +- @GVK:此注解标识该类为一个自定义模型,同时必须继承 `AbstractExtension`。 + - kind:表示自定义模型所表示的 REST 资源。 + - group:表示一组公开的资源,通常采用域名形式,Halo 项目保留使用空组和任何以 `*.halo.run` 结尾的组名供其单独使用。 + 选择群组名称时,我们建议选择你的群组或组织拥有的子域,例如 `widget.mycompany.com`,而这里提到的公开并不是指你的自定义资源可以被任何人访问, + 而是指你的自定义模型对象可以被以 APIs 的形式访问。 + - version:API 的版本,它与 group 组合使用为 `apiVersion=GROUP/VERSION`,例如`api.halo.run/v1alpha1`。 + - singular: 资源的单数名称,这允许客户端不透明地处理复数和单数,必须全部小写,通常是将 `kind` 的值转换为小写作为 `singular` 的值。 + - plural: 资源的复数名称,自定义资源在 `/apis///.../` 下提供,必须为全部小写,通常是将 `kind` 的值转换为小写并转为复数形式作为 `plural` 的值。 +- @Schema:属性校验注解,会在创建/修改资源前对资源进行简单校验,参考 [schema-validator](https://www.openapi4j.org/schema-validator.html)。 +::: + +一个自定义模型通常包括以下几个部分: + +- `apiVersion`: 用于标识自定义模型的 API 版本,它由 `GVK` 注解的 `group` 和 `version` 组合而成。 +- `kind`: 用于标识自定义模型的类型,它由 `GVK` 注解的 `kind` 声明。 +- `metadata`: 用于标识自定义模型的元数据: + - `name`: 用于标识自定义模型的名称。 + - `creationTimestamp`: 用于标识自定义模型的创建时间,无法修改,只在创建时自动生成。 + - `version`: 用于标识自定义模型的数据乐观锁版本,无法修改,由更新时自动填充,如果更新时指定了 `version` 且与当前 `version` 不一致则会更新失败。 + - `deletionTimestamp`: 用于标识自定义模型的删除时间,表示此自定义模型对象被声明为删除,此时仍然可以通过 API 访问到此对象,参考 [自定义模型对象生命周期](../../basics/framework.md#extension-lifecycle) + - `finalizers`: 用于标识终结器,它是一个字符串集合,用于标识自定义模型对象是否可回收,参考 [自定义模型对象生命周期](../../basics/framework.md#extension-lifecycle) + - `labels`: 用于标识自定义模型的标签,它是一个字符串键值对集合,用于标识自定义模型对象的标签,可以通过标签来查询自定义模型对象。 + - `annotations`: 用于存放扩展信息,它是一个字符串键值对集合,用于存放自定义模型对象的扩展信息。 +- `spec`: 用于声明自定义模型对象的期望状态,它是声明式的,用户只需要声明期望状态,实际状态由具体的控制器来维护,最终达到用户期望的状态。 +- `status`: 用于描述自定义模型对象资源状态的变化,和一些实际状态。 + +其中 `apiVersion`、`kind`、`metadata`都包含在了 AbstractExtension 类中,所以我们只需要关注 `spec` 和 `status` 即可,参考:[Halo 架构概览之自定义模型](../../basics/framework.md#extension) + +## 声明自定义模型对象 {#declare-extension-object} + +有了自定义模型后可以通过在插件项目的 `src/main/resources/extensions` 目录下声明 `yaml` 文件来创建一个自定义模型对象, +此目录下的所有自定义模型 `yaml` 都会在插件启动后被创建: + +```yaml +apiVersion: my-plugin.halo.run/v1alpha1 +kind: Person +metadata: + name: fake-person +spec: + name: halo + age: 18 + gender: male +``` + +在该目录下声明自定义模型对象所使用的 `yaml` 文件的文件名是任意的,只根据 `kind` 和 `apiVersion` 来确定自定义模型对象的类型。 + +## 命名规范 {#naming-conventions} + +### metadata name {#metadata-name} + +`metadata.name` 它是自定义模型对象的唯一标识名,包含不超过 253 个字符,仅包含小写字母、数字或`-`,以字母或数字开头,以字母或数字结尾。 + +### labels + +`labels` 它是一个字符串键值对集合, Key 的基本结构为 `/`,完整的 label 键通常包括一个可选的前缀和名称,二者通过斜杠(/)分隔。 + +- 前缀(可选):通常是域名的反向表示形式,用于避免键名冲突。例如,halo.run/post-slug +- 名称:标识 label 的具体含义,如 post-slug。 + +前缀规则: + +- 如果 label 用于特定于一个组织的资源,建议使用一个前缀,如 `plugin.halo.run/plugin-name`。 +- 前缀必须是一个有效的 DNS 子域名(参考 metadata.name),且最多可包含 253 个字符。 +- 保留了不带前缀的 label 键以及特定前缀(如 halo.run),因此插件不可使用。 + +名称规则: + +- 名称必须是合法的 DNS 标签,最多可包含 63 个字符。 +- 必须以字母数字字符开头和结尾。 +- 可以包含 `-`、`.`、`_` 和`字母数字`字符。 + +通用规范: + +- 避免使用容易引起混淆或误解的键名。 +- 尽量保持简洁明了,易于理解。 +- 使用易于记忆和识别的单词或缩写。 + +一致性和清晰性: + +- 在整个项目或组织中保持一致的命名约定。 +- labels 应直观地反映其代表的信息或用途。 +- 不要在 labels 中包含敏感信息,例如用户凭据或个人识别信息。 + +## 使用索引 {#using-indexes} + +自定义模型虽然带来了很大的灵活性可扩展性,但也引入了查询问题,自定义模型对象存储在数据库中是 `byte[]` 的形式存在的,从而实现不依赖于数据库特性,你可以使用 `MySQL`,`PostgreSQL`,`H2` 等数据库来来作为存储介质,但查询自定义模型对象时无法使用数据库的索引特性,这就导致了查询自定义模型对象的效率问题,Halo 自己实现了一套索引机制来解决这个问题。 + +索引是一种存储数据结构,可提供对数据集中字段的高效查找。索引将自定义模型中的字段映射到数据库行,以便在查询特定字段时不需要完整的扫描。查询数据之前,必须对需要查询的字段创建索引。索引可以包含一个或多个字段的值。索引可以包含唯一值或重复值。索引中的值按照索引中的顺序进行排序。 + +索引可以提高查询性能,但会占用额外的存储空间,因为它们需要存储索引字段的副本。索引的大小取决于字段的数据类型和索引的类型,因此,创建索引时应该考虑存储成本和性能收益。 + +你可以通过以下方式在注册自定义模型时声明索引: + +```java +@Override +public void start() { + schemeManager.register(Moment.class, indexSpecs -> { + indexSpecs.add(new IndexSpec() + .setName("spec.tags") + .setIndexFunc(multiValueAttribute(Moment.class, moment -> { + var tags = moment.getSpec().getTags(); + return tags == null ? Set.of() : tags; + })) + ); + // more index spec +} +``` + +`IndexSpec` 用于声明索引项,它包含以下属性: + +- name:索引名称,在同一个自定义模型的索引中必须唯一,一般建议使用字段路径作为索引名称,例如 `spec.slug`。 +- order:对索引值的排序方式,支持 `ASC` 和 `DESC`,默认为 `ASC`。 +- unique:是否唯一索引,如果为 `true` 则索引值必须唯一,如果创建自定义模型对象时检测到此索引字段有重复值则会创建失败。 +- indexFunc:索引函数,用于获取索引值,接收当前自定义模型对象,返回一个索引值,索引值必须是字符串任意类型,如果不是字符串类型则需要自己转为字符串,可以使用 `IndexAttributeFactory` 提供的静态方法来创建 `indexFunc`: + - `simpleAttribute()`:用于得到一个返回单个值的索引函数,例如 `moment -> moment.getSpec().getSlug()`。 + - `multiValueAttribute()`:用于得到一个返回多个值的索引函数,例如 `moment -> moment.getSpec().getTags()`。 + +当注册自定义模型时声明了索引后,Halo 会在插件启动时构建索引,在构建索引期间插件出于未启动状态。 + +Halo 默认会为每个自定义模型建立以下几个索引,因此不需要为下列字段再次声明索引: + +- `metadata.name` 创建唯一索引 +- `metadata.labels` +- `metadata.creationTimestamp` +- `metadata.deletionTimestamp` + +创建了索引的字段可以在查询时使用 `fieldSelector` 参数来查询,参考 [自定义模型 APIs](#extension-apis)。 + +## 自定义模型 APIs {#extension-apis} + +定义好自定义模型并注册后,会根据 `GVK` 注解自动生成一组 `CRUD` APIs,规则为: +`/apis////{extensionname}/` + +对于上述 Person 自定义模型将有以下 APIs: + +```shell +# 用于列出所有 Person 自定义模型对象 +GET /apis/my-plugin.halo.run/v1alpha1/persons + +# 用于查询指定名称更新自定义模型对象 +PUT /apis/my-plugin.halo.run/v1alpha1/persons/{name} + +# 用于创建自定义模型对象 +POST /apis/my-plugin.halo.run/v1alpha1/persons + +# 用于根据指定名称删除自定义模型对象 +DELETE /apis/my-plugin.halo.run/v1alpha1/persons/{name} +``` + +对于这组自动生成的 `CRUD` APIs,你可以通过定义[控制器](../../basics/framework.md#controller)来完成对数据修改后的业务逻辑处理来满足大部分的场景需求。 + +`GET /apis/my-plugin.halo.run/v1alpha1/persons` 这个 API 用于查询自定义模型对象,它支持以下参数: + +- page:页码,从 1 开始。 +- size:每页数据量,如果不传此参数默认为查询所有。 +- sort:排序字段,格式为 `字段名,排序方式`,例如 `name,desc`,如果不传此参数默认为按照 `metadata.creationTimestamp` 降序排序,排序使用的字段必须是注册为索引的字段。 +- labelSelector:标签选择器,格式为 `key=value`,例如 `labelSelector=name=halo`,如果不传此参数默认为查询所有,此标签选择器筛选的是 `metadata.labels`,支持的操作符有 `=`、 `!=`、`!` 和 `存在检查`: + - `=` 表示等于,例如 `labelSelector=name=halo` 表示查询 `metadata.labels` 中 `name` 的值等于 `halo` 的自定义模型对象。 + - `!=` 表示不等于,例如 `labelSelector=name!=halo` 表示查询 `metadata.labels` 中 `name` 的值不等于 `halo`的自定义模型对象。 + - `!` 表示不存在 key,例如 `labelSelector=!name` 表示查询 `metadata.labels` 不存在 `name` 这个 key 的自定义模型对象。 + - `存在检查` 表示查询存在 key 的对象,例如 `labelSelector=name` 表示查询 `metadata.labels` 存在 `name` 这个 key 的自定义模型对象。 +- fieldSelector:字段选择器,格式与 `labelSelector` 类似,但需要确保对应的字段是注册为索引的字段,例如 `fieldSelector=spec.name=slug` 表示查询 `spec.slug` 的值等于 `halo` 的自定义模型对象,支持的操作符有 `=`、`!=` 和 `in`。 + - `=` 表示等于,例如 `fieldSelector=spec.slug=halo` 表示查询 `spec.slug` 的值等于 `halo` 的自定义模型对象。 + - `!=` 表示不等于,例如 `fieldSelector=spec.slug!=halo` 表示查询 `spec.slug` 的值不等于 `halo` 的自定义模型对象。 + - `in` 表示在集合中,例如 `fieldSelector=spec.slug=(halo,halo2)` 表示查询 `spec.slug` 的值在 `halo` 和 `halo2` 中的自定义模型对象。 + +这些查询参数是 `AND` 的关系,例如 `page=1&size=10&sort=name,desc&labelSelector=name=halo&fieldSelector=spec.slug=halo` 表示查询 `metadata.labels` 中 `name` 的值等于 `halo` 且 `spec.slug` 的值等于 `halo` 的自定义模型对象,并按照 `name` 字段降序排序,查询第 1 页,每页 10 条数据。 + +## 自定义 API {#custom-extension-apis} + +在一些场景下,只有自动生成的 `CRUD` API 往往是不够用的,此时可以通过自定义一些 APIs 来满足功能。 + +你可以使用 [Spring Framework Web](https://docs.spring.io/spring-framework/reference/web/webflux/new-framework.html) 的 Adaptive 写法来暴露 APIs,不同的是需要添加 `@ApiVersion` 注解,没有此注解的 `Controller` 将被忽略: + +```java +@ApiVersion("fake.halo.run/v1alpha1") +@RequestMapping("/apples") +@RestController +public class AppleController { + + @PostMapping("/starting") + public void starting() { + } +} +``` + +当插件被启动时,Halo 将会为此 AppleController 生成一个 API: + +```text +/apis/fake.halo.run/v1alpha1/apples/starting +``` + +但我们**更推荐使用** [Functional Endpoints](https://docs.spring.io/spring-framework/reference/web/webflux-functional.html) 写法来提供 APIs,这是一种轻量级函数式编程写法: + +```java +RouterFunction route = route() + .GET("/person/{id}", accept(APPLICATION_JSON), this::getPerson) + .GET("/person", accept(APPLICATION_JSON), this::listPeople) + .POST("/person", this::createPerson) + .add(otherRoute) + .build(); + +public Mono listPeople(ServerRequest request) { + // ... +} + +public Mono createPerson(ServerRequest request) { + // ... +} + +public Mono getPerson(ServerRequest request) { + // ... +} +``` + +HTTP 请求通过 HandlerFunction 处理:这是一个接收 ServerRequest 并返回延迟的 ServerResponse(即 `Mono`)的函数。 +请求和响应对象都有不可变的约定,它们提供了对 HTTP 请求和响应的 JDK 8 友好访问。HandlerFunction 相当于基于注解的编程模型中 @RequestMapping 方法的主体。 + +传入的请求通过 RouterFunction 路由到一个处理函数:这是一个接收 ServerRequest 并返回延迟的 HandlerFunction(即 `Mono`)的函数。 +当路由函数匹配时,返回一个处理函数;否则返回一个空的 Mono。RouterFunction 相当于 `@RequestMapping` 注解,但主要区别在于路由函数不仅提供数据,还提供行为。 + +ServerRequest 和 ServerResponse 是不可变的接口,它们提供了对 HTTP 请求和响应的 JDK 8 友好访问。请求和响应都针对主体流提供了 +[Reactive Streams](https://www.reactive-streams.org/) 的背压(back pressure)。请求主体用 Reactor Flux 或 Mono 表示。 +响应主体可用任何响应式流发布者(Publisher)表示,包括 Flux 和 Mono。 +更多相关信息,请参见 [Reactor 3 Reference Guide](https://projectreactor.io/docs/core/release/reference/) 和 [Webflux](https://docs.spring.io/spring-framework/reference/web/webflux.html)。 + +### 自定义 API 的路由规则 {#route-rules-for-custom-apis} + +自定义模型对象的自定义 APIs 的路由规则建议与自动生成的 CRUD APIs 的路由规则保持一致,这样可以方便用户理解和使用,因此对于自动生成的 CRUD APIs 的路由规则建议遵循以下规则: + +1. 以 `/apis///[//]` 规则组成 APIs。 +2. 为了与自动生成的 CRUD APIs 区分开避免冲突,我们选择通过不同的 group 来区分,自定义 APIs 的 group 建议遵循以下规则,以便保证唯一性的同时能与自定义模型产生关联: + + - 在 Console 端使用的自定义 APIs 的 group 规则建议为 `console.api.`,例如对于 Person 自定义模型需要额外的在 Console 端使用的自定义 APIs 那么 group 可以定义为 `console.api.my-plugin.halo.run`,则可能的用于查询 Person 列表的 API 的规则应为 `/apis/console.api.my-plugin.halo.run/v1alpha1/persons`。 + - 在个人中心使用的自定义 APIs 的 group 规则建议为 `uc.api.`,例如 `uc.api.my-plugin.halo.run`。 + - 为主题端提供的公开的自定义 APIs 的 group 规则建议为 `api.`,例如 `api.my-plugin.halo.run`。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/finder-for-theme.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/finder-for-theme.md new file mode 100644 index 00000000..60fb81f4 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/finder-for-theme.md @@ -0,0 +1,60 @@ +--- +title: 为主题提供数据 +description: 了解如何为主题提供更多获取和使用数据的方法。 +--- + +当你在插件中创建了自己的自定义模型时,你可能需要在主题模板中使用这些数据。或者,你提供一些额外的数据,以便主题可以使用它们,你可以通过创建一个自定义的 `finder` 来实现这一点。 + +## 创建一个 Finder + +首先,你需要创建一个 `interface`,并在其中定义你需要提供给主题获取数据的方法,方法的返回值可以是 `Mono` 或 `Flux` 类型,例如: + +```java +public interface LinkFinder { + Mono get(String linkName); + + Flux listAll(); +} +``` + +然后写一个实现类,实现这个 `interface`,并在类上添加 `@Finder` 注解,例如: + +```java +import run.halo.app.theme.finders.Finder; + +@Finder("myPluginLinkFinder") +public class LinkFinderImpl implements LinkFinder { + @Override + public Mono get(String linkName) { + // ... + } + + @Override + public Flux listAll() { + // ... + } +} +``` + +`@Finder` 注解的值是你在主题中使用的名称,例如,你可以在主题中使用 `myPluginLinkFinder.get('a-link-name')` 来获取数据,`myPluginLinkFinder` 就是你在 `@Finder` 注解中定义的名称。 + +## Finder 命名 + +为了避免与其他插件的 `finder` 名称冲突,建议在 `@Finder` 注解中添加一个你插件名称的前缀作为 `finder` 名称且名称需要是驼峰式的,不能包含除了 `_` 之外的其他特殊字符。 + +例如,你的插件名称是 `my_plugin`,你需要为主题提供一个获取链接的 `finder`,那么你可以这样定义 `@Finder` 注解: + +```java +@Finder("myPluginLinkFinder") +``` + +## 使用 Finder + +在主题中,你可以通过 `finder` 名称和方法名及对应的参数来获取数据,例如: + +```html +
+
+``` + +模板语法参考:[Thymeleaf](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#standard-expression-syntax)。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/login-handler-enhancer.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/login-handler-enhancer.md new file mode 100644 index 00000000..40e5949c --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/login-handler-enhancer.md @@ -0,0 +1,63 @@ +--- +title: 登录增强 +description: 了解如何在登录时如何允许 Halo 做登录逻辑的增强切入。 +--- + +## 背景 + +在 Halo 中,插件可以实现多种登录方式,例如LDAP、第三方登录等。然而,灵活的登录方式也带来了以下问题: + +1. 登录逻辑难以统一:例如登录成功后需要进行额外处理,这需要插件自行实现。 +2. Halo 或其他插件无法知晓登录状态:无法记录登录日志等额外处理。 +3. 新增安全特性适配:Halo 增加了新安全特性,插件需要适配才能使用,如在记住我机制中需要在登录成功后设置 remember-me cookie。 + +为了解决这些问题,Halo 提供了登录增强机制,插件可以在登录成功或失败时调用登录增强器,使 Halo 可以执行额外的处理逻辑。随着 Halo 的版本更新,这些逻辑也会更新,而插件无需做任何修改。 + +### 登录增强器 + +Halo 提供了一个 LoginHandlerEnhancer 的 Bean,插件可以通过依赖注入的方式在合适的位置调用该 Bean 的方法,以便 Halo 可以在登录成功或失败后执行逻辑切入。 + +```java +public interface LoginHandlerEnhancer { + + /** + * Invoked when login success. + * + * @param exchange The exchange. + * @param successfulAuthentication The successful authentication. + */ + Mono onLoginSuccess(ServerWebExchange exchange, Authentication successfulAuthentication); + + /** + * Invoked when login fails. + * + * @param exchange The exchange. + * @param exception the reason authentication failed + */ + Mono onLoginFailure(ServerWebExchange exchange, AuthenticationException exception); +} +``` + +例如在用户密码登录的处理器中,可以这样调用登录增强器: + +```java +public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandler, + ServerAuthenticationFailureHandler { + @Override + public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, + AuthenticationException exception) { + var exchange = webFilterExchange.getExchange(); + return loginHandlerEnhancer.onLoginFailure(exchange, exception) + .then(handleFailure(exchange, exception)); + } + + @Override + public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, + Authentication authentication) { + return loginHandlerEnhancer.onLoginSuccess(webFilterExchange.getExchange(), authentication) + .then(handleSuccess(webFilterExchange.getExchange(), authentication); + } +} +``` + +设备管理、记住我等机制都依赖于登录增强器。插件开发者可以通过在合适的时机调用登录增强器来实现这些功能,确保插件与 Halo 的安全特性无缝集成。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/reconciler.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/reconciler.md new file mode 100644 index 00000000..26ac36ed --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/reconciler.md @@ -0,0 +1,365 @@ +--- +title: 编写控制器 +description: 了解如何为自定义模型编写控制器 +--- + +控制器是 Halo 的关键组件,它们负责对每个自定义模型对象进行操作,协调所需状态和当前状态,参考: [控制器概述](../../basics/framework.md#controller)。 + +控制器通常在具有一般事件序列的控制循环中运行: + +1. 观察:每个控制器将被设计为观察一组自定义模型对象,例如文章的控制器会观察文章对象,插件的控制器会观察插件自定义模型对象等。 +2. 比较:控制器将对象配置的期望状态与其当前状态进行比较,以确定是否需要更改,例如插件的 `spec.enabled` 为 `true`,而插件的当前状态是未启动,则插件控制器会处理启动插件的逻辑。 +3. 操作:控制器将根据比较的结果执行相应的操作,以确保对象的实际状态与其期望状态一致,例如插件期望启动,插件控制器会处理启动插件的逻辑。 +3. 重复:上述所有步骤都由控制器重复执行直到与期望状态一致。 + +这是一个描述控制器作用的例子:房间里的温度自动调节器。 + +当你设置了温度,告诉了温度自动调节器你的期望状态(Desired State)。 +房间的实际温度是当前状态(Current State)。 通过对设备的开关控制,温度自动调节器让其当前状态接近期望状态,未到达期望状态则继续调节,直到达到期望状态。 + +在 Halo 中控制器的运行部分已经有一个默认实现,你只需要编写控制器的调谐的逻辑也就是 [控制器概述](../../basics/framework.md#controller) 中的所说的 Reconciler 即可。 + +## 编写 Reconciler + +Reconciler 是控制器的核心,它是一个接口,你需要实现它的 `reconcile()` 方法,该方法接收一个 `Reconciler.Request` 对象,它包含了当前自定义模型对象的名称,你可以通过它来获取自定义模型对象的当前状态和期望状态,然后编写调谐的逻辑。 + +```java +@Component +public class PostReconciler implements Reconciler { + @Override + public Result reconcile(Request request) { + + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + return builder + .extension(new Post()) + .build(); + } +} +``` + +以上是一个简单的 Reconciler 实现,它实现了 `reconcile()` 方法,然后在 `setupWith()` 方法中将其通过 `ControllerBuilder` 构建为一个控制器并指定了 +它要观察的自定义模型对象为`Post`,当文章自定义模型对象发生变化时,`reconcile()` 方法就会被调用,从 `Request request` 参数中你可以获得当前发生变化的文章自定义模型对象的名称,然后你就可以通过名称来查询到自定义模型对象进行调谐了。 + +### 构建控制器 + +`setupWith()` 方法用于根据当前类的 `reconcile` 方法构建控制器,你可以通过 `ControllerBuilder` 提供的方法来构建并定制控制器: + +```java +public class ControllerBuilder { + private final String name; + private Duration minDelay; + private Duration maxDelay; + private final Reconciler reconciler; + private Supplier nowSupplier; + private Extension extension; + private ExtensionMatcher onAddMatcher; + private ExtensionMatcher onDeleteMatcher; + private ExtensionMatcher onUpdateMatcher; + private ListOptions syncAllListOptions; + private boolean syncAllOnStart = true; + private int workerCount = 1; +} +``` + +- `name`:控制器的名称,用于标识控制器。 +- `minDelay`:控制器的最小延迟,用于控制控制器的最小调谐间隔,默认为 5 毫秒。 +- `maxDelay`:控制器的最大延迟,用于控制控制器的最大调谐间隔,默认为 1000 秒。 +- `reconciler`:控制器的调谐器,用于执行调谐逻辑,你需要实现 `Reconciler` 接口。 +- `nowSupplier`:用于获取当前时间的供应商,用于控制器的时间戳,默认使用 `Instant.now()` 获取当前时间。 +- `extension`:控制器要观察的自定义模型对象。 +- `onAddMatcher`:用于匹配添加事件的匹配器,当自定义模型对象被创建时会触发。 +- `onDeleteMatcher`:用于匹配删除事件的匹配器,当自定义模型对象被删除时会触发。 +- `onUpdateMatcher`:用于匹配更新事件的匹配器,当自定义模型对象被更新时会触发。 +- `syncAllListOptions`:用于同步所有自定义模型对象的查询条件,仅当 `syncAllOnStart` 为 `true` 时生效。 +- `syncAllOnStart`:是否在控制器启动时同步所有自定义模型对象,默认为 `true`,可以配合 `syncAllListOptions` 使用以缩小需要同步的对象范围避免不必要的同步,例如只同步某个用户创建的文章或者某个固定名称的 ConfigMap 对象。如果你的控制器不需要同步所有对象,可以将其设置为 `false`。 +- `workerCount`:控制器的工作线程数,用于控制控制器的并发度,如果你的控制器需要处理大量的对象,可以将其设置为大于 1 的值,以提高控制器的处理能力,但需要注意的是并发度越高,系统的负载也会越高。这里的并发度是指控制器的并发度,但是每个控制器还是单线程执行的。 + +#### ExtensionMatcher + +`onAddMatcher/onUpdateMatcher/onDeleteMatcher` 都是 `ExtensionMatcher` 类型,用于决定当自定义模型对象发生变化时是否触发控制器: + +```java +public interface ExtensionMatcher { + boolean match(Extension extension); +} +``` + +这里`match` 方法的 `Extension` 参数类型与 `ControllerBuilder` 中的 `extension` 类型始终是一致的,因此可以直接通过强制类型转换来得到需要的类型。 + +比如我们想要观察文章对象,但是只想观察文章对象中 `visible` 字段为 `PUBLIC` 的文章,可以这样 + +```java +public class PostReconciler implements Reconciler { + @Override + public Result reconcile(Request request) { + return Result.doNotRetry(); + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + // 只想观察 VisibleEnum.PUBLIC 的文章 + ExtensionMatcher extensionMatcher = extension -> { + var post = (Post) extension; + return VisibleEnum.PUBLIC.equals(post.getSpec().getVisible()); + }; + return builder + .extension(new Post()) + .onAddMatcher(extensionMatcher) + .onUpdateMatcher(extensionMatcher) + .onDeleteMatcher(extensionMatcher) + .build(); + } +} +``` + +#### 控制启动时同步的范围 + +如果想要在控制器启动时控制同步对象的范围,可以通过 `syncAllListOptions` 和 `syncAllOnStart` 来实现,例如只同步某个用户创建的文章: + +```java +public class PostReconciler implements Reconciler { + @Override + public Result reconcile(Request request) { + return Result.doNotRetry(); + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + return builder + .extension(new Post()) + .syncAllListOptions(ListOptions.builder() + .fieldQuery(QueryFactory.equal("spec.owner", "guqing")) + .build() + ) + .syncAllOnStart(true) + .build(); + } +} +``` + +### Reconciler 的返回值 + +`reconcile()` 方法的返回值是一个 `Result` 对象,它包含了调谐的结果,你可以通过它来告诉控制器是否需要重试,如果需要重试则控制器会在稍后再次调用 `reconcile()` 方法,而这个过程会一直重复,直到 `reconcile()` 方法返回成功为止,这个过程被称之为调谐循环(Reconciliation Loop)。 + +```java +record Result(boolean reEnqueue, Duration retryAfter) {} +``` + +`Result` 对象包含了两个属性:reEnqueue 和 retryAfter,reEnqueue 用于标识是否需要重试,retryAfter 用于标识重试的时间间隔,如果 reEnqueue 为 true 则会在 retryAfter 指定的时间间隔后再次调用 `reconcile()` 方法,如果 reEnqueue 为 false 则不会再次调用 `reconcile()` 方法。 + +在没有特殊需要时,`retryAfter` 可以不指定,控制器会有一套默认的重试策略。 + +如果直接返回 `null` 则会被视为成功,效果等同于返回 `new Result(false, null)`。 + +### Reconciler 的异常处理 + +当 `reconcile()` 方法抛出异常时,控制器会将异常记录到日志中,然后会将 `Request request` 对象重新放入队列中,等待下次调用 `reconcile()` 方法,这个过程会一直重复,直到 `reconcile()` 成功,对于默认重试策略,每次重试间隔会越来越长,直到达到最长间隔后不再增加。 + +## 控制器示例 + +本章节将通过一个简单的示例来演示如何编写控制器。 + +### 场景:事件管理系统 + +创建一个名为 ”EventTracker“ 的自定义模型,用于管理和追踪组织内的各种事件。这些事件可以是会议、研讨会、社交聚会或任何其他类型的组织活动。 +“EventTracker“ 自定义模型将提供一个框架,用于记录事件的详细信息,如时间、地点、参与者和状态。 + +由于这里的重点是控制器,因此我们将忽略自定义模型的详细信息,只关注控制器的实现,一个可能的 “EventTracker” 数据结构如下: + +```yaml +apiVersion: tracker.halo.run/v1alpha1 +kind: EventTracker +metadata: + name: event-tracker-1 +spec: + eventName: "Halo Meetup" + eventDate: "2024-01-20T12:00:00Z" + location: "Chengdu" + participants: ["@sig-doc", "@sig-console", "@sig-halo"] + description: "Halo Meetup in Chengdu" +status: + phase: "Planned" # Planned, Ongoing, Completed + participants: [] + conditions: + - type: "Invalid" + status: "True" + reason: "InvalidEventDate" + message: "Event date is invalid" +``` + +业务逻辑处理: + +1. 事件创建: + + - 当新的 EventTracker 资源被创建时,控制器需验证所有必要字段的存在和格式正确性。 + - 初始化事件状态为 Planned。 + +2. 事件更新: + + - 检查 eventDate、location 和 participants 字段的变更。 + - 如果接近事件日期,自动更新状态为 Ongoing。 + +3. 状态管理: + + - 根据当前日期和事件日期自动管理 phase 字段。 + - 当事件日期过去时,将状态更新为 Completed。 +4. 数据验证和完整性: + - 确保所有输入数据的格式正确且合理。 + - 如有不一致或缺失的重要信息,记录警告或错误。 +5. 事件提醒和通知: + - 在事件状态改变或临近事件日期时发送通知。 +6. 清理和维护: + - 对于已完成的事件,提供自动清理机制,例如在事件结束后一定时间内删除资源。 + +首先实现 EventTracker 控制器的协调循环主体,通过依赖注入 `ExtensionClient` 可以用于获取当前变更的对象: + +```java +@Component +@RequiredArgsConstructor +public class EventTrackerReconciler implements Reconciler { + + private final ExtensionClient client; + + @Override + public Result reconcile(Request request) { + // ... + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + return builder + .extension(new EventTracker()) + .build(); + } +} +``` + +然后在 `reconcile()` 方法中根据 `EventTracker` 对象的状态来执行响应的操作,确保执行逻辑是是幂等的,这意味着即使多次执行相同操作,结果也应该是一致的。 + +```java +public Result reconcile(Request request) { + client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> { + // 获取到当前变更的 EventTracker 对象 + // 1. 检查必要字段的存在和格式正确性 + // 2. 初始化事件状态为 Planned + if (eventTracker.getStatus() == null) { + eventTracker.setStatus(new EventTracker.Status()); + } + var status = eventTracker.getStatus(); + if (status.getPhase() == null) { + status.setPhase(EventTracker.Phase.PLANNED); + } + + var eventName = eventTracker.getSpec().getEventName(); + if (StringUtils.isBlank(eventName)) { + Condition condition = Condition.builder() + .type("Invalid") + .reason("InvalidEventName") + .message("Event name is invalid") + .status(ConditionStatus.FALSE) + .lastTransitionTime(Instant.now()) + .build(); + status.getConditions().addAndEvictFIFO(condition); + } + + client.update(eventTracker); + }); + return new Result(false, null); +} +``` + +上述,我们通过 `client.fetch()` 方法获取到了当前变更的 `EventTracker` 对象,然后根据 `EventTracker` 对象的状态来执行响应的操作,例如初始化事件状态为 Planned,检查必要字段的存在和格式正确性等,但需要注意控制器的执行是异步的,如果我们通过 `EventTracker` 的 API 来创建或更改了一个 `EventTracker` 对象,那么 API 会在控制器执行之前返回结果,这意味着在用户界面看到的结果可能不是最新的,并且可能会在稍后更新。 + +对于上述校验 `eventName` 的逻辑只是保证后续的执行是可靠的,如果有些字段是必须的,那么我们可以通过 `@Schema` 注解来标注,为了让控制器中校验字段失败的信息能够呈现到用户界面,我们通过向 `status.conditions` 中添加了一条 Condition 记录来用于记录这个事件,再用户界面可以展示这个 Condition 记录的信息以让用户知晓。 + +最后,我们通过 `client.update()` 方法来更新 `EventTracker` 对象,这个过程就是将实际状态回写到 `EventTracker` 对象并应用到数据库中,这样就完成了一次调谐。 + +当 `EventTracker` 对象发生变更时,控制器也会被执行,这时我们可以根据 `EventTracker` 对象的状态来执行响应的操作,例如检查和更新 `eventDate`、`location` 和 `participants` 字段的变更,如果接近事件日期,自动更新状态为 Ongoing。 + +```java +public Result reconcile(Request request) { + client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> { + // ...此处省略之前的逻辑 + if (isApproach(eventTracker.getSpec().getEventDate())) { + status.setPhase(EventTracker.Phase.ONGOING); + sendNotification(eventTracker, "Event is ongoing"); + } + }); + return new Result(false, null); +} +``` + +这里我们通过 `isApproach()` 方法来表示判断是否接近事件日期,如果接近则更新状态为 Ongoing,并使用 `sendNotification` 来发送发送通知。 + +> 为了简化示例,我们省略了 `isApproach()` 和 `sendNotification` 方法的实现。 + +还可以根据 `spec.participants` 字段来解析参与者信息,然后将其添加到 `status.participants` 中,这样就可以在用户界面看到参与者信息了。 + +```java +public Result reconcile(Request request) { + client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> { + // ...此处省略之前的逻辑 + var participants = eventTracker.getSpec().getParticipants(); + resolveParticipants(participants).forEach(status::addParticipant); + }); + return new Result(false, null); +} +``` + +### 使用 Finalizers + +`Finalizers` 允许控制器实现异步预删除钩子。假设您为正在实现的 API 类型的每个对象创建了一个外部资源,例如存储桶,并且您希望在从 Halo 中删除相应对象 +时清理外部资源,您可以使用终结器来删除外部资源资源。 + +比如 `EventTracker` 对象被删除时,我们需要删除 `EventTracker` 对象记录的日志,这时我们可以通过 `Finalizers` 来实现。 + +首先我们需要在 `reconcile()` 的开头判断 `EventTracker` 对象的 `metadata.deletionTimestamp` 是否存在,如果存在则表示 `EventTracker` 对象被删除了, +这时我们就可以执行清理操作。 + +```java +public Result reconcile(Request request) { + client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> { + if (ExtensionOperator.isDeleted(eventTracker)) { // 1. 判断是否被删除 + // 2. 调用 removeFinalizers 方法移除终结器(稍后会说明) + ExtensionUtil.removeFinalizers(eventTracker.getMetadata(), Set.of(FINALIZER_NAME)); + // 3. 执行清理操作 + cleanUpLogsForTracker(eventTracker); + // 4. 更新 EventTracker 对象将变更应用到数据库中 + client.update(eventTracker); + // 5. return 避免执行后续逻辑 + return; + } + // ...此处省略之前的逻辑 + }); + return new Result(false, null); +} +``` + +1. `ExtensionOperator.isDeleted` 方法是 Halo 提供的工具方法,用于判断对象是否被删除,它会判断 `metadata.deletionTimestamp` 是否存在,如果存在则表示对象被标记删除了。 +关于自定义模型对象的删除可以参考:[自定义模型对象生命周期](../../basics/framework.md#extension-lifecycle) +2. `ExtensionUtil.removeFinalizers` 方法是 Halo 提供的工具方法,用于移除对象的终结器,它接收两个参数,第一个参数是对象的元数据,第二个参数是要移除的终结器名称集合,它来自 `run.halo.app.extension.ExtensionUtil`。 +3. `cleanUpLogsForTracker` 方法是我们自己实现的,这里的示例是用于清理 `EventTracker` 对象记录的日志,你可以根据自己的业务需求来实现,如清理外部资源等。 + +经过上述步骤,我们只是写了移除终结器但是发现没有添加终结器的逻辑,添加终结器的逻辑需要在判断删除之后,`metadata.finalizers` 是一个字符串集合,用于标识对象是否可回收,如果 `metadata.finalizers` 不为空则表示对象不可回收,否则表示对象可回收,我们可以通过 `ExtensionUtil.addFinalizers` 方法来添加终结器。 + +最佳实践是,一个控制器最多添加一个终结器,名称为了防止冲突可以使用当前业务的 `group/终结器名称` 来命名,例如 `tracker.halo.run/finalizer`,例如在 Halo 中文章的控制器使用了一个终结器,但可能插件也会定义一个文章控制器来扩展文章的业务,那么根据最佳实践命名终结器可以避免冲突。 + +```java +private static final String FINALIZER_NAME = "tracker.halo.run/finalizer"; + +public Result reconcile(Request request) { + client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> { + if (ExtensionOperator.isDeleted(eventTracker)) { +// ... 省略删除逻辑 + } + // 添加终结器 + ExtensionUtil.addFinalizers(post.getMetadata(), Set.of(FINALIZER_NAME)); + // ...此处省略之前的逻辑 + // 会在更新时将终结器的变更写入到数据库中 + client.update(eventTracker); + }); +} +``` diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/reverseproxy.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/reverseproxy.md new file mode 100644 index 00000000..7a63e698 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/reverseproxy.md @@ -0,0 +1,36 @@ +--- +title: 静态资源代理 +description: 了解如何使用静态资源代理来访问插件中的静态资源 +--- + +插件中的静态资源如图片等如果想被外部访问到,需要放到 `src/main/resources` 目录下,并通过创建 `ReverseProxy` 自定义模型对象来进行静态资源代理访问。 + +例如 `src/main/resources` 下的 `static` 目录下有一张 `halo.jpg`: + +1. 首先需要在 `src/main/resources/extensions` 下创建一个 `yaml`,文件名可以任意。 +2. 声明 `ReverseProxy` 对象如下: + + ```yaml + apiVersion: plugin.halo.run/v1alpha1 + kind: ReverseProxy + metadata: + # 为了避免与其他插件冲突,推荐带上插件名称前缀 + name: my-plugin-fake-reverse-proxy + rules: + - path: /res/** + file: + directory: static + # 如果想代理 static 下所有静态资源则省略 filename 配置 + filename: halo.jpg + ``` + +插件启动后会根据 `/plugins/{plugin-name}/assets/**` 规则生成访问路径, +因此该 `ReverseProxy` 的访问路径为: `/plugins/my-plugin/assets/res/halo.jpg`。 + +- `rules` 下可以添加多组规则。 +- `path` 为路径前缀。 +- `file` 表示访问文件系统,目前暂时仅支持这一种。 +- `directory` 表示要代理的目标文件目录,它相对于 `src/main/resources/` 目录。 +- `filename` 表示要代理的目标文件名。 + +`directory` 和 `filename` 都是可选的,但必须至少有一个被配置。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/role-template.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/role-template.md new file mode 100644 index 00000000..896d42fc --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/role-template.md @@ -0,0 +1,307 @@ +--- +title: API 权限控制 +description: 了解如何对插件中的 API 定义角色模板以接入权限控制 +--- + +插件中的 APIs 无论是自定义模型自动生成的 APIs 或者是通过 `@Controller` 自定义的 APIs 都只有超级管理员能够访问,如果想将这些 APIs 授权给其他用户访问, +则需要定义一些[角色模板](../../basics/framework.md#rbac)的资源以便可以在用户界面上将其分配给其他角色使用。 + +## 角色模板定义 + +定义角色模板需要遵循一定的规范: + +- **文件位置和标记**:角色模板定义文件存放于 `src/main/resources/extensions`,文件名可以任意,它的 kind 为 Role 且必须具有标签 `halo.run/role-template: "true"` 来标识其为模板。 +- **角色类型**:通常,我们为同一种资源定义两种角色模板:只读权限和管理权限,分别对应 `view` 和 `manage`,如果需要更细粒度的控制,可以定义更多的角色模板。 +- **角色名称**:角色名称必须以插件名作为前缀,以避免与其他插件冲突,例如 `my-plugin-role-view-persons`。 +- **角色依赖**:如果一个角色需要依赖于另一个角色,可以通过 `rbac.authorization.halo.run/dependencies` 作为 key 的 `metadata.annotations` 来声明依赖关系。 +- **UI 权限**:如果需要在前端界面上控制某个角色的权限,可以通过 `rbac.authorization.halo.run/ui-permissions` 作为 key 的 `metadata.annotations` 来声明。 +- **角色模板分组**:如果需要将多个角色模板归为一组显示,可以通过 `rbac.authorization.halo.run/module` 作为 key 的 `metadata.annotations` 来声明分组名称。 +- **角色显示名称**:如果需要在前端界面上显示角色的友好名称,可以通过 `rbac.authorization.halo.run/display-name` 作为 key 的 `metadata.annotations` 来声明显示名称。 +- **隐藏角色模板**:如果不想在前端界面上显示某个角色模板,可以通过 `halo.run/hidden: "true"` 的 `metadata.labels` 来隐藏角色模板。 + +角色模板定义的基本框架如下: + +```yaml +apiVersion: v1alpha1 +kind: Role +metadata: + name: role-template-name + labels: + halo.run/role-template: "true" +rules: + - apiGroups: [] + resources: [] + resourceNames: [] + verbs: [] + - nonResourceURLs: [] + verbs: [] +``` + +在遵循上述规范的基础上,最重要的是定义 `rules` 字段,它是一个数组,用于定义角色模板的权限规则,规则分为两种类型:[资源型](#resource-rules)和[非资源型](#non-resource-rules)。 + +### 资源型规则 {#resource-rules} + +资源型规则用于定义对资源的操作权限,API 符合以下特征: + +- 以 `/api` 开头,且以 `/api//[//]` 规则组成 APIs,最少路径层级为 3 即 `/api//`,最多路径层级为 5 即包含 `` 和 ``,例如 `/api/v1/posts`。 +- 以 `/apis///[//]` 规则组成的 APIs,最少路径层级为 4 即 `/apis///`,最多路径层级为 6 即包含 `` 和 ``,例如 `/apis/my-plugin.halo.run/v1alpha1/persons`。 + +:::info 注 +`[]`包裹的部分表示可选,`/api` 前缀被 Halo 保留,不允许插件定义以 `/api` 开头的资源型 APIs,所以插件的资源型 APIs 都是以 `/apis` 开头的。 +::: + +通常可以通过 `apiGroups`、`resources`、`resourceNames`、`verbs` 来组合定义。 +例如对于资源型 API `GET /apis/my-plugin.halo.run/v1alpha1/persons`,可以定义如下规则: + +```yaml +rules: + - apiGroups: [ "my-plugin.halo.run" ] + resources: [ "my-plugin/persons" ] + verbs: [ "list" ] +``` + +而对于资源型 API `GET /apis/my-plugin.halo.run/v1alpha1/persons/zhangsan`,可以定义如下规则: + +```yaml +rules: + - apiGroups: [ "my-plugin.halo.run" ] + resources: [ "my-plugin/persons" ] + resourceNames: [ "zhangsan" ] + verbs: [ "get" ] +``` + +关于 `verbs` 的详细说明请参考 [Verbs 详解](#verbs)。 + +### 非资源型规则 {#non-resource-rules} + +凡是不符合资源型 APIs 规则的 APIs 都被定型为非资源型 APIs,例如 `/healthz`,可以使用以下配置方式: + +```yaml +rules: + - nonResourceURLs: ["/healthz", "/healthz/*"] + verbs: [ "get", "create"] +``` + +非资源型规则使用 `nonResourceURLs` 来定义,其中 `nonResourceURLs` 是一个字符串数组,用于定义非资源型 APIs 的路径,`verbs` 用于定义非资源型 APIs 的请求动词。 + +`nonResourceURL` 中的 `*` 是一个全局通配符,表示匹配所有路径,如 `/healthz/*` 表示匹配 `/healthz/` 下的所有路径。 + +### 示例:定义人员管理角色模板 + +以下 YAML 文件展示了如何定义用于人员管理的角色模板: + +```yaml +apiVersion: v1alpha1 +kind: Role +metadata: + # 使用 plugin name 作为前缀防止与其他插件冲突,比如这里的 my-plugin + name: my-plugin-role-view-persons + labels: + halo.run/role-template: "true" + annotations: + rbac.authorization.halo.run/module: "Persons Management" + rbac.authorization.halo.run/display-name: "Person Manage" + rbac.authorization.halo.run/ui-permissions: | + ["plugin:my-plugin:person:view"] +rules: + - apiGroups: ["my-plugin.halo.run"] + resources: ["my-plugin/persons"] + verbs: ["*"] +--- +apiVersion: v1alpha1 +kind: Role +metadata: + name: my-plugin-role-manage-persons + labels: + halo.run/role-template: "true" + annotations: + rbac.authorization.halo.run/dependencies: | + [ "role-template-view-person" ] + rbac.authorization.halo.run/module: "Persons Management" + rbac.authorization.halo.run/display-name: "Person Manage" + rbac.authorization.halo.run/ui-permissions: | + ["plugin:my-plugin:person:manage"] +rules: + - apiGroups: [ "my-plugin.halo.run" ] + resources: [ "my-plugin/persons" ] + verbs: [ "get", "list" ] +``` + +上述便是根据 [自定义模型](./extension.md) 章节中定义的 Person 自定义模型来配置角色模板的示例。 + +1. 定义了一个用于管理 Person 自定义模型对象的角色模板 `my-plugin-role-manage-persons`,它具有所有权限。 +2. 定义了一个只允许查询 Person 资源的角色模板 `my-plugin-role-view-persons`。 +3. `metadata.name` 的命名规则参考 [metadata name 命名规范](../server/extension.md#metadata-name)。 + +下面让我们回顾一下这些配置: + +`rules` 是个数组,它允许配置多组规则: + +- `apiGroups` 对应 `GVK` 中的 `group` 所声明的值。 +- `resources` 对应 API 中的 resource 部分。 +- `verbs` 表示请求动词,可选值为 "create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"。参考 [Verbs 详解](#verbs)。 + +`metadata.labels` 中必须包含 `halo.run/role-template: "true"` 以表示它此资源要作为角色模板。 + +`metadata.annotations` 中: + +- `rbac.authorization.halo.run/dependencies`:用于声明角色间的依赖关系,例如管理角色必须要依赖查看角色,以避免分配了管理权限却没有查看权限的情况。 +- `rbac.authorization.halo.run/module`:角色模板分组名称。在此示例中,管理 Person 的模板角色将和查看 Person 的模板角色将被在 UI 层面归为一组展示。 +- `rbac.authorization.halo.run/display-name`:模板角色的显示名称,用于展示为用户可读的名称信息。 + +### UI 权限控制 {#ui-permissions} + +通过在角色模板的 `metadata.annotations` 中定义 `rbac.authorization.halo.run/ui-permissions` 来控制 UI 权限,这样可以在前端界面通过这个权限来控制菜单或者页面按钮是否展示。 + +值的规则为 `plugin:{your-plugin-name}:scope-name`, `scope-name` 为你自定义的权限名称,如上面的示例中的 `plugin:my-plugin:person:view` 和 `plugin:my-plugin:person:manage`。 + +你可以在 UI 层面使用这个权限来控制菜单是否展示: + + ```javascript + { + path: "", + name: "HelloWorld", + component: DefaultView, + meta: { + permissions: ["plugin:my-plugin:person:view"] + } + } + ``` + +> 该配置示例为在插件前端部分入口文件 `index.ts`。 + +或者在按钮或页面组件中使用这个权限来控制是否展示: + +```html + +``` + +### Verbs 详解 {#verbs} + +`verbs` 字段用于指定用户或服务在特定资源上能执行的操作类型。这些操作被定义为一组“动词”,每个动词与相应的 HTTP 请求方法相对应。为了更好地理解如何确定合适的 `verbs`,以下是详细的解释和每种动词的具体用途: + +动词和对应的 HTTP 方法: + +- create: 对应 HTTP 的 POST 方法。用于创建一个新的资源实例,如果是创建子资源且不需要资源名称可以使用 `-` 表示缺省,如 `POST /apis/my-plugin.halo.run/v1alpha1/persons/-/subresource`,同时需要注意 `POST /apis/my-plugin.halo.run/v1alpha1/persons/{some-name}` 不是一个符合规范的 create 操作,创建资源不应该包含资源名称。 +- get: 对应 HTTP 的 GET 方法。用于获取单个资源的详细信息,即 API 中包含 resourceName 部分如 `GET /apis/my-plugin.halo.run/v1alpha1/persons/zhangsan`。 +- list: 同样对应 HTTP 的 GET 方法,但用于获取资源的集合(列表),这通常涵盖了多个资源实例的摘要或详细信息,如 `GET /apis/my-plugin.halo.run/v1alpha1/persons`。 +- watch: 也是对应 HTTP 的 GET 方法。用于实时监控资源或资源集合的变化,通常是通过 WebSocket 连接来实现的,如 `ws://localhost:8090/apis/my-plugin.halo.run/v1alpha1/persons`。 +- update: 对应 HTTP 的 PUT 方法。用于更新现有资源的全部内容。 +- patch: 对应 HTTP 的 PATCH 方法。用于对现有资源进行部分更新。 +- delete: 对应 HTTP 的 DELETE 方法。用于删除单个资源, 即 API 中包含 resourceName 部分如 `DELETE /apis/my-plugin.halo.run/v1alpha1/persons/zhangsan`。 +- deletecollection: 同样对应 HTTP 的 DELETE 方法,但用于删除一个资源集合。 + +可以使用如下表格来简化理解: + +| Verb | HTTP Method(s) | Description | +|--------------------|----------------|--------------------------| +| `create` | POST | 创建新资源实例 | +| `get` | GET | 获取单个资源详细信息 | +| `list` | GET | 获取资源列表 | +| `watch` | GET | 监控资源或资源集合的变化 | +| `update` | PUT | 更新现有资源 | +| `patch` | PATCH | 部分更新资源 | +| `delete` | DELETE | 删除单个资源 | +| `deletecollection` | DELETE | 删除资源集合 | + +## 默认角色 + +在 Halo 中,每个访问者都至少有一个角色,包括未登录的用户(被称为匿名用户)它们会拥有角色为 `anonymous` 的角色,而已登录的用户则会至少拥有一个角色名为 `authenticated` 的角色, +但这两个角色不会显示在角色列表中。 + +`anonymous` 角色的定义参考 [anonymous 角色](https://github.com/halo-dev/halo/blob/main/application/src/main/resources/extensions/role-template-anonymous.yaml)。 + +`authenticated` 角色的定义参考 [authenticated 角色](https://github.com/halo-dev/halo/blob/main/application/src/main/resources/extensions/role-template-authenticated.yaml)。 + +进入角色列表页面,你会看到一些内置角色,用于方便你快速的分配权限给用户,并可以基于这些角色来创建新的角色: + +- 超级管理员:拥有所有权限,不可删除,不可编辑。 +- 访客:拥有默认的 `anonymous` 和 `authenticated` 角色的权限。 +- 投稿者:拥有“允许投稿”的权限。 +- 作者:拥有“允许管理自己的文章”和”允许发布自己的文章“的权限。 +- 文章管理员:拥有“允许管理所有文章”的权限。 + +## 角色绑定 + +角色绑定用于将角色中定义的权限授予一个或一组用户。它包含主体列表(用户)以及对所授予角色的引用。 + +角色绑定示例: + +```yaml +apiVersion: v1alpha1 +# 这个角色绑定允许 "guqing" 用户拥有 "post-reader" 角色的权限 +# 你需要在 Halo 中已经定义了一个名为 "post-reader" 的角色。 +kind: RoleBinding +metadata: + name: guqing-post-reader-binding +roleRef: + # "roleRef" 指定了绑定到的角色 + apiGroup: '' + # 这里必须是 Role + kind: Role + # 这里的 name 必须匹配到一个已经定义的角色 + name: post-reader +subjects: +- apiGroup: '' + kind: User + # 这里的 name 是用户的 username + name: guqing +``` + +在 Halo 中,当你给一个用户分配角色后,实际上就是创建了一个 ”RoleBinding” 对象来完成的。 + +## 聚合角色 + +你可以聚合角色来将多个角色的权限聚合到一个已有的角色中,这样你就不需要再为每个用户分配多个角色了。 + +聚合角色是通过在你定义的角色模板中添加 `"rbac.authorization.halo.run/aggregate-to-` 开头的 label 来实现的,例如 + +```yaml +apiVersion: v1alpha1 +kind: "Role" +metadata: + name: role-template-view-categories + labels: + halo.run/role-template: "true" + rbac.authorization.halo.run/aggregate-to-editor: "true" + annotations: + rbac.authorization.halo.run/ui-permissions: | + [ "system:categories:view", "uc:categories:view" ] +rules: + - apiGroups: [ "content.halo.run" ] + resources: [ "categories" ] + verbs: [ "get", "list" ] +``` + +`rbac.authorization.halo.run/aggregate-to-editor` 表示将 `role-template-view-categories` 角色聚合到 `editor` 角色中,这样所有拥有 `editor` 角色的用户都会拥有 `role-template-view-categories` 角色的权限。 + +如果你想将你写的资源型 APIs 公开给所有用户访问,这时你可以通过聚合角色来将你的资源型 APIs 的角色聚合到 `anonymous` 角色中,这样所有用户都可以访问你的资源型 APIs 了。 + +```yaml +apiVersion: v1alpha1 +kind: Role +metadata: + name: my-plugin-role-view-persons + labels: + halo.run/role-template: "true" + rbac.authorization.halo.run/aggregate-to-anonymous: "true" + annotations: + rbac.authorization.halo.run/module: "Persons Management" + rbac.authorization.halo.run/display-name: "Person Manage" + rbac.authorization.halo.run/ui-permissions: | + ["plugin:my-plugin:person:view"] +rules: + - apiGroups: ["my-plugin.halo.run"] + resources: ["my-plugin/persons"] + verbs: ["*"] +``` + +`rbac.authorization.halo.run/aggregate-to-anonymous` 的写法就表示将 `my-plugin-role-view-persons` 角色聚合到 `anonymous` 角色中。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/setting-fetcher.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/setting-fetcher.md new file mode 100644 index 00000000..a572a39c --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/setting-fetcher.md @@ -0,0 +1,139 @@ +--- +title: 获取插件配置 +description: 了解如何获取插件定义的设置表单对应的配置数据,以及如何在插件中使用配置数据。 +--- + +插件的 `plugin.yaml` 中允许配置 `settingName` 和 `configMapName` 字段,用于定义插件的个性化设置。 +本文介绍如何获取插件定义的设置表单对应的配置数据,以及如何在插件中使用配置数据。 + +## 概述 + +Halo 提供了两个 Bean 用于获取插件配置数据:`SettingFetcher` 和 `ReactiveSettingFetcher`,分别用于同步和异步获取配置数据。 + +以 `ReactiveSettingFetcher` 为例,提供了以下方法: + +```java +public interface ReactiveSettingFetcher { + + Mono fetch(String group, Class clazz); + + @NonNull + Mono get(String group); + + @NonNull + Mono> getValues(); +} +``` + +- `fetch` 方法用于获取指定分组的配置数据,并将其转换为指定的 Java 类型。 +- `get` 方法用于获取指定分组的配置数据,返回 `JsonNode` 类型。 +- `getValues` 方法用于获取所有配置数据,返回 `Map` 类型,其中键为分组名称,值为配置对象。 + +`ReactiveSettingFetcher` 和 `SettingFetcher` 底层都对配置数据进行了缓存,以提高性能,并且在配置变更时会自动刷新缓存,所以直接调用这些方法即可获取最新的配置数据。 + +## 监听配置变更 + +当用户修改插件配置时,可以通过监听 `PluginConfigUpdatedEvent` 事件,执行相应的操作。`PluginConfigUpdatedEvent` 包含了配置变更前后的数据,使插件能够对变化做出响应。 + +```java +public class PluginConfigUpdatedEvent extends ApplicationEvent { + private final Map oldConfig; + private final Map newConfig; + + // ... +} +``` + +## 使用示例 + +### 定义设置表单 + +假设插件定义了一个名为 `setting-seo` 的设置表单,其中包含了 `blockSpiders`、`keywords` 和 `description` 三个字段: + +```yaml +apiVersion: v1alpha1 +kind: Setting +metadata: + name: setting-seo +spec: + forms: + - group: seo + label: SEO 设置 + formSchema: + - $formkit: checkbox + name: blockSpiders + label: "屏蔽搜索引擎" + value: false + - $formkit: textarea + name: keywords + label: "站点关键词" + - $formkit: textarea + name: description + label: "站点描述" +``` + +### 配置 plugin.yaml + +在 `plugin.yaml` 中配置 `settingName` 和 `configMapName` 字段: + +```yaml +apiVersion: plugin.halo.run/v1alpha1 +kind: Plugin +metadata: + name: fake-plugin +spec: + displayName: "Fake Plugin" + # ... + configMapName: setting-seo-configmap + settingName: setting-seo +``` + +### 定义值类 + +为了方便使用,定义一个值类存储配置数据: + +```java +public record SeoSetting(boolean blockSpiders, String keywords, String description) { + public static final String GROUP = "seo"; +} +``` + +### 获取配置数据 + +通过依赖注入 `ReactiveSettingFetcher` 并使用 `fetch(group, type)` 方法查询配置: + +```java +@Service +@RequiredArgsConstructor +public class SeoService { + private final ReactiveSettingFetcher settingFetcher; + + public Mono checkSeo() { + return settingFetcher.fetch(SeoSetting.GROUP, SeoSetting.class) + .doOnNext(seoSetting -> { + if (seoSetting.blockSpiders()) { + // do something + } + }) + .then(); + } +} +``` + +### 监听配置变更 + +通过监听 `PluginConfigUpdatedEvent` 事件来处理配置变更: + +```java +@Component +public class SeoConfigListener { + @EventListener + public void onConfigUpdated(PluginConfigUpdatedEvent event) { + if (event.getNewConfig().containsKey(SeoSetting.GROUP)) { + // do something + } + } +} +``` + +通过以上示例,可以看到如何使用 `ReactiveSettingFetcher` 获取配置数据,并通过监听 `PluginConfigUpdatedEvent` 来处理配置变更事件,确保系统能及时响应配置的变化。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/template-for-theme.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/template-for-theme.md new file mode 100644 index 00000000..fed27911 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/template-for-theme.md @@ -0,0 +1,90 @@ +--- +title: 在插件中提供主题模板 +description: 了解如何为主题扩充模板。 +--- + +当你在插件中创建了自己的自定义模型后,你可能需要在主题端提供一个模板来展示这些数据,这一般有两种方式: + +1. 插件规定模板名称,由主题选择性适配,如瞬间插件提供了 `/moments` 的路由渲染 `moment.html` 模板,主题可以选择性的提供 `moment.html` 模板来展示瞬间数据。 +2. 插件提供默认模板,当主题没有提供对应的模板时,使用默认模板,主题提供了对应的模板时,使用主题提供的模板。 + +## 创建一个模板 + +首先,你需要在插件的 `resources` 目录下创建一个 `templates` 目录,然后在 `templates` 目录下提供你的模板,例如: + +```text +├── templates +│ ├── moment.html +``` + +然后提供一个路由用于渲染这个模板,例如: + +```java +import run.halo.app.theme.TemplateNameResolver; + +@RequiredArgsConstructor +@Configuration(proxyBeanMethods = false) +public class MomentRouter { + private final TemplateNameResolver templateNameResolver; + + @Bean + RouterFunction momentRouterFunction() { + return route(GET("/moments"), this::renderMomentPage).build(); + } + + Mono renderMomentPage(ServerRequest request) { + // 或许你需要准备你需要提供给模板的默认数据,非必须 + var model = new HashMap(); + model.put("moments", List.of()); + return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "moments") + .flatMap(templateName -> ServerResponse.ok().render(templateName, model)); + } +} +``` + +使用 `TemplateNameResolver` 来解析模板名称,如果主题提供了对应的模板,那么就使用主题提供的模板,否则使用插件提供的模板,如果直接返回模板名称,那么只会使用主题提供的模板,如果主题没有提供对应的模板,那么会抛出异常。 + +## 模板片段 + +如果你的默认模板不止一个,你可能需要通过模板片段来抽取一些公共的部分,例如,你的插件提供了一个 `moment.html` 模板,你可能需要抽取一些公共的部分,例如头部、尾部等,你可以这样做: + +```text +├── templates +│ ├── moment.html +│ ├── fragments +│ │ ├── layout.html +``` + +然后定义一个 `layout.html` 模板,例如: + +```html + + + + + Moment + + +
+ +
+ + +``` + +那么使用 `layout.html` 模板中提供的 `fragment` 时,你需要这样做: + +```html +
+ Hello World +
+``` + +`plugin:plugin-moment:fragments/layout` 即为使用 `layout.html` 模板的路径,必须以 `plugin::`前缀作为开头,`fragments/layout` 为模板相对于 `resources/templates` 的路径,`` 即为你的插件名称。 + +**总结:** + +1. 定义模板片段时与主题端定义模板片段时一样 +2. 使用模板片段时,必须以 `plugin::` 前缀作为开头,后跟模板相对于 `resources/templates` 的路径,例如 `plugin:plugin-moment:fragments/layout`,`plugin-moment` 即为你的插件名称,`fragments/layout` 为模板相对于 `resources/templates` 的路径。 + +参考:[Thymeleaf 模板片段](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#including-template-fragments) diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/websocket.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/websocket.md new file mode 100644 index 00000000..94c723e3 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/server/websocket.md @@ -0,0 +1,46 @@ +--- +title: 实现 WebSocket +description: 了解在插件中如何实现 WebSocket。 +--- + +从 Halo 2.15.0 版本开始,核心提供了 WebSocketEndpoint 接口,其主要目的是为了方便插件实现 WebSocket 功能。 + +插件只需要实现这个接口,并添加 `@Component` 注解,WebSocket 实现将会在插件启动后生效,插件卸载后,该实现也会随之删除。 + +在插件中实现 WebSocket 的代码样例如下: + +```java +@Component +public class MyWebSocketEndpoint implements WebSocketEndpoint { + + @Override + public GroupVersion groupVersion() { + return GroupVersion.parseApiVersion("my-plugin.halowrite.com/v1alpha1"); + } + + @Override + public String urlPath() { + return "/resources"; + } + + @Override + public WebSocketHandler handler() { + return session -> { + var messages = session.receive() + .map(message -> { + var payload = message.getPayloadAsText(); + return session.textMessage(payload.toUpperCase()); + }); + return session.send(messages); + }; + } +} +``` + +当插件安装成功后,可以通过路径 `/apis/my-plugin.halowrite.com/v1alpha1/resources` 访问。 示例如下: + +```bash +websocat --basic-auth admin:admin ws://127.0.0.1:8090/apis/my-plugin.halowrite.com/v1alpha1/resources +``` + +需要注意的是, 插件中实现的 WebSocket 相关的 API 仍然受当前权限系统约束。 diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/api-request.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/api-request.md new file mode 100644 index 00000000..f01215c7 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/api-request.md @@ -0,0 +1,70 @@ +--- +title: API 请求 +description: 介绍如何在插件的 UI 中请求 API 接口 +--- + +在 2.17.0 版本中,Halo 提供了新的 `@halo-dev/api-client` 包,用于简化在 Halo 内部、插件的 UI 中、外部应用程序中请求 Halo 接口的逻辑。此文档将介绍如何在插件的 UI 中使用 `@halo-dev/api-client` 包。 + +## 安装 + +```shell +pnpm install @halo-dev/api-client axios +``` + +## 模块介绍 + +在 `@halo-dev/api-client` 包中导出了以下模块: + +```ts +import { + coreApiClient, + consoleApiClient, + ucApiClient, + publicApiClient, + createCoreApiClient, + createConsoleApiClient, + createUcApiClient, + createPublicApiClient, + axiosInstance +} from "@halo-dev/api-client" +``` + +- **coreApiClient**: 为 Halo 所有自定义模型的 CRUD 接口封装的 api client。 +- **consoleApiClient**: 为 Halo 针对 Console 提供的接口封装的 api client。 +- **ucApiClient**: 为 Halo 针对 UC 提供的接口封装的 api client。 +- **publicApiClient**: 为 Halo 所有公开访问的接口封装的 api client。 +- **createCoreApiClient**: 用于创建自定义模型的 CRUD 接口封装的 api client,需要传入 axios 实例。 +- **createConsoleApiClient**: 用于创建 Console 接口封装的 api client,需要传入 axios 实例。 +- **createUcApiClient**: 用于创建 UC 接口封装的 api client,需要传入 axios 实例。 +- **createPublicApiClient**: 用于创建公开访问接口封装的 api client,需要传入 axios 实例。 +- **axiosInstance**: 内部默认创建的 axios 实例。 + +## 使用 + +在 Halo 的插件项目中,如果是调用 Halo 内部的接口,那么直接使用上面介绍的模块即可,无需任何配置,在 Halo 内部已经处理好了异常逻辑,包括登录失效、无权限等。 + +其中,`coreApiClient`、`consoleApiClient`、`ucApiClient`、`publicApiClient` 模块是对 Halo 内部所有 API 请求的封装,无需传入任何请求地址,比如: + +```ts +import { coreApiClient } from "@halo-dev/api-client" + +coreApiClient.content.post.listPost().then(response => { + // handle response +}) +``` + +如果需要调用插件提供的接口,可以直接使用 `axiosInstance` 实例,比如: + +```ts +import { axiosInstance } from "@halo-dev/api-client" + +axiosInstance.get("/apis/foo.halo.run/v1alpha1/bar").then(response => { + // handle response +}) +``` + +此外,在最新的 `@halo-dev/ui-plugin-bundler-kit@2.17.0` 中,已经排除了 `@halo-dev/api-client`、`axios` 依赖,所以最终产物中的相关依赖会自动使用 Halo 本身提供的依赖,无需关心最终产物大小。 + +:::info 提醒 +如果插件中使用了 `@halo-dev/api-client@2.17.0` 和 `@halo-dev/ui-plugin-bundler-kit@2.17.0`,需要提升 `plugin.yaml` 中的 `spec.requires` 版本为 `>=2.17.0`。 +::: diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/annotations-form.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/annotations-form.md new file mode 100644 index 00000000..98f4e349 --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/annotations-form.md @@ -0,0 +1,55 @@ +--- +title: AnnotationsForm +description: 元数据表单组件 +--- + +此组件用于提供统一的 [Annotations 表单](../../../../annotations-form.md),可以根据 `group` 和 `kind` 属性自动渲染对应的表单项。 + +## 使用示例 + +```html + + + +``` + +## Props + +| 属性名 | 类型 | 默认值 | 描述 | +|---------|------------------------------------|---------|-----------------------------------------| +| `group` | string | 无,必填 | 定义组件所属的分组。 | +| `kind` | string | 无,必填 | 定义组件的种类。 | +| `value` | \{ [key: string]: string; \} \| null \| null | 可选,包含键值对的对象或空值,用于存储数据。 | diff --git a/versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/attachment-file-type-icon.md b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/attachment-file-type-icon.md new file mode 100644 index 00000000..2b24894f --- /dev/null +++ b/versioned_docs/version-2.19/developer-guide/plugin/api-reference/ui/components/attachment-file-type-icon.md @@ -0,0 +1,25 @@ +--- +title: AttachmentFileTypeIcon +description: 附件文件类型图标组件 +--- + +此组件用于根据文件名显示文件类型图标。 + +## 使用示例 + +```html + + +