From 2b89d65009de7a810d82079ee2c70825bf85eb32 Mon Sep 17 00:00:00 2001 From: qwqcode Date: Wed, 23 Oct 2024 00:26:09 +0800 Subject: [PATCH] save --- conf/artalk.example.zh-CN.yml | 8 + docs/swagger/docs.go | 777 ++++++++++++++++++ docs/swagger/swagger.json | 777 ++++++++++++++++++ docs/swagger/swagger.yaml | 478 +++++++++++ internal/config/base.go | 4 + internal/config/config.go | 6 + internal/dao/migrate.go | 3 +- internal/entity/plugin.go | 28 + internal/entity/plugin_cooked.go | 37 + internal/entity/plugin_option.go | 21 + package.json | 2 +- pnpm-lock.yaml | 578 ++++++------- public/artalk-plugin-options.schema.json | 9 + public/registry.json | 43 + server/common/conf.go | 82 +- server/handler/plugin_get.go | 147 ++++ server/handler/plugin_install.go | 142 ++++ server/handler/plugin_list.go | 134 +++ server/handler/plugin_registry.go | 84 ++ server/handler/plugin_uninstall.go | 43 + server/handler/plugin_update.go | 85 ++ server/handler/plugin_upgrade.go | 26 + server/server.go | 10 + ui/artalk-sidebar/components.d.ts | 2 + ui/artalk-sidebar/package.json | 1 + .../src/assets/nav-icon-plugins.svg | 3 + .../src/components/AppDialog.vue | 76 ++ .../src/components/AppHeader.vue | 4 + .../src/components/AppNavigationMenu.ts | 7 + .../src/components/PluginOptionsEditor.vue | 121 +++ ui/artalk-sidebar/src/i18n-en.ts | 1 + ui/artalk-sidebar/src/i18n/fr.ts | 1 + ui/artalk-sidebar/src/i18n/ja.ts | 1 + ui/artalk-sidebar/src/i18n/ko.ts | 1 + ui/artalk-sidebar/src/i18n/ru.ts | 1 + ui/artalk-sidebar/src/i18n/zh-CN.ts | 1 + ui/artalk-sidebar/src/i18n/zh-TW.ts | 1 + ui/artalk-sidebar/src/pages/plugins.vue | 632 ++++++++++++++ ui/artalk-sidebar/src/pages/settings.vue | 2 + ui/artalk-sidebar/src/stores/nav.ts | 1 + ui/artalk-sidebar/typed-router.d.ts | 1 + ui/artalk/src/api/v2.ts | 338 ++++++++ ui/artalk/src/mount.ts | 63 +- ui/plugin-kit/package.json | 3 +- ui/plugin-kit/src/plugin/main.ts | 15 + ui/plugin-kit/src/plugin/options-schema.ts | 38 + ui/plugin-kit/tsup.config.ts | 2 +- ui/plugin-lightbox/src/main.ts | 4 + 48 files changed, 4524 insertions(+), 320 deletions(-) create mode 100644 internal/entity/plugin.go create mode 100644 internal/entity/plugin_cooked.go create mode 100644 internal/entity/plugin_option.go create mode 100644 public/artalk-plugin-options.schema.json create mode 100644 public/registry.json create mode 100644 server/handler/plugin_get.go create mode 100644 server/handler/plugin_install.go create mode 100644 server/handler/plugin_list.go create mode 100644 server/handler/plugin_registry.go create mode 100644 server/handler/plugin_uninstall.go create mode 100644 server/handler/plugin_update.go create mode 100644 server/handler/plugin_upgrade.go create mode 100644 ui/artalk-sidebar/src/assets/nav-icon-plugins.svg create mode 100644 ui/artalk-sidebar/src/components/AppDialog.vue create mode 100644 ui/artalk-sidebar/src/components/PluginOptionsEditor.vue create mode 100644 ui/artalk-sidebar/src/pages/plugins.vue create mode 100644 ui/plugin-kit/src/plugin/options-schema.ts diff --git a/conf/artalk.example.zh-CN.yml b/conf/artalk.example.zh-CN.yml index 738cc5cd..7e69c7c9 100644 --- a/conf/artalk.example.zh-CN.yml +++ b/conf/artalk.example.zh-CN.yml @@ -395,6 +395,14 @@ auth: client_secret: "" domain: "" +# 插件系统 +plugin: + # 启用插件系统 + enabled: true + # 注册表 URL + # (默认注册表:https://artalk.js.org/plugins) + registry_url: https://artalk.js.org/plugins + # 界面配置 frontend: # 评论框占位文字 diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index c0634c58..f5a67d62 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -1944,6 +1944,547 @@ const docTemplate = `{ } } }, + "/plugin_registry/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update the plugin registry data", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Update Plugin Registry", + "operationId": "UpdatePluginRegistry", + "parameters": [ + { + "description": "The options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ParamsPluginRegistryUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.Map" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plugins": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a list of plugins by some conditions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Get Plugin List", + "operationId": "GetPlugins", + "parameters": [ + { + "type": "boolean", + "description": "Only installed plugins", + "name": "only_installed", + "in": "query" + }, + { + "type": "string", + "description": "Search keywords", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ResponsePluginList" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plugins/{plugin_id}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a plugin info by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Get Plugin Info", + "operationId": "GetPlugin", + "parameters": [ + { + "type": "string", + "description": "The plugin ID", + "name": "plugin_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ResponsePluginGet" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update a plugin status by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Update Plugin", + "operationId": "UpdatePlugin", + "parameters": [ + { + "type": "string", + "description": "The plugin ID", + "name": "plugin_id", + "in": "path", + "required": true + }, + { + "description": "The plugin status", + "name": "plugin", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ParamsPluginUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ResponsePluginUpdate" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plugins/{plugin_id}/install": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Install a plugin by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Install Plugin", + "operationId": "InstallPlugin", + "parameters": [ + { + "type": "string", + "description": "The plugin ID", + "name": "plugin_id", + "in": "path", + "required": true + }, + { + "description": "The options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ParamsPluginInstall" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.Map" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plugins/{plugin_id}/uninstall": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Uninstall a plugin by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Uninstall Plugin", + "operationId": "UninstallPlugin", + "parameters": [ + { + "type": "string", + "description": "The plugin ID", + "name": "plugin_id", + "in": "path", + "required": true + }, + { + "description": "The options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ParamsPluginUninstall" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.Map" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plugins/{plugin_id}/upgrade": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Upgrade a plugin by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Upgrade Plugin", + "operationId": "UpgradePlugin", + "parameters": [ + { + "type": "string", + "description": "The plugin ID", + "name": "plugin_id", + "in": "path", + "required": true + }, + { + "description": "The options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ParamsPluginUpgrade" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.Map" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, "/send_email": { "post": { "security": [ @@ -3788,12 +4329,19 @@ const docTemplate = `{ "type": "object", "required": [ "frontend_conf", + "plugins", "version" ], "properties": { "frontend_conf": { "$ref": "#/definitions/common.Map" }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/common.PluginItem" + } + }, "version": { "$ref": "#/definitions/common.ApiVersionData" } @@ -3819,6 +4367,27 @@ const docTemplate = `{ "type": "object", "additionalProperties": true }, + "common.PluginItem": { + "type": "object", + "required": [ + "source", + "type" + ], + "properties": { + "integrity": { + "type": "string" + }, + "options": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/entity.PluginType" + } + } + }, "entity.CookedComment": { "type": "object", "required": [ @@ -4000,6 +4569,104 @@ const docTemplate = `{ } } }, + "entity.CookedPlugin": { + "type": "object", + "required": [ + "author_link", + "author_name", + "compatible", + "description", + "donate_link", + "enabled", + "id", + "installed", + "integrity", + "local_version", + "min_artalk_version", + "name", + "npm_name", + "options_schema", + "repo_link", + "repo_name", + "source", + "type", + "updated_at", + "upgrade_available", + "verified", + "version" + ], + "properties": { + "author_link": { + "type": "string" + }, + "author_name": { + "type": "string" + }, + "compatible": { + "type": "boolean" + }, + "compatible_notice": { + "type": "string" + }, + "description": { + "type": "string" + }, + "donate_link": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "installed": { + "type": "boolean" + }, + "integrity": { + "type": "string" + }, + "local_version": { + "type": "string" + }, + "min_artalk_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "npm_name": { + "type": "string" + }, + "options_schema": { + "type": "string" + }, + "repo_link": { + "type": "string" + }, + "repo_name": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "upgrade_available": { + "type": "boolean" + }, + "verified": { + "type": "boolean" + }, + "version": { + "type": "string" + } + } + }, "entity.CookedSite": { "type": "object", "required": [ @@ -4124,6 +4791,17 @@ const docTemplate = `{ } } }, + "entity.PluginType": { + "type": "string", + "enum": [ + "plugin", + "theme" + ], + "x-enum-varnames": [ + "PluginTypePlugin", + "PluginTypeTheme" + ] + }, "handler.Map": { "type": "object", "additionalProperties": true @@ -4345,6 +5023,34 @@ const docTemplate = `{ } } }, + "handler.ParamsPluginInstall": { + "type": "object" + }, + "handler.ParamsPluginRegistryUpdate": { + "type": "object" + }, + "handler.ParamsPluginUninstall": { + "type": "object" + }, + "handler.ParamsPluginUpdate": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "client_options": { + "description": "The plugin client options (JSON string)", + "type": "string" + }, + "enabled": { + "description": "The plugin enabled status", + "type": "boolean" + } + } + }, + "handler.ParamsPluginUpgrade": { + "type": "object" + }, "handler.ParamsSettingApply": { "type": "object", "required": [ @@ -5181,6 +5887,77 @@ const docTemplate = `{ } } }, + "handler.ResponsePluginGet": { + "type": "object", + "required": [ + "client_options", + "enabled", + "options_schema", + "plugin" + ], + "properties": { + "client_options": { + "description": "The plugin client options (JSON string)", + "type": "string" + }, + "enabled": { + "description": "The plugin enabled status", + "type": "boolean" + }, + "options_schema": { + "description": "The plugin options schema (JSON string)", + "type": "string" + }, + "plugin": { + "description": "The plugin info", + "allOf": [ + { + "$ref": "#/definitions/entity.CookedPlugin" + } + ] + } + } + }, + "handler.ResponsePluginList": { + "type": "object", + "required": [ + "plugins", + "plugins_count", + "themes", + "themes_count" + ], + "properties": { + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.CookedPlugin" + } + }, + "plugins_count": { + "type": "integer" + }, + "themes": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.CookedPlugin" + } + }, + "themes_count": { + "type": "integer" + } + } + }, + "handler.ResponsePluginUpdate": { + "type": "object", + "required": [ + "plugin" + ], + "properties": { + "plugin": { + "$ref": "#/definitions/entity.CookedPlugin" + } + } + }, "handler.ResponseSettingGet": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index aabb12a7..037f5adc 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -1937,6 +1937,547 @@ } } }, + "/plugin_registry/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update the plugin registry data", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Update Plugin Registry", + "operationId": "UpdatePluginRegistry", + "parameters": [ + { + "description": "The options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ParamsPluginRegistryUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.Map" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plugins": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a list of plugins by some conditions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Get Plugin List", + "operationId": "GetPlugins", + "parameters": [ + { + "type": "boolean", + "description": "Only installed plugins", + "name": "only_installed", + "in": "query" + }, + { + "type": "string", + "description": "Search keywords", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ResponsePluginList" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plugins/{plugin_id}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a plugin info by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Get Plugin Info", + "operationId": "GetPlugin", + "parameters": [ + { + "type": "string", + "description": "The plugin ID", + "name": "plugin_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ResponsePluginGet" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update a plugin status by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Update Plugin", + "operationId": "UpdatePlugin", + "parameters": [ + { + "type": "string", + "description": "The plugin ID", + "name": "plugin_id", + "in": "path", + "required": true + }, + { + "description": "The plugin status", + "name": "plugin", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ParamsPluginUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ResponsePluginUpdate" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plugins/{plugin_id}/install": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Install a plugin by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Install Plugin", + "operationId": "InstallPlugin", + "parameters": [ + { + "type": "string", + "description": "The plugin ID", + "name": "plugin_id", + "in": "path", + "required": true + }, + { + "description": "The options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ParamsPluginInstall" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.Map" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plugins/{plugin_id}/uninstall": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Uninstall a plugin by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Uninstall Plugin", + "operationId": "UninstallPlugin", + "parameters": [ + { + "type": "string", + "description": "The plugin ID", + "name": "plugin_id", + "in": "path", + "required": true + }, + { + "description": "The options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ParamsPluginUninstall" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.Map" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/plugins/{plugin_id}/upgrade": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Upgrade a plugin by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "Upgrade Plugin", + "operationId": "UpgradePlugin", + "parameters": [ + { + "type": "string", + "description": "The plugin ID", + "name": "plugin_id", + "in": "path", + "required": true + }, + { + "description": "The options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ParamsPluginUpgrade" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.Map" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Map" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, "/send_email": { "post": { "security": [ @@ -3781,12 +4322,19 @@ "type": "object", "required": [ "frontend_conf", + "plugins", "version" ], "properties": { "frontend_conf": { "$ref": "#/definitions/common.Map" }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/common.PluginItem" + } + }, "version": { "$ref": "#/definitions/common.ApiVersionData" } @@ -3812,6 +4360,27 @@ "type": "object", "additionalProperties": true }, + "common.PluginItem": { + "type": "object", + "required": [ + "source", + "type" + ], + "properties": { + "integrity": { + "type": "string" + }, + "options": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/entity.PluginType" + } + } + }, "entity.CookedComment": { "type": "object", "required": [ @@ -3993,6 +4562,104 @@ } } }, + "entity.CookedPlugin": { + "type": "object", + "required": [ + "author_link", + "author_name", + "compatible", + "description", + "donate_link", + "enabled", + "id", + "installed", + "integrity", + "local_version", + "min_artalk_version", + "name", + "npm_name", + "options_schema", + "repo_link", + "repo_name", + "source", + "type", + "updated_at", + "upgrade_available", + "verified", + "version" + ], + "properties": { + "author_link": { + "type": "string" + }, + "author_name": { + "type": "string" + }, + "compatible": { + "type": "boolean" + }, + "compatible_notice": { + "type": "string" + }, + "description": { + "type": "string" + }, + "donate_link": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "installed": { + "type": "boolean" + }, + "integrity": { + "type": "string" + }, + "local_version": { + "type": "string" + }, + "min_artalk_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "npm_name": { + "type": "string" + }, + "options_schema": { + "type": "string" + }, + "repo_link": { + "type": "string" + }, + "repo_name": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "upgrade_available": { + "type": "boolean" + }, + "verified": { + "type": "boolean" + }, + "version": { + "type": "string" + } + } + }, "entity.CookedSite": { "type": "object", "required": [ @@ -4117,6 +4784,17 @@ } } }, + "entity.PluginType": { + "type": "string", + "enum": [ + "plugin", + "theme" + ], + "x-enum-varnames": [ + "PluginTypePlugin", + "PluginTypeTheme" + ] + }, "handler.Map": { "type": "object", "additionalProperties": true @@ -4338,6 +5016,34 @@ } } }, + "handler.ParamsPluginInstall": { + "type": "object" + }, + "handler.ParamsPluginRegistryUpdate": { + "type": "object" + }, + "handler.ParamsPluginUninstall": { + "type": "object" + }, + "handler.ParamsPluginUpdate": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "client_options": { + "description": "The plugin client options (JSON string)", + "type": "string" + }, + "enabled": { + "description": "The plugin enabled status", + "type": "boolean" + } + } + }, + "handler.ParamsPluginUpgrade": { + "type": "object" + }, "handler.ParamsSettingApply": { "type": "object", "required": [ @@ -5174,6 +5880,77 @@ } } }, + "handler.ResponsePluginGet": { + "type": "object", + "required": [ + "client_options", + "enabled", + "options_schema", + "plugin" + ], + "properties": { + "client_options": { + "description": "The plugin client options (JSON string)", + "type": "string" + }, + "enabled": { + "description": "The plugin enabled status", + "type": "boolean" + }, + "options_schema": { + "description": "The plugin options schema (JSON string)", + "type": "string" + }, + "plugin": { + "description": "The plugin info", + "allOf": [ + { + "$ref": "#/definitions/entity.CookedPlugin" + } + ] + } + } + }, + "handler.ResponsePluginList": { + "type": "object", + "required": [ + "plugins", + "plugins_count", + "themes", + "themes_count" + ], + "properties": { + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.CookedPlugin" + } + }, + "plugins_count": { + "type": "integer" + }, + "themes": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.CookedPlugin" + } + }, + "themes_count": { + "type": "integer" + } + } + }, + "handler.ResponsePluginUpdate": { + "type": "object", + "required": [ + "plugin" + ], + "properties": { + "plugin": { + "$ref": "#/definitions/entity.CookedPlugin" + } + } + }, "handler.ResponseSettingGet": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index a2e89d47..0a0e8679 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -32,10 +32,15 @@ definitions: properties: frontend_conf: $ref: '#/definitions/common.Map' + plugins: + items: + $ref: '#/definitions/common.PluginItem' + type: array version: $ref: '#/definitions/common.ApiVersionData' required: - frontend_conf + - plugins - version type: object common.JSONResult: @@ -52,6 +57,20 @@ definitions: common.Map: additionalProperties: true type: object + common.PluginItem: + properties: + integrity: + type: string + options: + type: string + source: + type: string + type: + $ref: '#/definitions/entity.PluginType' + required: + - source + - type + type: object entity.CookedComment: properties: badge_color: @@ -184,6 +203,78 @@ definitions: - vote_down - vote_up type: object + entity.CookedPlugin: + properties: + author_link: + type: string + author_name: + type: string + compatible: + type: boolean + compatible_notice: + type: string + description: + type: string + donate_link: + type: string + enabled: + type: boolean + id: + type: string + installed: + type: boolean + integrity: + type: string + local_version: + type: string + min_artalk_version: + type: string + name: + type: string + npm_name: + type: string + options_schema: + type: string + repo_link: + type: string + repo_name: + type: string + source: + type: string + type: + type: string + updated_at: + type: string + upgrade_available: + type: boolean + verified: + type: boolean + version: + type: string + required: + - author_link + - author_name + - compatible + - description + - donate_link + - enabled + - id + - installed + - integrity + - local_version + - min_artalk_version + - name + - npm_name + - options_schema + - repo_link + - repo_name + - source + - type + - updated_at + - upgrade_available + - verified + - version + type: object entity.CookedSite: properties: first_url: @@ -273,6 +364,14 @@ definitions: - name - receive_email type: object + entity.PluginType: + enum: + - plugin + - theme + type: string + x-enum-varnames: + - PluginTypePlugin + - PluginTypeTheme handler.Map: additionalProperties: true type: object @@ -435,6 +534,25 @@ definitions: - site_name - title type: object + handler.ParamsPluginInstall: + type: object + handler.ParamsPluginRegistryUpdate: + type: object + handler.ParamsPluginUninstall: + type: object + handler.ParamsPluginUpdate: + properties: + client_options: + description: The plugin client options (JSON string) + type: string + enabled: + description: The plugin enabled status + type: boolean + required: + - enabled + type: object + handler.ParamsPluginUpgrade: + type: object handler.ParamsSettingApply: properties: yaml: @@ -1024,6 +1142,54 @@ definitions: - vote_down - vote_up type: object + handler.ResponsePluginGet: + properties: + client_options: + description: The plugin client options (JSON string) + type: string + enabled: + description: The plugin enabled status + type: boolean + options_schema: + description: The plugin options schema (JSON string) + type: string + plugin: + allOf: + - $ref: '#/definitions/entity.CookedPlugin' + description: The plugin info + required: + - client_options + - enabled + - options_schema + - plugin + type: object + handler.ResponsePluginList: + properties: + plugins: + items: + $ref: '#/definitions/entity.CookedPlugin' + type: array + plugins_count: + type: integer + themes: + items: + $ref: '#/definitions/entity.CookedPlugin' + type: array + themes_count: + type: integer + required: + - plugins + - plugins_count + - themes + - themes_count + type: object + handler.ResponsePluginUpdate: + properties: + plugin: + $ref: '#/definitions/entity.CookedPlugin' + required: + - plugin + type: object handler.ResponseSettingGet: properties: envs: @@ -2396,6 +2562,318 @@ paths: summary: Increase Page Views (PV) tags: - Page + /plugin_registry/update: + post: + consumes: + - application/json + description: Update the plugin registry data + operationId: UpdatePluginRegistry + parameters: + - description: The options + in: body + name: options + required: true + schema: + $ref: '#/definitions/handler.ParamsPluginRegistryUpdate' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.Map' + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: Update Plugin Registry + tags: + - Plugin + /plugins: + get: + consumes: + - application/json + description: Get a list of plugins by some conditions + operationId: GetPlugins + parameters: + - description: Only installed plugins + in: query + name: only_installed + type: boolean + - description: Search keywords + in: query + name: search + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ResponsePluginList' + "403": + description: Forbidden + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: Get Plugin List + tags: + - Plugin + /plugins/{plugin_id}: + get: + consumes: + - application/json + description: Get a plugin info by ID + operationId: GetPlugin + parameters: + - description: The plugin ID + in: path + name: plugin_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ResponsePluginGet' + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: Get Plugin Info + tags: + - Plugin + put: + consumes: + - application/json + description: Update a plugin status by ID + operationId: UpdatePlugin + parameters: + - description: The plugin ID + in: path + name: plugin_id + required: true + type: string + - description: The plugin status + in: body + name: plugin + required: true + schema: + $ref: '#/definitions/handler.ParamsPluginUpdate' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ResponsePluginUpdate' + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: Update Plugin + tags: + - Plugin + /plugins/{plugin_id}/install: + post: + consumes: + - application/json + description: Install a plugin by ID + operationId: InstallPlugin + parameters: + - description: The plugin ID + in: path + name: plugin_id + required: true + type: string + - description: The options + in: body + name: options + required: true + schema: + $ref: '#/definitions/handler.ParamsPluginInstall' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.Map' + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: Install Plugin + tags: + - Plugin + /plugins/{plugin_id}/uninstall: + post: + consumes: + - application/json + description: Uninstall a plugin by ID + operationId: UninstallPlugin + parameters: + - description: The plugin ID + in: path + name: plugin_id + required: true + type: string + - description: The options + in: body + name: options + required: true + schema: + $ref: '#/definitions/handler.ParamsPluginUninstall' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.Map' + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: Uninstall Plugin + tags: + - Plugin + /plugins/{plugin_id}/upgrade: + post: + consumes: + - application/json + description: Upgrade a plugin by ID + operationId: UpgradePlugin + parameters: + - description: The plugin ID + in: path + name: plugin_id + required: true + type: string + - description: The options + in: body + name: options + required: true + schema: + $ref: '#/definitions/handler.ParamsPluginUpgrade' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.Map' + "400": + description: Bad Request + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/handler.Map' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: Upgrade Plugin + tags: + - Plugin /send_email: post: consumes: diff --git a/internal/config/base.go b/internal/config/base.go index 292207a3..cbefc8af 100644 --- a/internal/config/base.go +++ b/internal/config/base.go @@ -3,6 +3,7 @@ package config //go:generate go run ./meta/gen --format go --locale en --pkg config -o ./cache.go import ( + "cmp" "fmt" "os" "path" @@ -146,6 +147,9 @@ func (conf *Config) normalPatch() { log.Warn("[SocialLogin] config `auth.callback` is not set, now it is: ", strconv.Quote(callbackURL)) conf.Auth.Callback = callbackURL } + + // Default plugin registry URL + conf.Plugin.RegistryURL = cmp.Or(conf.Plugin.RegistryURL, "https://artalk.js.org/plugins") } // 多语言配置修补 diff --git a/internal/config/config.go b/internal/config/config.go index 42f9ada0..44cae20d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,6 +30,7 @@ type Config struct { ImgUpload ImgUploadConf `koanf:"img_upload" json:"img_upload"` // 图片上传 AdminNotify AdminNotifyConf `koanf:"admin_notify" json:"admin_notify"` // 其他通知方式 Auth AuthConf `koanf:"auth" json:"auth"` // Social Login + Plugin PluginConf `koanf:"plugin" json:"plugin"` // Plugin system Frontend map[string]interface{} `koanf:"frontend" json:"frontend"` // deprecated options @@ -455,3 +456,8 @@ type AuthConf struct { Domain string `koanf:"domain" json:"domain"` } `koanf:"auth0" json:"auth0"` } + +type PluginConf struct { + Enabled bool `koanf:"enabled" json:"enabled"` + RegistryURL string `koanf:"registry_url" json:"registry_url"` +} diff --git a/internal/dao/migrate.go b/internal/dao/migrate.go index b0bc38fa..f88baff6 100644 --- a/internal/dao/migrate.go +++ b/internal/dao/migrate.go @@ -18,7 +18,8 @@ func (dao *Dao) MigrateModels() { // Migrate the schema dao.DB().AutoMigrate(&entity.Site{}, &entity.Page{}, &entity.User{}, &entity.AuthIdentity{}, &entity.UserEmailVerify{}, - &entity.Comment{}, &entity.Notify{}, &entity.Vote{}) + &entity.Comment{}, &entity.Notify{}, &entity.Vote{}, + &entity.Plugin{}, &entity.PluginOption{}) // Delete all foreign key constraints // Leave relationship maintenance to the program and reduce the difficulty of database management. diff --git a/internal/entity/plugin.go b/internal/entity/plugin.go new file mode 100644 index 00000000..e4b4c2ed --- /dev/null +++ b/internal/entity/plugin.go @@ -0,0 +1,28 @@ +package entity + +import ( + "gorm.io/gorm" +) + +type PluginType string + +const ( + PluginTypePlugin PluginType = "plugin" + PluginTypeTheme PluginType = "theme" +) + +type Plugin struct { + gorm.Model + + PluginID string `gorm:"index"` + Name string + Type PluginType + Source string + Integrity string + Version string + Enabled bool +} + +func (n Plugin) IsEmpty() bool { + return n.ID == 0 +} diff --git a/internal/entity/plugin_cooked.go b/internal/entity/plugin_cooked.go new file mode 100644 index 00000000..4329aeff --- /dev/null +++ b/internal/entity/plugin_cooked.go @@ -0,0 +1,37 @@ +package entity + +type CookedPlugin struct { + PluginRegistryItem + + Installed bool `json:"installed"` + Enabled bool `json:"enabled"` + LocalVersion string `json:"local_version"` + UpgradeAvailable bool `json:"upgrade_available"` + Compatible bool `json:"compatible"` + CompatibleNotice string `json:"compatible_notice,omitempty" validate:"optional"` +} + +type PluginRegistryItem struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + AuthorName string `json:"author_name"` + AuthorLink string `json:"author_link"` + RepoName string `json:"repo_name"` + RepoLink string `json:"repo_link"` + NpmName string `json:"npm_name"` + Source string `json:"source"` + Integrity string `json:"integrity"` + OptionsSchema string `json:"options_schema"` + DonateLink string `json:"donate_link"` + Verified bool `json:"verified"` + Version string `json:"version"` + UpdatedAt string `json:"updated_at"` + MinArtalkVersion string `json:"min_artalk_version"` +} + +type PluginRegistryData struct { + Plugins []PluginRegistryItem `json:"plugins"` + Themes []PluginRegistryItem `json:"themes"` +} diff --git a/internal/entity/plugin_option.go b/internal/entity/plugin_option.go new file mode 100644 index 00000000..00e15068 --- /dev/null +++ b/internal/entity/plugin_option.go @@ -0,0 +1,21 @@ +package entity + +import ( + "gorm.io/gorm" +) + +const PluginID_RegistryData = "__artalk_registry_data__" +const PluginOptionName_OptionsSchema = "options_schema" +const PluginOptionName_ClientOptions = "client_options" + +type PluginOption struct { + gorm.Model + + PluginID string `gorm:"index"` + Name string + Value string +} + +func (n PluginOption) IsEmpty() bool { + return n.ID == 0 +} diff --git a/package.json b/package.json index 3e448f4c..4ba5df2b 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "test": "pnpm -F artalk test" }, "devDependencies": { - "@artalk/plugin-kit": "^1.0.7", + "@artalk/plugin-kit": "^1.0.9", "@eslint/compat": "^1.2.1", "@eslint/js": "^9.13.0", "@playwright/test": "^1.48.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86b338db..5bfedb72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: '@artalk/plugin-kit': - specifier: ^1.0.7 - version: 1.0.7(@types/node@22.7.7)(artalk@2.9.1(marked@14.1.3))(typescript@5.6.3)(vite@5.4.9(@types/node@22.7.7)(less@4.2.0)(sass@1.80.3)(terser@5.36.0)) + specifier: ^1.0.9 + version: link:ui/plugin-kit '@eslint/compat': specifier: ^1.2.1 version: 1.2.1(eslint@9.13.0) @@ -222,6 +222,9 @@ importers: pinia: specifier: ^2.2.4 version: 2.2.4(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)) + timeago.js: + specifier: ^4.0.2 + version: 4.0.2 typescript: specifier: ^5.6.3 version: 5.6.3 @@ -301,13 +304,13 @@ importers: dependencies: '@microsoft/api-extractor': specifier: ^7.47.11 - version: 7.47.11(@types/node@22.7.7) + version: 7.47.11(@types/node@18.19.58) picocolors: specifier: ^1.1.1 version: 1.1.1 typescript: specifier: '*' - version: 5.6.3 + version: 5.5.4 vite: specifier: '*' version: 5.4.9(@types/node@22.7.7)(less@4.2.0)(sass@1.80.3)(terser@5.36.0) @@ -317,10 +320,13 @@ importers: version: link:../artalk esbuild-plugin-raw: specifier: ^0.1.8 - version: 0.1.8(esbuild@0.24.0) + version: 0.1.8(esbuild@0.23.1) tsup: specifier: ^8.3.0 - version: 8.3.0(@microsoft/api-extractor@7.47.11(@types/node@22.7.7))(@swc/core@1.7.36(@swc/helpers@0.5.13))(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.3)(yaml@2.6.0) + version: 8.3.0(@microsoft/api-extractor@7.47.11(@types/node@18.19.58))(@swc/core@1.7.36(@swc/helpers@0.5.13))(postcss@8.4.47)(tsx@4.19.1)(typescript@5.5.4)(yaml@2.6.0) + typescript-json-schema: + specifier: ^0.65.1 + version: 0.65.1(@swc/core@1.7.36(@swc/helpers@0.5.13)) ui/plugin-lightbox: devDependencies: @@ -441,13 +447,6 @@ packages: artalk: ^2.9.1 katex: ^0.16.11 - '@artalk/plugin-kit@1.0.7': - resolution: {integrity: sha512-3ggnD0SCQm7b+UpdqOQNzPZdXQjNm5UQLO1iknipSkJ57Nj9E/aLBNSOuW3y6K6djTFY95IKTTJ8wD4ssDV+aQ==} - peerDependencies: - artalk: ^2.9.1 - typescript: '*' - vite: '*' - '@artalk/plugin-lightbox@0.2.4': resolution: {integrity: sha512-cYewbb2rkwofDqWq51MrMO/idbN7mcYTrIicsi8xpoRJdr7hwLzqM6ODih9cnbiS25l8+3PVYPTIn3g5WeqkYg==} peerDependencies: @@ -565,6 +564,10 @@ packages: react: '>=18' react-dom: '>=18' + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@csstools/css-parser-algorithms@3.0.2': resolution: {integrity: sha512-6tC/MnlEvs5suR4Ahef4YlBccJDHZuxGsAlxXmybWjZ5jPxlzLSMlRZ9mVHSRvlD+CmtE7+hJ+UQbfXrws/rUQ==} engines: {node: '>=18'} @@ -635,12 +638,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.24.0': - resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -653,12 +650,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.24.0': - resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -671,12 +662,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.24.0': - resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -689,12 +674,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.24.0': - resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -707,12 +686,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.24.0': - resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -725,12 +698,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.24.0': - resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -743,12 +710,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.24.0': - resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -761,12 +722,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.24.0': - resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -779,12 +734,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.24.0': - resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -797,12 +746,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.24.0': - resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -815,12 +758,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.24.0': - resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -833,12 +770,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.24.0': - resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -851,12 +782,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.24.0': - resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -869,12 +794,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.24.0': - resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -887,12 +806,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.24.0': - resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -905,12 +818,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.24.0': - resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -923,12 +830,6 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.24.0': - resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -941,24 +842,12 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.24.0': - resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/openbsd-arm64@0.23.1': resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.24.0': - resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -971,12 +860,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.24.0': - resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -989,12 +872,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.24.0': - resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -1007,12 +884,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.24.0': - resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -1025,12 +896,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.24.0': - resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -1043,12 +908,6 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.24.0': - resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1190,6 +1049,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@mdn/browser-compat-data@5.6.8': resolution: {integrity: sha512-ueuvAVqVaPF+bEclXAH/P+qfUJ2IMJDaeUS+j8HC/maWTdV5tcm2eTvlGdXRLiq0rJAZk0Zy22i51rzW0B2izQ==} @@ -1618,6 +1480,18 @@ packages: '@tanstack/virtual-core@3.10.8': resolution: {integrity: sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==} + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -1663,6 +1537,9 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node@18.19.58': + resolution: {integrity: sha512-2ryJttbOAWCYuZMdk4rmZZ6oqE+GSL5LxbaTVe4PCs0FUrHObZZAQL4ihMw9/cH1Pn8lSQ9TXVhsM4LrnfZ0aA==} + '@types/node@22.7.7': resolution: {integrity: sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==} @@ -1930,6 +1807,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + acorn@8.13.0: resolution: {integrity: sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==} engines: {node: '>=0.4.0'} @@ -2005,6 +1886,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -2051,11 +1935,6 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} - artalk@2.9.1: - resolution: {integrity: sha512-IFo9XqWDalsHy8BsmMA5SSB9bozBa/sBhTm/+O5KwA6DnC95lFKv7C6ScMx/Xa4ue5qSQ7VV5vxRgCh/raohkQ==} - peerDependencies: - marked: ^14.1.0 - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2209,6 +2088,10 @@ packages: cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -2295,6 +2178,9 @@ packages: typescript: optional: true + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -2424,6 +2310,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2550,11 +2440,6 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.24.0: - resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} - engines: {node: '>=18'} - hasBin: true - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3507,6 +3392,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} @@ -3833,6 +3721,9 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-equal@1.2.5: + resolution: {integrity: sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4184,6 +4075,10 @@ packages: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -4509,6 +4404,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + timeago.js@4.0.2: + resolution: {integrity: sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -4579,6 +4477,20 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsconfck@3.1.4: resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} @@ -4666,11 +4578,20 @@ packages: typescript: optional: true + typescript-json-schema@0.65.1: + resolution: {integrity: sha512-tuGH7ff2jPaUYi6as3lHyHcKpSmXIqN7/mu50x3HlYn0EHzLpmt3nplZ7EuhUkO0eqDRc9GqWNkfjgBPIS9kxg==} + hasBin: true + typescript@5.4.2: resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} engines: {node: '>=14.17'} hasBin: true + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.6.3: resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} @@ -4693,6 +4614,9 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} @@ -4805,6 +4729,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + validate-html-nesting@1.2.2: resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} @@ -5183,10 +5110,22 @@ packages: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yargs@17.0.1: resolution: {integrity: sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==} engines: {node: '>=12'} + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -5338,16 +5277,6 @@ snapshots: artalk: link:ui/artalk katex: 0.16.11 - '@artalk/plugin-kit@1.0.7(@types/node@22.7.7)(artalk@2.9.1(marked@14.1.3))(typescript@5.6.3)(vite@5.4.9(@types/node@22.7.7)(less@4.2.0)(sass@1.80.3)(terser@5.36.0))': - dependencies: - '@microsoft/api-extractor': 7.47.11(@types/node@22.7.7) - artalk: 2.9.1(marked@14.1.3) - picocolors: 1.1.1 - typescript: 5.6.3 - vite: 5.4.9(@types/node@22.7.7)(less@4.2.0)(sass@1.80.3)(terser@5.36.0) - transitivePeerDependencies: - - '@types/node' - '@artalk/plugin-lightbox@0.2.4(artalk@ui+artalk)(fancybox@3.0.1)(lightbox2@2.11.5)(lightgallery@2.7.2)(photoswipe@5.4.4)': dependencies: artalk: link:ui/artalk @@ -5497,6 +5426,10 @@ snapshots: react-is: 18.3.1 react-shallow-renderer: 16.15.0(react@18.3.1) + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2)': dependencies: '@csstools/css-tokenizer': 3.0.2 @@ -5560,213 +5493,141 @@ snapshots: '@esbuild/aix-ppc64@0.23.1': optional: true - '@esbuild/aix-ppc64@0.24.0': - optional: true - '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.23.1': optional: true - '@esbuild/android-arm64@0.24.0': - optional: true - '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.23.1': optional: true - '@esbuild/android-arm@0.24.0': - optional: true - '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.23.1': optional: true - '@esbuild/android-x64@0.24.0': - optional: true - '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.23.1': optional: true - '@esbuild/darwin-arm64@0.24.0': - optional: true - '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.23.1': optional: true - '@esbuild/darwin-x64@0.24.0': - optional: true - '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.23.1': optional: true - '@esbuild/freebsd-arm64@0.24.0': - optional: true - '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.23.1': optional: true - '@esbuild/freebsd-x64@0.24.0': - optional: true - '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.23.1': optional: true - '@esbuild/linux-arm64@0.24.0': - optional: true - '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.23.1': optional: true - '@esbuild/linux-arm@0.24.0': - optional: true - '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.23.1': optional: true - '@esbuild/linux-ia32@0.24.0': - optional: true - '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.23.1': optional: true - '@esbuild/linux-loong64@0.24.0': - optional: true - '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.23.1': optional: true - '@esbuild/linux-mips64el@0.24.0': - optional: true - '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.23.1': optional: true - '@esbuild/linux-ppc64@0.24.0': - optional: true - '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.23.1': optional: true - '@esbuild/linux-riscv64@0.24.0': - optional: true - '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.23.1': optional: true - '@esbuild/linux-s390x@0.24.0': - optional: true - '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.23.1': optional: true - '@esbuild/linux-x64@0.24.0': - optional: true - '@esbuild/netbsd-x64@0.21.5': optional: true '@esbuild/netbsd-x64@0.23.1': optional: true - '@esbuild/netbsd-x64@0.24.0': - optional: true - '@esbuild/openbsd-arm64@0.23.1': optional: true - '@esbuild/openbsd-arm64@0.24.0': - optional: true - '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.23.1': optional: true - '@esbuild/openbsd-x64@0.24.0': - optional: true - '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.23.1': optional: true - '@esbuild/sunos-x64@0.24.0': - optional: true - '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.23.1': optional: true - '@esbuild/win32-arm64@0.24.0': - optional: true - '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.23.1': optional: true - '@esbuild/win32-ia32@0.24.0': - optional: true - '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.23.1': optional: true - '@esbuild/win32-x64@0.24.0': - optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@9.13.0)': dependencies: eslint: 9.13.0 @@ -5912,6 +5773,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@mdn/browser-compat-data@5.6.8': {} '@microsoft/api-extractor-model@7.29.6(@types/node@22.7.7)': @@ -5922,6 +5788,14 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@microsoft/api-extractor-model@7.29.8(@types/node@18.19.58)': + dependencies: + '@microsoft/tsdoc': 0.15.0 + '@microsoft/tsdoc-config': 0.17.0 + '@rushstack/node-core-library': 5.9.0(@types/node@18.19.58) + transitivePeerDependencies: + - '@types/node' + '@microsoft/api-extractor-model@7.29.8(@types/node@22.7.7)': dependencies: '@microsoft/tsdoc': 0.15.0 @@ -5929,6 +5803,25 @@ snapshots: '@rushstack/node-core-library': 5.9.0(@types/node@22.7.7) transitivePeerDependencies: - '@types/node' + optional: true + + '@microsoft/api-extractor@7.47.11(@types/node@18.19.58)': + dependencies: + '@microsoft/api-extractor-model': 7.29.8(@types/node@18.19.58) + '@microsoft/tsdoc': 0.15.0 + '@microsoft/tsdoc-config': 0.17.0 + '@rushstack/node-core-library': 5.9.0(@types/node@18.19.58) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.14.2(@types/node@18.19.58) + '@rushstack/ts-command-line': 4.23.0(@types/node@18.19.58) + lodash: 4.17.21 + minimatch: 3.0.8 + resolve: 1.22.8 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.4.2 + transitivePeerDependencies: + - '@types/node' '@microsoft/api-extractor@7.47.11(@types/node@22.7.7)': dependencies: @@ -5947,6 +5840,7 @@ snapshots: typescript: 5.4.2 transitivePeerDependencies: - '@types/node' + optional: true '@microsoft/api-extractor@7.47.7(@types/node@22.7.7)': dependencies: @@ -6297,6 +6191,19 @@ snapshots: optionalDependencies: '@types/node': 22.7.7 + '@rushstack/node-core-library@5.9.0(@types/node@18.19.58)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 7.0.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.8 + semver: 7.5.4 + optionalDependencies: + '@types/node': 18.19.58 + '@rushstack/node-core-library@5.9.0(@types/node@22.7.7)': dependencies: ajv: 8.13.0 @@ -6309,6 +6216,7 @@ snapshots: semver: 7.5.4 optionalDependencies: '@types/node': 22.7.7 + optional: true '@rushstack/rig-package@0.5.3': dependencies: @@ -6322,12 +6230,20 @@ snapshots: optionalDependencies: '@types/node': 22.7.7 + '@rushstack/terminal@0.14.2(@types/node@18.19.58)': + dependencies: + '@rushstack/node-core-library': 5.9.0(@types/node@18.19.58) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 18.19.58 + '@rushstack/terminal@0.14.2(@types/node@22.7.7)': dependencies: '@rushstack/node-core-library': 5.9.0(@types/node@22.7.7) supports-color: 8.1.1 optionalDependencies: '@types/node': 22.7.7 + optional: true '@rushstack/ts-command-line@4.22.6(@types/node@22.7.7)': dependencies: @@ -6338,6 +6254,15 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@rushstack/ts-command-line@4.23.0(@types/node@18.19.58)': + dependencies: + '@rushstack/terminal': 0.14.2(@types/node@18.19.58) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@rushstack/ts-command-line@4.23.0(@types/node@22.7.7)': dependencies: '@rushstack/terminal': 0.14.2(@types/node@22.7.7) @@ -6346,6 +6271,7 @@ snapshots: string-argv: 0.3.2 transitivePeerDependencies: - '@types/node' + optional: true '@sec-ant/readable-stream@0.4.1': {} @@ -6451,6 +6377,14 @@ snapshots: '@tanstack/virtual-core@3.10.8': {} + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@types/argparse@1.0.38': {} '@types/babel__core@7.20.5': @@ -6502,6 +6436,10 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/node@18.19.58': + dependencies: + undici-types: 5.26.5 + '@types/node@22.7.7': dependencies: undici-types: 6.19.8 @@ -6844,6 +6782,10 @@ snapshots: dependencies: acorn: 8.13.0 + acorn-walk@8.3.4: + dependencies: + acorn: 8.13.0 + acorn@8.13.0: {} agent-base@7.1.1: @@ -6934,6 +6876,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arg@4.1.3: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -7018,10 +6962,6 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 - artalk@2.9.1(marked@14.1.3): - dependencies: - marked: 14.1.3 - assertion-error@2.0.1: {} assignment@2.0.0: {} @@ -7205,6 +7145,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} color-convert@1.9.3: @@ -7272,6 +7218,8 @@ snapshots: typescript: 5.6.3 optional: true + create-require@1.1.1: {} + cross-env@7.0.3: dependencies: cross-spawn: 7.0.3 @@ -7388,6 +7336,8 @@ snapshots: diff-sequences@29.6.3: {} + diff@4.0.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -7584,9 +7534,9 @@ snapshots: es6-promise@3.3.1: {} - esbuild-plugin-raw@0.1.8(esbuild@0.24.0): + esbuild-plugin-raw@0.1.8(esbuild@0.23.1): dependencies: - esbuild: 0.24.0 + esbuild: 0.23.1 esbuild@0.21.5: optionalDependencies: @@ -7641,33 +7591,6 @@ snapshots: '@esbuild/win32-ia32': 0.23.1 '@esbuild/win32-x64': 0.23.1 - esbuild@0.24.0: - optionalDependencies: - '@esbuild/aix-ppc64': 0.24.0 - '@esbuild/android-arm': 0.24.0 - '@esbuild/android-arm64': 0.24.0 - '@esbuild/android-x64': 0.24.0 - '@esbuild/darwin-arm64': 0.24.0 - '@esbuild/darwin-x64': 0.24.0 - '@esbuild/freebsd-arm64': 0.24.0 - '@esbuild/freebsd-x64': 0.24.0 - '@esbuild/linux-arm': 0.24.0 - '@esbuild/linux-arm64': 0.24.0 - '@esbuild/linux-ia32': 0.24.0 - '@esbuild/linux-loong64': 0.24.0 - '@esbuild/linux-mips64el': 0.24.0 - '@esbuild/linux-ppc64': 0.24.0 - '@esbuild/linux-riscv64': 0.24.0 - '@esbuild/linux-s390x': 0.24.0 - '@esbuild/linux-x64': 0.24.0 - '@esbuild/netbsd-x64': 0.24.0 - '@esbuild/openbsd-arm64': 0.24.0 - '@esbuild/openbsd-x64': 0.24.0 - '@esbuild/sunos-x64': 0.24.0 - '@esbuild/win32-arm64': 0.24.0 - '@esbuild/win32-ia32': 0.24.0 - '@esbuild/win32-x64': 0.24.0 - escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -8737,6 +8660,8 @@ snapshots: dependencies: semver: 7.6.3 + make-error@1.3.6: {} + mark.js@8.11.1: {} markdown-it@14.1.0: @@ -9075,6 +9000,8 @@ snapshots: path-browserify@1.0.1: {} + path-equal@1.2.5: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -9418,6 +9345,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.1.4 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sass@1.80.3: @@ -9841,6 +9770,8 @@ snapshots: dependencies: any-promise: 1.3.0 + timeago.js@4.0.2: {} + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -9894,6 +9825,26 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-node@10.9.2(@swc/core@1.7.36(@swc/helpers@0.5.13))(@types/node@18.19.58)(typescript@5.5.4): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.19.58 + acorn: 8.13.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.5.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.7.36(@swc/helpers@0.5.13) + tsconfck@3.1.4(typescript@5.6.3): optionalDependencies: typescript: 5.6.3 @@ -9910,6 +9861,35 @@ snapshots: tslib@2.8.0: {} + tsup@8.3.0(@microsoft/api-extractor@7.47.11(@types/node@18.19.58))(@swc/core@1.7.36(@swc/helpers@0.5.13))(postcss@8.4.47)(tsx@4.19.1)(typescript@5.5.4)(yaml@2.6.0): + dependencies: + bundle-require: 5.0.0(esbuild@0.23.1) + cac: 6.7.14 + chokidar: 3.6.0 + consola: 3.2.3 + debug: 4.3.7 + esbuild: 0.23.1 + execa: 5.1.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.4.47)(tsx@4.19.1)(yaml@2.6.0) + resolve-from: 5.0.0 + rollup: 4.24.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyglobby: 0.2.9 + tree-kill: 1.2.2 + optionalDependencies: + '@microsoft/api-extractor': 7.47.11(@types/node@18.19.58) + '@swc/core': 1.7.36(@swc/helpers@0.5.13) + postcss: 8.4.47 + typescript: 5.5.4 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsup@8.3.0(@microsoft/api-extractor@7.47.11(@types/node@22.7.7))(@swc/core@1.7.36(@swc/helpers@0.5.13))(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.3)(yaml@2.6.0): dependencies: bundle-require: 5.0.0(esbuild@0.23.1) @@ -10006,8 +9986,24 @@ snapshots: - eslint - supports-color + typescript-json-schema@0.65.1(@swc/core@1.7.36(@swc/helpers@0.5.13)): + dependencies: + '@types/json-schema': 7.0.15 + '@types/node': 18.19.58 + glob: 7.2.3 + path-equal: 1.2.5 + safe-stable-stringify: 2.5.0 + ts-node: 10.9.2(@swc/core@1.7.36(@swc/helpers@0.5.13))(@types/node@18.19.58)(typescript@5.5.4) + typescript: 5.5.4 + yargs: 17.7.2 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + typescript@5.4.2: {} + typescript@5.5.4: {} + typescript@5.6.3: {} uc.micro@2.1.0: {} @@ -10026,6 +10022,8 @@ snapshots: uncrypto@0.1.3: {} + undici-types@5.26.5: {} + undici-types@6.19.8: {} undici@6.20.1: {} @@ -10171,6 +10169,8 @@ snapshots: util-deprecate@1.0.2: {} + v8-compile-cache-lib@3.0.1: {} + validate-html-nesting@1.2.2: {} vfile-message@4.0.2: @@ -10579,6 +10579,8 @@ snapshots: yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs@17.0.1: dependencies: cliui: 7.0.4 @@ -10589,6 +10591,18 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + yocto-queue@0.1.0: {} yoctocolors@2.1.1: {} diff --git a/public/artalk-plugin-options.schema.json b/public/artalk-plugin-options.schema.json new file mode 100644 index 00000000..f6599947 --- /dev/null +++ b/public/artalk-plugin-options.schema.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "config": { + "description": "Config for all lightbox plugins" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/public/registry.json b/public/registry.json new file mode 100644 index 00000000..9058ad36 --- /dev/null +++ b/public/registry.json @@ -0,0 +1,43 @@ +{ + "plugins": [ + { + "type": "plugin", + "id": "artalk-plugin-katex", + "name": "KaTeX Integration", + "description": "Render math formulas with KaTeX", + "author_name": "Artalk", + "author_link": "https://github.com/ArtalkJS/Artalk", + "donate_link": "https://buymeacoffee.com/artalk", + "repo_name": "ArtalkJS/Artalk", + "repo_link": "https://github.com/ArtalkJS/Artalk", + "npm_name": "@artalk/plugin-katex", + "verified": true, + "version": "0.2.4", + "source": "https://cdn.jsdelivr.net/npm/@artalk/plugin-katex@0.2.4/dist/artalk-plugin-katex.js", + "integrity": "sha512-MNVJlPUTE82JkhJ2Pb7HgoMiPOqXJQKjzAbnblEp/cABl1uhhTCt+3SBMaGKsVObrsIQi81VvXi5sglcWAVCKw==", + "options_schema": "http://localhost:23366/artalk-plugin-options.schema.json", + "updated_at": "2024-10-05T10:57:07.434Z", + "min_artalk_version": "2.9.1" + }, + { + "type": "plugin", + "id": "artalk-plugin-auth", + "name": "Auth Plugin", + "description": "3rd party social login support", + "author_name": "Artalk", + "author_link": "https://github.com/ArtalkJS/Artalk", + "donate_link": "https://buymeacoffee.com/artalk", + "repo_name": "ArtalkJS/Artalk", + "repo_link": "https://github.com/ArtalkJS/Artalk", + "npm_name": "@artalk/plugin-auth", + "verified": true, + "version": "0.0.1", + "source": "https://cdn.jsdelivr.net/npm/@artalk/plugin-auth@0.0.1/dist/artalk-plugin-auth.js", + "integrity": "sha512-Jy9+gfQ1CFsqQjQMhNflhZaKgliGfd2kLNhuHoJufNjFdLtKC6egEjnapoK5xITGCo6SU6Rim5DaUvZXzMdt0g==", + "options_schema": "", + "updated_at": "2024-10-05T10:59:06.265Z", + "min_artalk_version": "2.9.1" + } + ], + "themes": [] +} diff --git a/server/common/conf.go b/server/common/conf.go index 649d2a4e..c4c6044f 100644 --- a/server/common/conf.go +++ b/server/common/conf.go @@ -7,6 +7,7 @@ import ( "github.com/artalkjs/artalk/v2/internal/config" "github.com/artalkjs/artalk/v2/internal/core" + "github.com/artalkjs/artalk/v2/internal/entity" "github.com/artalkjs/artalk/v2/internal/utils" "github.com/artalkjs/artalk/v2/server/middleware" "github.com/gofiber/fiber/v2" @@ -29,6 +30,7 @@ func GetApiVersionDataMap() ApiVersionData { type ConfData struct { FrontendConf Map `json:"frontend_conf"` + Plugins []PluginItem `json:"plugins"` Version ApiVersionData `json:"version"` } @@ -52,31 +54,27 @@ func GetApiPublicConfDataMap(app *core.App, c *fiber.Ctx) ConfData { frontendConf["locale"] = app.Conf().Locale } - if _, ok := frontendConf["pluginURLs"].([]any); !ok { - frontendConf["pluginURLs"] = []any{} - } - pluginURLs := frontendConf["pluginURLs"].([]any) - - if app.Conf().Auth.Enabled { - pluginURLs = append(pluginURLs, "dist/plugins/artalk-plugin-auth.js") - } - - if !slices.Contains([]string{"en", "zh-CN", ""}, app.Conf().Locale) { - pluginURLs = append(pluginURLs, fmt.Sprintf("dist/i18n/%s.js", app.Conf().Locale)) - } + customPluginURLsRaw := frontendConf["pluginURLs"].([]any) + customPluginURLs := lo.Map(customPluginURLsRaw, func(url any, _ int) string { + u, ok := url.(string) + if !ok { + return "" + } + return u + }) + frontendConf["pluginURLs"] = []any{} // Cleared, only for forward compatibility - frontendConf["pluginURLs"] = handlePluginURLs(app, - lo.Map(pluginURLs, func(u any, _ int) string { - return strings.TrimSpace(fmt.Sprintf("%v", u)) - })) + // Plugin system in Artalk v2.10.0+ + // TODO: this is a db query, should be cached return ConfData{ FrontendConf: frontendConf, + Plugins: getEnabledPlugins(app, customPluginURLs), Version: GetApiVersionDataMap(), } } -func handlePluginURLs(app *core.App, urls []string) []string { +func handleCustomPluginURLs(app *core.App, urls []string) []string { return utils.RemoveDuplicates(lo.Filter(urls, func(u string, _ int) bool { if strings.TrimSpace(u) == "" { return false @@ -90,3 +88,53 @@ func handlePluginURLs(app *core.App, urls []string) []string { return false })) } + +type PluginItem struct { + Source string `json:"source"` + Type entity.PluginType `json:"type"` + Integrity string `json:"integrity" validate:"optional"` + Options string `json:"options" validate:"optional"` +} + +func getEnabledPlugins(app *core.App, customURLs []string) []PluginItem { + var plugins []PluginItem + + // User plugins + if app.Conf().Plugin.Enabled { + var dbPlugins []entity.Plugin + app.Dao().DB().Where(&entity.Plugin{Enabled: true}).Find(&dbPlugins) + + plugins = append(plugins, lo.Map(dbPlugins, func(plugin entity.Plugin, _ int) PluginItem { + return PluginItem{ + Source: plugin.Source, + Integrity: plugin.Integrity, + Type: plugin.Type, + } + })...) + + // Custom plugins + for _, url := range handleCustomPluginURLs(app, customURLs) { + plugins = append(plugins, PluginItem{ + Source: url, + Type: entity.PluginTypePlugin, + }) + } + } + + // Import internal plugins + if app.Conf().Auth.Enabled { + plugins = append(plugins, PluginItem{ + Source: "dist/plugins/artalk-plugin-auth.js", + Type: entity.PluginTypePlugin, + }) + } + + if !slices.Contains([]string{"en", "zh-CN", ""}, app.Conf().Locale) { + plugins = append(plugins, PluginItem{ + Source: fmt.Sprintf("dist/i18n/%s.js", app.Conf().Locale), + Type: entity.PluginTypePlugin, + }) + } + + return plugins +} diff --git a/server/handler/plugin_get.go b/server/handler/plugin_get.go new file mode 100644 index 00000000..50631fb9 --- /dev/null +++ b/server/handler/plugin_get.go @@ -0,0 +1,147 @@ +package handler + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/artalkjs/artalk/v2/internal/config" + "github.com/artalkjs/artalk/v2/internal/core" + "github.com/artalkjs/artalk/v2/internal/dao" + "github.com/artalkjs/artalk/v2/internal/entity" + "github.com/artalkjs/artalk/v2/server/common" + "github.com/blang/semver" + "github.com/gofiber/fiber/v2" + "github.com/samber/lo" +) + +type ResponsePluginGet struct { + Enabled bool `json:"enabled"` // The plugin enabled status + ClientOptions string `json:"client_options"` // The plugin client options (JSON string) + OptionsSchema string `json:"options_schema"` // The plugin options schema (JSON string) + Plugin entity.CookedPlugin `json:"plugin"` // The plugin info +} + +// @Id GetPlugin +// @Summary Get Plugin Info +// @Description Get a plugin info by ID +// @Tags Plugin +// @Security ApiKeyAuth +// @Param plugin_id path string true "The plugin ID" +// @Accept json +// @Produce json +// @Success 200 {object} ResponsePluginGet +// @Failure 400 {object} Map{msg=string} +// @Failure 500 {object} Map{msg=string} +// @Router /plugins/{plugin_id} [get] +func PluginGet(app *core.App, router fiber.Router) { + router.Get("/plugins/:plugin_id", common.AdminGuard(app, func(c *fiber.Ctx) error { + pluginID := c.Params("plugin_id") + + // Get Installed Data + var installed entity.Plugin + app.Dao().DB().Where(&entity.Plugin{PluginID: pluginID}).First(&installed) + + // If Plugin Installed + clientOptionsJSON := "" + optionsSchemaJSON := "" + if !installed.IsEmpty() { + // Get Options + clientOptionsJSON = getPluginOptionRecord(app.Dao(), pluginID, entity.PluginOptionName_ClientOptions) + + // Get Options Schema + optionsSchemaJSON = getPluginOptionRecord(app.Dao(), pluginID, entity.PluginOptionName_OptionsSchema) + } + + // Get Registry Data + registryPlugin := findRegistryPlugin(pluginID, app.Dao()) + + cookedPlugin := cookPlugin(registryPlugin, &installed) + + return common.RespData(c, ResponsePluginGet{ + Enabled: installed.Enabled, + ClientOptions: clientOptionsJSON, + OptionsSchema: optionsSchemaJSON, + Plugin: cookedPlugin, + }) + })) +} + +func getPluginRegistryCache(dao *dao.Dao) (entity.PluginRegistryData, error) { + var data entity.PluginRegistryData + + var record entity.PluginOption + dao.DB().Where(&entity.PluginOption{ + PluginID: entity.PluginID_RegistryData, + }).First(&record) + + if !record.IsEmpty() && record.Value != "" { + if err := json.Unmarshal([]byte(record.Value), &data); err != nil { + return entity.PluginRegistryData{}, err + } + } + return data, nil +} + +func findRegistryPlugin(pluginID string, dao *dao.Dao) *entity.PluginRegistryItem { + var registryPlugin *entity.PluginRegistryItem // if exists, the `registryPlugin` will be not nil + if registryJSON, err := getPluginRegistryCache(dao); err == nil { + registryPlugin = findPluginInRegistryList(pluginID, registryJSON.Plugins) // Find in Plugins + if registryPlugin == nil { + registryPlugin = findPluginInRegistryList(pluginID, registryJSON.Themes) // Find in Themes + } + } + return registryPlugin +} + +func findPluginInRegistryList(pluginID string, registryItems []entity.PluginRegistryItem) *entity.PluginRegistryItem { + var found *entity.PluginRegistryItem + if rp, ok := lo.Find(registryItems, func(item entity.PluginRegistryItem) bool { + return item.ID == pluginID + }); ok { + found = &rp + } + return found +} + +func checkPluginInstalled(installed []entity.Plugin, pluginID string) (entity.Plugin, bool) { + return lo.Find(installed, func(plugin entity.Plugin) bool { return plugin.PluginID == pluginID }) +} + +func cookPlugin(registryPlugin *entity.PluginRegistryItem, installedPlugin *entity.Plugin) entity.CookedPlugin { + if registryPlugin == nil && installedPlugin == nil { + return entity.CookedPlugin{} + } + + plugin := entity.CookedPlugin{ + Installed: false, + Enabled: false, + } + + // Registry Data exists + if registryPlugin != nil { + plugin.PluginRegistryItem = *registryPlugin + + // Check compatibility + plugin.Compatible = registryPlugin.MinArtalkVersion == "" || !semver.MustParse(registryPlugin.MinArtalkVersion).GT(semver.MustParse(strings.TrimPrefix(config.Version, "v"))) + plugin.CompatibleNotice = lo.If(!plugin.Compatible, fmt.Sprintf("The plugin required at least Artalk v%s.", registryPlugin.MinArtalkVersion)).Else("") + } + + // Installed Data exists + if installedPlugin != nil && !installedPlugin.IsEmpty() { + plugin.Installed = true + plugin.Enabled = installedPlugin.Enabled + + // Check upgrade + plugin.UpgradeAvailable = semver.MustParse(registryPlugin.Version).GT(semver.MustParse(installedPlugin.Version)) + plugin.LocalVersion = installedPlugin.Version + } + + return plugin +} + +func getPluginOptionRecord(dao *dao.Dao, pluginID, optionName string) string { + var record entity.PluginOption + dao.DB().Where(&entity.PluginOption{PluginID: pluginID, Name: optionName}).First(&record) + return record.Value +} diff --git a/server/handler/plugin_install.go b/server/handler/plugin_install.go new file mode 100644 index 00000000..fd361b0c --- /dev/null +++ b/server/handler/plugin_install.go @@ -0,0 +1,142 @@ +package handler + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/artalkjs/artalk/v2/internal/core" + "github.com/artalkjs/artalk/v2/internal/entity" + "github.com/artalkjs/artalk/v2/internal/log" + "github.com/artalkjs/artalk/v2/server/common" + "github.com/blang/semver" + "github.com/gofiber/fiber/v2" +) + +type ParamsPluginInstall struct { +} + +// @Id InstallPlugin +// @Summary Install Plugin +// @Description Install a plugin by ID +// @Tags Plugin +// @Security ApiKeyAuth +// @Param plugin_id path string true "The plugin ID" +// @Param options body ParamsPluginInstall true "The options" +// @Accept json +// @Produce json +// @Success 200 {object} Map{} +// @Failure 400 {object} Map{msg=string} +// @Failure 500 {object} Map{msg=string} +// @Router /plugins/{plugin_id}/install [post] +func PluginInstall(app *core.App, router fiber.Router) { + router.Post("/plugins/:plugin_id/install", installPlugin(app, InstallPluginTypeInstall)) +} + +type InstallPluginType string + +const ( + InstallPluginTypeInstall InstallPluginType = "install" + InstallPluginTypeUpgrade InstallPluginType = "upgrade" +) + +func installPlugin(app *core.App, mode InstallPluginType) func(*fiber.Ctx) error { + return common.AdminGuard(app, func(c *fiber.Ctx) error { + isUpgradeMode := mode == InstallPluginTypeUpgrade + pluginID := c.Params("plugin_id") + + // Check had installed + var installed entity.Plugin + app.Dao().DB().Where(&entity.Plugin{PluginID: pluginID}).First(&installed) + + // Find Registry Plugins + plugin := findRegistryPlugin(pluginID, app.Dao()) + if plugin == nil { + return common.RespError(c, 400, "Plugin not found.") + } + + if !isUpgradeMode { + if !installed.IsEmpty() { + return common.RespError(c, 400, "Plugin already installed.") + } + + // Install Plugin + app.Dao().DB().Save(&entity.Plugin{ + PluginID: plugin.ID, + Name: plugin.Name, + Type: entity.PluginType(plugin.Type), + Source: plugin.Source, + Integrity: plugin.Integrity, + Version: plugin.Version, + Enabled: true, + }) + + downloadRemoteOptionsSchema(app, plugin.ID, plugin.OptionsSchema) + } else { + if installed.IsEmpty() { + return common.RespError(c, 400, "Plugin not installed.") + } + + // Upgrade Plugin + if semver.MustParse(plugin.Version).GT(semver.MustParse(installed.Version)) { + installed.Version = plugin.Version + installed.Source = plugin.Source + installed.Integrity = plugin.Integrity + app.Dao().DB().Save(&installed) + + downloadRemoteOptionsSchema(app, plugin.ID, plugin.OptionsSchema) + } else { + return common.RespError(c, 400, "No new version available.") + } + } + + return common.RespSuccess(c) + }) +} + +// Download remote options schema (artalk-plugin-options.schema.json) +// Save to database table plugin_options, (plugin_id, name=options_schema, value=JSON) +func downloadRemoteOptionsSchema(app *core.App, pluginID string, schemaURL string) { + if schemaURL == "" { + return + } + + // Download from url + resp, err := http.Get(schemaURL) + if err != nil || resp.StatusCode != 200 { + log.Error("[PluginOptionsSchema] Failed to get options schema from '", schemaURL, "': ", resp.StatusCode, " ", err) + return + } + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Error("[PluginOptionsSchema] Failed to read body from '", schemaURL, "': ", err) + return + } + + // Check is json valid + if err := json.Unmarshal(body, &map[string]any{}); err != nil { + log.Error("[PluginOptionsSchema] Options schema '", schemaURL, "' is not valid JSON: ", err) + return + } + + // Find Plugin Options Schema + var record entity.PluginOption + app.Dao().DB().Where(&entity.PluginOption{ + PluginID: pluginID, + Name: entity.PluginOptionName_OptionsSchema, + }).First(&record) + + if record.IsEmpty() { + // Create Plugin Options Schema + record = entity.PluginOption{ + PluginID: pluginID, + Name: entity.PluginOptionName_OptionsSchema, + Value: string(body), + } + } else { + // Update Plugin Options Schema + record.Value = string(body) + } + + app.Dao().DB().Save(&record) +} diff --git a/server/handler/plugin_list.go b/server/handler/plugin_list.go new file mode 100644 index 00000000..0f1e9495 --- /dev/null +++ b/server/handler/plugin_list.go @@ -0,0 +1,134 @@ +package handler + +import ( + "slices" + "strings" + + "github.com/artalkjs/artalk/v2/internal/core" + "github.com/artalkjs/artalk/v2/internal/entity" + "github.com/artalkjs/artalk/v2/internal/log" + "github.com/artalkjs/artalk/v2/server/common" + "github.com/gofiber/fiber/v2" + "github.com/samber/lo" +) + +type ParamsPluginList struct { + Search string `query:"search" json:"search" validate:"optional"` // Search keywords + OnlyInstalled bool `query:"only_installed" json:"only_installed" validate:"optional"` // Only installed plugins +} + +type ResponsePluginList struct { + Plugins []entity.CookedPlugin `json:"plugins"` + Themes []entity.CookedPlugin `json:"themes"` + PluginsCount int64 `json:"plugins_count"` + ThemesCount int64 `json:"themes_count"` +} + +// @Id GetPlugins +// @Summary Get Plugin List +// @Description Get a list of plugins by some conditions +// @Tags Plugin +// @Param options query ParamsPluginList true "The options" +// @Security ApiKeyAuth +// @Accept json +// @Produce json +// @Success 200 {object} ResponsePluginList +// @Failure 403 {object} Map{msg=string} +// @Router /plugins [get] +func PluginList(app *core.App, router fiber.Router) { + router.Get("/plugins", common.AdminGuard(app, func(c *fiber.Ctx) error { + var p ParamsPluginList + if isOK, resp := common.ParamsDecode(c, &p); !isOK { + return resp + } + + // Find Installed Plugins + var installed []entity.Plugin + app.Dao().DB().Model(&entity.Plugin{}).Order("created_at DESC").Find(&installed) + + // Find Registry Plugins + registry, err := getPluginRegistryCache(app.Dao()) + if err != nil { + log.Error("[PluginList] Failed to get registry data: ", err) + return common.RespError(c, 500, "Failed to get registry data.") + } + + plugins := mergePluginsWithInstallInfo(registry.Plugins, installed) + themes := mergePluginsWithInstallInfo(registry.Themes, installed) + + // Supply local plugins that are not in the registry + extendPluginsOutRegistry(plugins, installed) + extendPluginsOutRegistry(themes, installed) + + // Filter + if p.Search != "" { + filterFn := func(plugin entity.CookedPlugin, index int) bool { + return strings.Contains(strings.ToLower(plugin.Name), strings.ToLower(p.Search)) || strings.EqualFold(plugin.ID, p.Search) + } + plugins = lo.Filter(plugins, filterFn) + themes = lo.Filter(themes, filterFn) + } + + if p.OnlyInstalled { + filterFn := func(plugin entity.CookedPlugin, index int) bool { + return plugin.Installed + } + plugins = lo.Filter(plugins, filterFn) + themes = lo.Filter(themes, filterFn) + } + + // Sort by name + sortFn := func(a, b entity.CookedPlugin) int { + return strings.Compare(a.Name, b.Name) + } + slices.SortFunc(plugins, sortFn) + slices.SortFunc(themes, sortFn) + + return common.RespData(c, ResponsePluginList{ + Plugins: plugins, + PluginsCount: int64(len(plugins)), + Themes: themes, + ThemesCount: int64(len(themes)), + }) + })) +} + +func mergePluginsWithInstallInfo( + registryPlugins []entity.PluginRegistryItem, + installedPluginList []entity.Plugin, +) []entity.CookedPlugin { + return lo.Map(registryPlugins, func(registryPlugin entity.PluginRegistryItem, _ int) entity.CookedPlugin { + var installedPlugin *entity.Plugin + if p, ok := checkPluginInstalled(installedPluginList, registryPlugin.ID); ok { + installedPlugin = &p + } + + return cookPlugin(®istryPlugin, installedPlugin) + }) +} + +func extendPluginsOutRegistry(plugins []entity.CookedPlugin, installed []entity.Plugin) { + for _, insPlugin := range installed { + // Skip plugin already in the list + if lo.ContainsBy(plugins, func(item entity.CookedPlugin) bool { + return item.ID == insPlugin.PluginID + }) { + continue + } + + // Extend plugin out of registry but installed + plugins = append(plugins, entity.CookedPlugin{ + Enabled: insPlugin.Enabled, + Installed: true, + Compatible: true, + PluginRegistryItem: entity.PluginRegistryItem{ + ID: insPlugin.PluginID, + Name: insPlugin.Name, + Type: string(insPlugin.Type), + Source: insPlugin.Source, + Integrity: insPlugin.Integrity, + Version: insPlugin.Version, + }, + }) + } +} diff --git a/server/handler/plugin_registry.go b/server/handler/plugin_registry.go new file mode 100644 index 00000000..9202be43 --- /dev/null +++ b/server/handler/plugin_registry.go @@ -0,0 +1,84 @@ +package handler + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/artalkjs/artalk/v2/internal/core" + "github.com/artalkjs/artalk/v2/internal/entity" + "github.com/artalkjs/artalk/v2/internal/log" + "github.com/artalkjs/artalk/v2/server/common" + "github.com/gofiber/fiber/v2" +) + +type ParamsPluginRegistryUpdate struct { +} + +// @Id UpdatePluginRegistry +// @Summary Update Plugin Registry +// @Description Update the plugin registry data +// @Tags Plugin +// @Security ApiKeyAuth +// @Param options body ParamsPluginRegistryUpdate true "The options" +// @Accept json +// @Produce json +// @Success 200 {object} Map{} +// @Failure 400 {object} Map{msg=string} +// @Failure 500 {object} Map{msg=string} +// @Router /plugin_registry/update [post] +func PluginRegistryUpdate(app *core.App, router fiber.Router) { + router.Post("/plugin_registry/update", common.AdminGuard(app, func(c *fiber.Ctx) error { + var p ParamsPluginRegistryUpdate + if isOK, resp := common.ParamsDecode(c, &p); !isOK { + return resp + } + + registryBaseURL := strings.TrimSuffix(app.Conf().Plugin.RegistryURL, "/") + registryURL := registryBaseURL + "/registry.json" + + // Http Get + resp, err := http.Get(registryURL) + if err != nil || resp.StatusCode != 200 { + log.Error("[PluginRegistryUpdate] Failed to get registry data: ", resp.StatusCode, " ", err) + return common.RespError(c, 500, fmt.Sprintf("Failed to get registry data. Got status code: %d", resp.StatusCode)) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Error("[PluginRegistryUpdate] Failed to read body: ", err) + return common.RespError(c, 500, "Failed to read body.") + } + + // Check JSON + var data entity.PluginRegistryData + if err := json.Unmarshal(body, &data); err != nil { + log.Error("[PluginRegistryUpdate] Failed to parse registry data: ", err) + return common.RespError(c, 400, "Failed to parse registry data.") + } + + // Unmarshal JSON + var jsonStr []byte + if jsonStr, err = json.Marshal(data); err != nil { + log.Error("[PluginRegistryUpdate] Failed to marshal registry data: ", err) + return common.RespError(c, 500, "Failed to marshal registry data.") + } + + // Save to DB + var record entity.PluginOption + if err := app.Dao().DB().Where(entity.PluginOption{ + PluginID: entity.PluginID_RegistryData, + }).FirstOrCreate(&record).Error; err != nil { + return common.RespError(c, 500, "Failed to find or create registry data.") + } + + // Update value + record.Value = string(jsonStr) + if err := app.Dao().DB().Save(&record).Error; err != nil { + return common.RespError(c, 500, "Failed to save registry data.") + } + + return common.RespSuccess(c) + })) +} diff --git a/server/handler/plugin_uninstall.go b/server/handler/plugin_uninstall.go new file mode 100644 index 00000000..5cfb081d --- /dev/null +++ b/server/handler/plugin_uninstall.go @@ -0,0 +1,43 @@ +package handler + +import ( + "github.com/artalkjs/artalk/v2/internal/core" + "github.com/artalkjs/artalk/v2/internal/entity" + "github.com/artalkjs/artalk/v2/server/common" + "github.com/gofiber/fiber/v2" +) + +type ParamsPluginUninstall struct { +} + +// @Id UninstallPlugin +// @Summary Uninstall Plugin +// @Description Uninstall a plugin by ID +// @Tags Plugin +// @Security ApiKeyAuth +// @Param plugin_id path string true "The plugin ID" +// @Param options body ParamsPluginUninstall true "The options" +// @Accept json +// @Produce json +// @Success 200 {object} Map{} +// @Failure 400 {object} Map{msg=string} +// @Failure 500 {object} Map{msg=string} +// @Router /plugins/{plugin_id}/uninstall [post] +func PluginUninstall(app *core.App, router fiber.Router) { + router.Post("/plugins/:plugin_id/uninstall", common.AdminGuard(app, func(c *fiber.Ctx) error { + pluginID := c.Params("plugin_id") + + var installed entity.Plugin + app.Dao().DB().Where(&entity.Plugin{PluginID: pluginID}).First(&installed) + + if installed.IsEmpty() { + return common.RespError(c, 400, "Plugin not installed.") + } + + if err := app.Dao().DB().Unscoped().Delete(&installed).Error; err != nil { + return common.RespError(c, 500, "Failed to uninstall plugin.") + } + + return common.RespSuccess(c) + })) +} diff --git a/server/handler/plugin_update.go b/server/handler/plugin_update.go new file mode 100644 index 00000000..1668e189 --- /dev/null +++ b/server/handler/plugin_update.go @@ -0,0 +1,85 @@ +package handler + +import ( + "encoding/json" + + "github.com/artalkjs/artalk/v2/internal/core" + "github.com/artalkjs/artalk/v2/internal/entity" + "github.com/artalkjs/artalk/v2/server/common" + "github.com/gofiber/fiber/v2" +) + +type ParamsPluginUpdate struct { + Enabled bool `json:"enabled" validate:"required"` // The plugin enabled status + ClientOptions string `json:"client_options" validate:"optional"` // The plugin client options (JSON string) +} + +type ResponsePluginUpdate struct { + Plugin entity.CookedPlugin `json:"plugin"` +} + +// @Id UpdatePlugin +// @Summary Update Plugin +// @Description Update a plugin status by ID +// @Tags Plugin +// @Security ApiKeyAuth +// @Param plugin_id path string true "The plugin ID" +// @Param plugin body ParamsPluginUpdate true "The plugin status" +// @Accept json +// @Produce json +// @Success 200 {object} ResponsePluginUpdate +// @Failure 400 {object} Map{msg=string} +// @Failure 500 {object} Map{msg=string} +// @Router /plugins/{plugin_id} [put] +func PluginUpdate(app *core.App, router fiber.Router) { + router.Put("/plugins/:plugin_id", common.AdminGuard(app, func(c *fiber.Ctx) error { + pluginID := c.Params("plugin_id") + var p ParamsPluginUpdate + if isOK, resp := common.ParamsDecode(c, &p); !isOK { + return resp + } + + // Get Registry Data + registryPlugin := findRegistryPlugin(pluginID, app.Dao()) + + // Check had installed + var installed entity.Plugin + app.Dao().DB().Where(&entity.Plugin{PluginID: pluginID}).First(&installed) + + if installed.IsEmpty() { + return common.RespError(c, 400, "Plugin not installed.") + } + + // Update Plugin + installed.Enabled = p.Enabled + app.Dao().DB().Save(&installed) + + // Update Options + if p.ClientOptions != "" { + // Validate JSON + if err := json.Unmarshal([]byte(p.ClientOptions), &map[string]any{}); err != nil { + return common.RespError(c, 400, "Param 'client_options' is not a valid JSON string.") + } + + // (PluginID, Name, Value) = (pluginID, "client_options", p.ClientOptions) + var clientOptionsRecord entity.PluginOption + app.Dao().DB().Where(&entity.PluginOption{PluginID: pluginID, Name: entity.PluginOptionName_ClientOptions}).First(&clientOptionsRecord) + if clientOptionsRecord.IsEmpty() { + clientOptionsRecord = entity.PluginOption{ + PluginID: pluginID, + Name: entity.PluginOptionName_ClientOptions, + Value: p.ClientOptions, + } + } else { + clientOptionsRecord.Value = p.ClientOptions + } + app.Dao().DB().Save(&clientOptionsRecord) + } + + cookedPlugin := cookPlugin(registryPlugin, &installed) + + return common.RespData(c, ResponsePluginUpdate{ + Plugin: cookedPlugin, + }) + })) +} diff --git a/server/handler/plugin_upgrade.go b/server/handler/plugin_upgrade.go new file mode 100644 index 00000000..82b34cc5 --- /dev/null +++ b/server/handler/plugin_upgrade.go @@ -0,0 +1,26 @@ +package handler + +import ( + "github.com/artalkjs/artalk/v2/internal/core" + "github.com/gofiber/fiber/v2" +) + +type ParamsPluginUpgrade struct { +} + +// @Id UpgradePlugin +// @Summary Upgrade Plugin +// @Description Upgrade a plugin by ID +// @Tags Plugin +// @Security ApiKeyAuth +// @Param plugin_id path string true "The plugin ID" +// @Param options body ParamsPluginUpgrade true "The options" +// @Accept json +// @Produce json +// @Success 200 {object} Map{} +// @Failure 400 {object} Map{msg=string} +// @Failure 500 {object} Map{msg=string} +// @Router /plugins/{plugin_id}/upgrade [post] +func PluginUpgrade(app *core.App, router fiber.Router) { + router.Post("/plugins/:plugin_id/upgrade", installPlugin(app, InstallPluginTypeUpgrade)) +} diff --git a/server/server.go b/server/server.go index a4af4191..3494b8bc 100644 --- a/server/server.go +++ b/server/server.go @@ -150,6 +150,16 @@ func admin(app *core.App, api fiber.Router) { h.SettingApply(app, api) h.SettingTemplate(app, api) h.Transfer(app, api) + + if app.Conf().Plugin.Enabled { + h.PluginList(app, api) + h.PluginGet(app, api) + h.PluginInstall(app, api) + h.PluginUpgrade(app, api) + h.PluginUninstall(app, api) + h.PluginUpdate(app, api) + h.PluginRegistryUpdate(app, api) + } } func reqID(fb *fiber.App) { diff --git a/ui/artalk-sidebar/components.d.ts b/ui/artalk-sidebar/components.d.ts index 84d356a4..ab2f67b5 100644 --- a/ui/artalk-sidebar/components.d.ts +++ b/ui/artalk-sidebar/components.d.ts @@ -7,6 +7,7 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AppDialog: typeof import('./src/components/AppDialog.vue')['default'] AppHeader: typeof import('./src/components/AppHeader.vue')['default'] AppNavigation: typeof import('./src/components/AppNavigation.vue')['default'] AppNavigationDesktop: typeof import('./src/components/AppNavigationDesktop.vue')['default'] @@ -18,6 +19,7 @@ declare module 'vue' { LogTerminal: typeof import('./src/components/LogTerminal.vue')['default'] PageEditor: typeof import('./src/components/PageEditor.vue')['default'] Pagination: typeof import('./src/components/Pagination.vue')['default'] + PluginOptionsEditor: typeof import('./src/components/PluginOptionsEditor.vue')['default'] PreferenceArr: typeof import('./src/components/PreferenceArr.vue')['default'] PreferenceGrp: typeof import('./src/components/PreferenceGrp.vue')['default'] PreferenceItem: typeof import('./src/components/PreferenceItem.vue')['default'] diff --git a/ui/artalk-sidebar/package.json b/ui/artalk-sidebar/package.json index d2e2da43..cf30bb5f 100644 --- a/ui/artalk-sidebar/package.json +++ b/ui/artalk-sidebar/package.json @@ -16,6 +16,7 @@ "artalk": "workspace:^", "crypto-js": "^4.2.0", "pinia": "^2.2.4", + "timeago.js": "^4.0.2", "typescript": "^5.6.3", "unplugin-auto-import": "^0.18.3", "unplugin-vue-components": "^0.27.4", diff --git a/ui/artalk-sidebar/src/assets/nav-icon-plugins.svg b/ui/artalk-sidebar/src/assets/nav-icon-plugins.svg new file mode 100644 index 00000000..d0d3d0e0 --- /dev/null +++ b/ui/artalk-sidebar/src/assets/nav-icon-plugins.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/artalk-sidebar/src/components/AppDialog.vue b/ui/artalk-sidebar/src/components/AppDialog.vue new file mode 100644 index 00000000..aa1399aa --- /dev/null +++ b/ui/artalk-sidebar/src/components/AppDialog.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/ui/artalk-sidebar/src/components/AppHeader.vue b/ui/artalk-sidebar/src/components/AppHeader.vue index 7df9c927..aa4faef8 100644 --- a/ui/artalk-sidebar/src/components/AppHeader.vue +++ b/ui/artalk-sidebar/src/components/AppHeader.vue @@ -189,6 +189,10 @@ const logout = () => { width: 60px; height: 60px; margin-left: 10px; + + @media (min-width: 1024px) { + display: none; + } } .dark-mode-toggle { diff --git a/ui/artalk-sidebar/src/components/AppNavigationMenu.ts b/ui/artalk-sidebar/src/components/AppNavigationMenu.ts index 0a08d314..267f7f61 100644 --- a/ui/artalk-sidebar/src/components/AppNavigationMenu.ts +++ b/ui/artalk-sidebar/src/components/AppNavigationMenu.ts @@ -5,6 +5,7 @@ import PagesIcon from '@/assets/nav-icon-pages.svg' import UsersIcon from '@/assets/nav-icon-users.svg' import SitesIcon from '@/assets/nav-icon-sites.svg' import TransferIcon from '@/assets/nav-icon-transfer.svg' +import PluginsIcon from '@/assets/nav-icon-plugins.svg' import SettingsIcon from '@/assets/nav-icon-settings.svg' import { useUserStore } from '@/stores/user' import { useNavStore } from '@/stores/nav' @@ -42,6 +43,12 @@ export const AdminPages: Record = { hideOnMobile: true, icon: TransferIcon, }, + plugins: { + label: 'plugins', + link: '/plugins', + hideOnMobile: true, + icon: PluginsIcon, + }, settings: { label: 'settings', link: '/settings', diff --git a/ui/artalk-sidebar/src/components/PluginOptionsEditor.vue b/ui/artalk-sidebar/src/components/PluginOptionsEditor.vue new file mode 100644 index 00000000..1119cfb4 --- /dev/null +++ b/ui/artalk-sidebar/src/components/PluginOptionsEditor.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/ui/artalk-sidebar/src/i18n-en.ts b/ui/artalk-sidebar/src/i18n-en.ts index 59dd604d..ae83b011 100644 --- a/ui/artalk-sidebar/src/i18n-en.ts +++ b/ui/artalk-sidebar/src/i18n-en.ts @@ -94,6 +94,7 @@ export const en = { logout: 'Logout', logoutConfirm: 'Are you sure you want to log out?', loginSelectHint: 'Please select the account you wish to log into:', + plugins: 'Plugins', } export default en diff --git a/ui/artalk-sidebar/src/i18n/fr.ts b/ui/artalk-sidebar/src/i18n/fr.ts index 30850f7c..336a8be2 100644 --- a/ui/artalk-sidebar/src/i18n/fr.ts +++ b/ui/artalk-sidebar/src/i18n/fr.ts @@ -98,6 +98,7 @@ export const fr: MessageSchema = { logout: 'Déconnexion', logoutConfirm: 'Êtes-vous sûr de vouloir vous déconnecter ?', loginSelectHint: 'Veuillez sélectionner le compte avec lequel vous souhaitez vous connecter :', + plugins: 'Plugins', } export default fr diff --git a/ui/artalk-sidebar/src/i18n/ja.ts b/ui/artalk-sidebar/src/i18n/ja.ts index d1103a7e..805d5b17 100644 --- a/ui/artalk-sidebar/src/i18n/ja.ts +++ b/ui/artalk-sidebar/src/i18n/ja.ts @@ -95,6 +95,7 @@ export const ja: MessageSchema = { logout: 'ログアウト', logoutConfirm: 'ログアウトしてもよろしいですか?', loginSelectHint: 'ログインするアカウントを選択してください:', + plugins: 'プラグイン', } export default ja diff --git a/ui/artalk-sidebar/src/i18n/ko.ts b/ui/artalk-sidebar/src/i18n/ko.ts index 8067978b..fbe22f90 100644 --- a/ui/artalk-sidebar/src/i18n/ko.ts +++ b/ui/artalk-sidebar/src/i18n/ko.ts @@ -94,6 +94,7 @@ export const ko: MessageSchema = { logout: '로그아웃', logoutConfirm: '로그아웃하시겠습니까?', loginSelectHint: '로그인할 계정을 선택하세요:', + plugins: '플러그인', } export default ko diff --git a/ui/artalk-sidebar/src/i18n/ru.ts b/ui/artalk-sidebar/src/i18n/ru.ts index db66c417..ce3eaf78 100644 --- a/ui/artalk-sidebar/src/i18n/ru.ts +++ b/ui/artalk-sidebar/src/i18n/ru.ts @@ -97,6 +97,7 @@ export const ru: MessageSchema = { logout: 'Выйти', logoutConfirm: 'Вы уверены, что хотите выйти?', loginSelectHint: 'Пожалуйста, выберите аккаунт, с которым вы хотите войти:', + plugins: 'Плагины', } export default ru diff --git a/ui/artalk-sidebar/src/i18n/zh-CN.ts b/ui/artalk-sidebar/src/i18n/zh-CN.ts index 97414346..baa022db 100644 --- a/ui/artalk-sidebar/src/i18n/zh-CN.ts +++ b/ui/artalk-sidebar/src/i18n/zh-CN.ts @@ -92,6 +92,7 @@ export const zhCN: MessageSchema = { logout: '退出登录', logoutConfirm: '确定要退出登录吗?', loginSelectHint: '请选择您要登录的账户:', + plugins: '插件', } export default zhCN diff --git a/ui/artalk-sidebar/src/i18n/zh-TW.ts b/ui/artalk-sidebar/src/i18n/zh-TW.ts index e86bcedb..d9cf0dfd 100644 --- a/ui/artalk-sidebar/src/i18n/zh-TW.ts +++ b/ui/artalk-sidebar/src/i18n/zh-TW.ts @@ -92,6 +92,7 @@ export const zhTW: MessageSchema = { logout: '登出', logoutConfirm: '確定要登出嗎?', loginSelectHint: '請選擇您要登入的帳號:', + plugins: '插件', } export default zhTW diff --git a/ui/artalk-sidebar/src/pages/plugins.vue b/ui/artalk-sidebar/src/pages/plugins.vue new file mode 100644 index 00000000..b3da2a86 --- /dev/null +++ b/ui/artalk-sidebar/src/pages/plugins.vue @@ -0,0 +1,632 @@ + + + + + diff --git a/ui/artalk-sidebar/src/pages/settings.vue b/ui/artalk-sidebar/src/pages/settings.vue index 0183120f..fb285566 100644 --- a/ui/artalk-sidebar/src/pages/settings.vue +++ b/ui/artalk-sidebar/src/pages/settings.vue @@ -18,11 +18,13 @@ onMounted(() => { nav.updateTabs({ sites: 'site', transfer: 'transfer', + plugins: 'plugins', }) watch(curtTab, (tab) => { if (tab === 'sites') router.replace('/sites') else if (tab === 'transfer') router.replace('/transfer') + else if (tab === 'plugins') router.replace('/plugins') }) Promise.all([ diff --git a/ui/artalk-sidebar/src/stores/nav.ts b/ui/artalk-sidebar/src/stores/nav.ts index 0e14e363..80e41732 100644 --- a/ui/artalk-sidebar/src/stores/nav.ts +++ b/ui/artalk-sidebar/src/stores/nav.ts @@ -82,6 +82,7 @@ export const useNavStore = defineStore('nav', () => { useRouter().beforeEach((to, from) => { isSearchEnabled.value = false + curtTab.value = '' }) const isMobile = useMobileWidth() diff --git a/ui/artalk-sidebar/typed-router.d.ts b/ui/artalk-sidebar/typed-router.d.ts index 75079ec9..26436b86 100644 --- a/ui/artalk-sidebar/typed-router.d.ts +++ b/ui/artalk-sidebar/typed-router.d.ts @@ -22,6 +22,7 @@ declare module 'vue-router/auto-routes' { '/comments': RouteRecordInfo<'/comments', '/comments', Record, Record>, '/login': RouteRecordInfo<'/login', '/login', Record, Record>, '/pages': RouteRecordInfo<'/pages', '/pages', Record, Record>, + '/plugins': RouteRecordInfo<'/plugins', '/plugins', Record, Record>, '/settings': RouteRecordInfo<'/settings', '/settings', Record, Record>, '/sites': RouteRecordInfo<'/sites', '/sites', Record, Record>, '/transfer': RouteRecordInfo<'/transfer', '/transfer', Record, Record>, diff --git a/ui/artalk/src/api/v2.ts b/ui/artalk/src/api/v2.ts index b41505dd..63085a7f 100644 --- a/ui/artalk/src/api/v2.ts +++ b/ui/artalk/src/api/v2.ts @@ -24,6 +24,7 @@ export interface CommonApiVersionData { export interface CommonConfData { frontend_conf: CommonMap + plugins: CommonPluginItem[] version: CommonApiVersionData } @@ -36,6 +37,13 @@ export interface CommonJSONResult { export type CommonMap = Record +export interface CommonPluginItem { + integrity?: string + options?: string + source: string + type: EntityPluginType +} + export interface EntityCookedComment { badge_color: string badge_name: string @@ -85,6 +93,32 @@ export interface EntityCookedPage { vote_up: number } +export interface EntityCookedPlugin { + author_link: string + author_name: string + compatible: boolean + compatible_notice?: string + description: string + donate_link: string + enabled: boolean + id: string + installed: boolean + integrity: string + local_version: string + min_artalk_version: string + name: string + npm_name: string + options_schema: string + repo_link: string + repo_name: string + source: string + type: string + updated_at: string + upgrade_available: boolean + verified: boolean + version: string +} + export interface EntityCookedSite { first_url: string id: number @@ -119,6 +153,11 @@ export interface EntityCookedUserForAdmin { receive_email: boolean } +export enum EntityPluginType { + PluginTypePlugin = 'plugin', + PluginTypeTheme = 'theme', +} + export type HandlerMap = Record export interface HandlerParamsCaptchaVerify { @@ -215,6 +254,21 @@ export interface HandlerParamsPageUpdate { title: string } +export type HandlerParamsPluginInstall = object + +export type HandlerParamsPluginRegistryUpdate = object + +export type HandlerParamsPluginUninstall = object + +export interface HandlerParamsPluginUpdate { + /** The plugin client options (JSON string) */ + client_options?: string + /** The plugin enabled status */ + enabled: boolean +} + +export type HandlerParamsPluginUpgrade = object + export interface HandlerParamsSettingApply { /** The content of the config file in YAML format */ yaml: string @@ -492,6 +546,28 @@ export interface HandlerResponsePageUpdate { vote_up: number } +export interface HandlerResponsePluginGet { + /** The plugin client options (JSON string) */ + client_options: string + /** The plugin enabled status */ + enabled: boolean + /** The plugin options schema (JSON string) */ + options_schema: string + /** The plugin info */ + plugin: EntityCookedPlugin +} + +export interface HandlerResponsePluginList { + plugins: EntityCookedPlugin[] + plugins_count: number + themes: EntityCookedPlugin[] + themes_count: number +} + +export interface HandlerResponsePluginUpdate { + plugin: EntityCookedPlugin +} + export interface HandlerResponseSettingGet { envs: string[] yaml: string @@ -1746,6 +1822,268 @@ export class Api extends HttpClient + this.request< + HandlerMap, + HandlerMap & { + msg?: string + } + >({ + path: `/plugin_registry/update`, + method: 'POST', + body: options, + secure: true, + type: ContentType.Json, + format: 'json', + ...params, + }), + } + plugins = { + /** + * @description Get a list of plugins by some conditions + * + * @tags Plugin + * @name GetPlugins + * @summary Get Plugin List + * @request GET:/plugins + * @secure + * @response `200` `HandlerResponsePluginList` OK + * @response `403` `(HandlerMap & { + msg?: string, + +})` Forbidden + */ + getPlugins: ( + query?: { + /** Only installed plugins */ + only_installed?: boolean + /** Search keywords */ + search?: string + }, + params: RequestParams = {}, + ) => + this.request< + HandlerResponsePluginList, + HandlerMap & { + msg?: string + } + >({ + path: `/plugins`, + method: 'GET', + query: query, + secure: true, + type: ContentType.Json, + format: 'json', + ...params, + }), + + /** + * @description Get a plugin info by ID + * + * @tags Plugin + * @name GetPlugin + * @summary Get Plugin Info + * @request GET:/plugins/{plugin_id} + * @secure + * @response `200` `HandlerResponsePluginGet` OK + * @response `400` `(HandlerMap & { + msg?: string, + +})` Bad Request + * @response `500` `(HandlerMap & { + msg?: string, + +})` Internal Server Error + */ + getPlugin: (pluginId: string, params: RequestParams = {}) => + this.request< + HandlerResponsePluginGet, + HandlerMap & { + msg?: string + } + >({ + path: `/plugins/${pluginId}`, + method: 'GET', + secure: true, + type: ContentType.Json, + format: 'json', + ...params, + }), + + /** + * @description Update a plugin status by ID + * + * @tags Plugin + * @name UpdatePlugin + * @summary Update Plugin + * @request PUT:/plugins/{plugin_id} + * @secure + * @response `200` `HandlerResponsePluginUpdate` OK + * @response `400` `(HandlerMap & { + msg?: string, + +})` Bad Request + * @response `500` `(HandlerMap & { + msg?: string, + +})` Internal Server Error + */ + updatePlugin: ( + pluginId: string, + plugin: HandlerParamsPluginUpdate, + params: RequestParams = {}, + ) => + this.request< + HandlerResponsePluginUpdate, + HandlerMap & { + msg?: string + } + >({ + path: `/plugins/${pluginId}`, + method: 'PUT', + body: plugin, + secure: true, + type: ContentType.Json, + format: 'json', + ...params, + }), + + /** + * @description Install a plugin by ID + * + * @tags Plugin + * @name InstallPlugin + * @summary Install Plugin + * @request POST:/plugins/{plugin_id}/install + * @secure + * @response `200` `HandlerMap` OK + * @response `400` `(HandlerMap & { + msg?: string, + +})` Bad Request + * @response `500` `(HandlerMap & { + msg?: string, + +})` Internal Server Error + */ + installPlugin: ( + pluginId: string, + options: HandlerParamsPluginInstall, + params: RequestParams = {}, + ) => + this.request< + HandlerMap, + HandlerMap & { + msg?: string + } + >({ + path: `/plugins/${pluginId}/install`, + method: 'POST', + body: options, + secure: true, + type: ContentType.Json, + format: 'json', + ...params, + }), + + /** + * @description Uninstall a plugin by ID + * + * @tags Plugin + * @name UninstallPlugin + * @summary Uninstall Plugin + * @request POST:/plugins/{plugin_id}/uninstall + * @secure + * @response `200` `HandlerMap` OK + * @response `400` `(HandlerMap & { + msg?: string, + +})` Bad Request + * @response `500` `(HandlerMap & { + msg?: string, + +})` Internal Server Error + */ + uninstallPlugin: ( + pluginId: string, + options: HandlerParamsPluginUninstall, + params: RequestParams = {}, + ) => + this.request< + HandlerMap, + HandlerMap & { + msg?: string + } + >({ + path: `/plugins/${pluginId}/uninstall`, + method: 'POST', + body: options, + secure: true, + type: ContentType.Json, + format: 'json', + ...params, + }), + + /** + * @description Upgrade a plugin by ID + * + * @tags Plugin + * @name UpgradePlugin + * @summary Upgrade Plugin + * @request POST:/plugins/{plugin_id}/upgrade + * @secure + * @response `200` `HandlerMap` OK + * @response `400` `(HandlerMap & { + msg?: string, + +})` Bad Request + * @response `500` `(HandlerMap & { + msg?: string, + +})` Internal Server Error + */ + upgradePlugin: ( + pluginId: string, + options: HandlerParamsPluginUpgrade, + params: RequestParams = {}, + ) => + this.request< + HandlerMap, + HandlerMap & { + msg?: string + } + >({ + path: `/plugins/${pluginId}/upgrade`, + method: 'POST', + body: options, + secure: true, + type: ContentType.Json, + format: 'json', + ...params, + }), + } sendEmail = { /** * @description Send an email to test the email sender diff --git a/ui/artalk/src/mount.ts b/ui/artalk/src/mount.ts index dfcec420..119c21e7 100644 --- a/ui/artalk/src/mount.ts +++ b/ui/artalk/src/mount.ts @@ -2,6 +2,7 @@ import { handleConfFormServer } from './config' import { DefaultPlugins } from './plugins' import { mergeDeep } from './lib/merge-deep' import { MountError } from './plugins/mount-error' +import type { CommonPluginItem } from './api/v2' import type { ConfigPartial, ArtalkPlugin, Context } from '@/types' /** @@ -50,8 +51,9 @@ export async function mount(localConf: ConfigPartial, ctx: Context) { ctx.updateConf(conf) // Load remote plugins - conf.pluginURLs && - (await loadNetworkPlugins(conf.pluginURLs, ctx.getConf().server) + const remotePlugins = data.plugins || [] + remotePlugins && + (await loadNetworkPlugins(remotePlugins, ctx.getConf().server) .then((plugins) => { loadPlugins(plugins) }) @@ -63,32 +65,64 @@ export async function mount(localConf: ConfigPartial, ctx: Context) { /** * Dynamically load plugins from Network */ -async function loadNetworkPlugins(scripts: string[], apiBase: string): Promise> { +async function loadNetworkPlugins( + plugins: CommonPluginItem[], + apiBase: string, +): Promise> { const networkPlugins = new Set() - if (!scripts || !Array.isArray(scripts)) return networkPlugins + if (!plugins || !Array.isArray(plugins)) return networkPlugins const tasks: Promise[] = [] - scripts.forEach((url) => { + const addPlugin = (targetPlugin: CommonPluginItem) => { + Object.entries(window.ArtalkPlugins || {}).forEach(([pluginName, plugin]) => { + if (typeof plugin !== 'function' || networkPlugins.has(plugin)) return + + // Add plugin to list + networkPlugins.add(plugin) + + // Parse plugin options + let options: any = {} + try { + options = JSON.parse(targetPlugin.options || '{}') + } catch (err) { + console.error( + `[artalk] Failed to parse plugin '${pluginName}' options: '${targetPlugin.options}'.`, + err, + ) + } + PluginOptions.set(plugin, targetPlugin.options) + }) + } + + plugins.forEach((plugin) => { + if (!plugin.source) return + // check url valid - if (!/^(http|https):\/\//.test(url)) - url = `${apiBase.replace(/\/$/, '')}/${url.replace(/^\//, '')}` + if (!/^(http|https):\/\//.test(plugin.source)) + plugin.source = `${apiBase.replace(/\/$/, '')}/${plugin.source.replace(/^\//, '')}` tasks.push( new Promise((resolve) => { // check if loaded - if (document.querySelector(`script[src="${url}"]`)) { + if (document.querySelector(`script[src="${plugin.source}"]`)) { resolve() return } // load script const script = document.createElement('script') - script.src = url + script.src = plugin.source + script.crossOrigin = 'anonymous' + if (plugin.integrity) script.integrity = plugin.integrity + document.head.appendChild(script) - script.onload = () => resolve() - script.onerror = (err) => { - console.error('[artalk] Failed to load plugin', err) + script.onload = () => { + addPlugin(plugin) + resolve() + } + script.onerror = () => { + console.error(`[artalk] Failed to load plugin script from '${plugin.source}'.`) resolve() } }), @@ -97,10 +131,5 @@ async function loadNetworkPlugins(scripts: string[], apiBase: string): Promise { - if (typeof plugin === 'function') networkPlugins.add(plugin) - }) - return networkPlugins } diff --git a/ui/plugin-kit/package.json b/ui/plugin-kit/package.json index 0d147ee5..34049bb7 100644 --- a/ui/plugin-kit/package.json +++ b/ui/plugin-kit/package.json @@ -32,7 +32,8 @@ "devDependencies": { "artalk": "workspace:^", "esbuild-plugin-raw": "^0.1.8", - "tsup": "^8.3.0" + "tsup": "^8.3.0", + "typescript-json-schema": "^0.65.1" }, "peerDependencies": { "artalk": "workspace:^", diff --git a/ui/plugin-kit/src/plugin/main.ts b/ui/plugin-kit/src/plugin/main.ts index ede65e64..8afa9d1e 100644 --- a/ui/plugin-kit/src/plugin/main.ts +++ b/ui/plugin-kit/src/plugin/main.ts @@ -12,6 +12,7 @@ import logger from './logger' import { generateDts, rollupDeclarationFiles } from './dts' import { runPackageExportsLint } from './lint/package-exports-lint' import { ViteArtalkPluginKitCtx } from './context' +import { buildOptionsSchema } from './options-schema' const tsRE = /\.(m|c)?tsx?$/ const dtsRE = /\.d\.ts$/ @@ -21,6 +22,11 @@ export interface ViteArtalkPluginKitOptions { * The options for Artalk instance initialization in the dev page. */ artalkInitOptions?: ConfigPartial + + /** + * Enable schema generation for Artalk plugin options. + */ + buildOptionsSchema?: boolean } export const ViteArtalkPluginKit = (opts: ViteArtalkPluginKitOptions = {}): Plugin => { @@ -246,6 +252,15 @@ export const ViteArtalkPluginKit = (opts: ViteArtalkPluginKitOptions = {}): Plug } logger.info(`Generate dts files successfully!`) + + if (opts.buildOptionsSchema !== false) { + buildOptionsSchema({ + dtsPath: distDtsPath, + outDir: ctx.outDir, + }) + + logger.info(`Generate plugin options schema successfully!`) + } }, } } diff --git a/ui/plugin-kit/src/plugin/options-schema.ts b/ui/plugin-kit/src/plugin/options-schema.ts new file mode 100644 index 00000000..6b71b74c --- /dev/null +++ b/ui/plugin-kit/src/plugin/options-schema.ts @@ -0,0 +1,38 @@ +import fs from 'node:fs' +import path from 'node:path' +import * as TJS from 'typescript-json-schema' +import logger from './logger' + +export function buildOptionsSchema({ dtsPath, outDir }: { dtsPath: string; outDir: string }) { + // optionally pass argument to schema generator + const settings: TJS.PartialArgs = { + required: true, + ignoreErrors: true, + } + + // optionally pass ts compiler options + const compilerOptions: TJS.CompilerOptions = { + strictNullChecks: true, + } + + // optionally pass a base path + const program = TJS.getProgramFromFiles([dtsPath], compilerOptions) + + const generator = TJS.buildGenerator(program, settings) + + // extract from dtsPath file by regex of `ArtalkPlugin` + const dtsFileContent = fs.readFileSync(dtsPath, 'utf-8') + const symbolNameMatches = /ArtalkPlugin<(.*?)>/.exec(dtsFileContent) + if (!symbolNameMatches || symbolNameMatches.length < 2) { + logger.info(`Skip plugin options schema generation.`) + return + } + + // Get symbols for different types from generator. + const symbolName = symbolNameMatches[1] + const schema = generator?.getSchemaForSymbol(symbolName) + + // Save schema + const schemaPath = path.resolve(outDir, `artalk-plugin-options.schema.json`) + fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2), 'utf-8') +} diff --git a/ui/plugin-kit/tsup.config.ts b/ui/plugin-kit/tsup.config.ts index 9289a4b6..2a2f08e4 100644 --- a/ui/plugin-kit/tsup.config.ts +++ b/ui/plugin-kit/tsup.config.ts @@ -18,7 +18,7 @@ export default defineConfig([ ...shared, outDir: 'dist', entry: ['src/plugin/main.ts'], - external: ['artalk', 'typescript', 'picocolors', '@microsoft/api-extractor'], + external: ['artalk', 'typescript', 'picocolors', '@microsoft/api-extractor', 'typescript-json-schema'], esbuildPlugins: [RawPlugin()], }, { diff --git a/ui/plugin-lightbox/src/main.ts b/ui/plugin-lightbox/src/main.ts index f90d472a..20c9f388 100644 --- a/ui/plugin-lightbox/src/main.ts +++ b/ui/plugin-lightbox/src/main.ts @@ -7,15 +7,19 @@ const IMG_LINK_EL_CLASS = 'atk-lightbox-img' const IMG_LINK_EL_SEL = `.${IMG_LINK_EL_CLASS}` export interface ArtalkLightboxPluginOptions { + /** @ignore */ lightGallery?: { lib?: () => Promise } + /** @ignore */ lightBox?: { lib?: () => Promise } + /** @ignore */ fancyBox?: { lib?: () => Promise } + /** @ignore */ photoSwipe?: { lib?: () => Promise pswpModule?: () => Promise