diff --git a/.gitignore b/.gitignore index 1b4a277..d68cad1 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ /Release/ **/dist/ **/node_modules/ +src/YMhut.Box.PluginEcho/plugin-echo/ src/box-winUI/AppPackages/ server/update/build/ server/update/public/downloads/ diff --git a/docs/plugins/MIGRATION-v3.md b/docs/plugins/MIGRATION-v3.md index d701689..6353568 100644 --- a/docs/plugins/MIGRATION-v3.md +++ b/docs/plugins/MIGRATION-v3.md @@ -1,5 +1,7 @@ # 从旧清单迁移到 manifest v3 +客户端扫描到旧清单后会立即停用插件,并将目录移动到 `data/PluginRecycle/legacy--<时间>-<随机值>`。请从该可恢复副本升级清单后,再作为新包导入;旧授权、KV 与 WebView 缓存不会继承。 + 1. 添加 `manifestVersion: 3`、`apiVersion: "2"` 和 `runtime: "WebView"`。 2. 为 `permissions` 中每项权限添加非空 `permissionReasons`。 3. 将启动必需权限放入 `security.requiredPermissions`,其余保持可选。 diff --git a/docs/plugins/README.md b/docs/plugins/README.md index 495f253..c7ac54e 100644 --- a/docs/plugins/README.md +++ b/docs/plugins/README.md @@ -88,4 +88,4 @@ await window.ymhut.output.set(response.content); `WebView` 是默认和正式运行时。Tauri 独立窗口仅使用应用自带宿主,默认关闭;它要求插件开发者模式、`ExternalRuntime` 授权和按插件版本保存的二次确认。Tauri 插件内容运行在独立的 `https://p-.localhost` 原点和插件私有 WebView2 数据目录中,应用宿主页只通过一次性、当前用户命名管道把 Bridge 请求转发给同一个权限宿主。会话令牌仅存在于原生宿主内,不进入插件 JavaScript。插件包不能携带或构建原生可执行文件,也不能访问 Tauri 全局 API。 -旧清单自动降级为 `LegacyWebOnly`:本地 Web 内容可运行,Bridge、远程连接和外接运行时全部关闭。迁移步骤见 [MIGRATION-v3.md](MIGRATION-v3.md),完整边界见 [SECURITY.md](SECURITY.md),TypeScript 声明见 [ymhut.d.ts](ymhut.d.ts)。 +`manifestVersion < 3` 的旧清单不再运行。扫描时插件会立即停用、清除授权和 WebView 缓存,并移动到 `data/PluginRecycle/legacy--<时间>-<随机值>` 以便恢复;迁移失败时保持隐藏并在下次扫描重试。缺失或损坏的清单不会被自动删除。升级步骤见 [MIGRATION-v3.md](MIGRATION-v3.md),完整边界见 [SECURITY.md](SECURITY.md),TypeScript 声明见 [ymhut.d.ts](ymhut.d.ts)。 diff --git a/installer/ymhut_box_winui.iss b/installer/ymhut_box_winui.iss index c5a0975..f7faa59 100644 --- a/installer/ymhut_box_winui.iss +++ b/installer/ymhut_box_winui.iss @@ -1130,6 +1130,82 @@ begin #endif end; +function IsPreservedDirectoryTree(const RelativePath: string): Boolean; +var + Normalized: string; +begin + Normalized := LowerCase(Trim(RelativePath)); + Result := + (Normalized = 'data') or (Pos('data\', Normalized) = 1) or + (Normalized = 'feedbackpackages') or (Pos('feedbackpackages\', Normalized) = 1) or + (Normalized = 'config') or (Pos('config\', Normalized) = 1) or + (Normalized = 'runtime\uninstall-engine') or + (Pos('runtime\uninstall-engine\', Normalized) = 1); +end; + +function ManifestRequiresDirectory(const RelativePath: string): Boolean; +var + I: Integer; + DirectoryPrefix: string; + Item: string; +begin + Result := False; + DirectoryPrefix := LowerCase(RemoveBackslashUnlessRoot(RelativePath)) + '\'; + for I := 0 to NewPayloadFiles.Count - 1 do + begin + Item := LowerCase(NewPayloadFiles[I]); + if Pos(DirectoryPrefix, Item) = 1 then + begin + Result := True; + Exit; + end; + end; +end; + +function IsInstallDirectoryPath(const AppDir, Candidate: string): Boolean; +var + RootPath: string; + CandidatePath: string; +begin + RootPath := LowerCase(AddBackslash(RemoveBackslashUnlessRoot(AppDir))); + CandidatePath := LowerCase(AddBackslash(RemoveBackslashUnlessRoot(Candidate))); + Result := Pos(RootPath, CandidatePath) = 1; +end; + +procedure RemoveEmptyInstallDirectories(const AppDir, CurrentDir: string); +var + FindRec: TFindRec; + FullPath: string; + RelativePath: string; +begin + if (not DirExists(CurrentDir)) or (not IsInstallDirectoryPath(AppDir, CurrentDir)) then + Exit; + + if FindFirst(AddBackslash(CurrentDir) + '*', FindRec) then + begin + try + repeat + if (FindRec.Name <> '.') and (FindRec.Name <> '..') and + ((FindRec.Attributes and DirectoryAttribute) <> 0) then + begin + FullPath := AddBackslash(CurrentDir) + FindRec.Name; + RelativePath := RelativeInstallPath(AppDir, FullPath); + if (RelativePath <> '') and + ((FindRec.Attributes and $400) = 0) and + (not IsPreservedDirectoryTree(RelativePath)) then + begin + RemoveEmptyInstallDirectories(AppDir, FullPath); + if (not ManifestRequiresDirectory(RelativePath)) and RemoveDir(FullPath) then + Log('Removed empty obsolete install directory: ' + RelativePath); + end; + end; + until not FindNext(FindRec); + finally + FindClose(FindRec); + end; + end; +end; + procedure FinalizeLegacyInstallLayout(); var AppDir: string; @@ -1140,6 +1216,7 @@ begin DeleteKnownObsoletePayload(AppDir) else QuarantineUnknownLegacyFiles(AppDir, AppDir); + RemoveEmptyInstallDirectories(AppDir, AppDir); CleanLegacyRuntimeAndCache(); end; diff --git a/src/YMhut.Box.Core/Plugins/BuiltIn/ipcheck-demo/README.md b/src/YMhut.Box.Core/Plugins/BuiltIn/bridge-capabilities-demo/README.md similarity index 96% rename from src/YMhut.Box.Core/Plugins/BuiltIn/ipcheck-demo/README.md rename to src/YMhut.Box.Core/Plugins/BuiltIn/bridge-capabilities-demo/README.md index 652c96b..c59e59f 100644 --- a/src/YMhut.Box.Core/Plugins/BuiltIn/ipcheck-demo/README.md +++ b/src/YMhut.Box.Core/Plugins/BuiltIn/bridge-capabilities-demo/README.md @@ -1,4 +1,4 @@ -# IPCheck manifest v3 安全示例 +# Bridge 能力 manifest v3 示例 该内置插件演示 manifest v3、必需/可选权限、精确公网 HTTPS 来源和 `PluginHostProtocol v2` Bridge。页面和脚本全部随插件本地发布,不依赖远程代码。 diff --git a/src/YMhut.Box.Core/Plugins/BuiltIn/ipcheck-demo/index.html b/src/YMhut.Box.Core/Plugins/BuiltIn/bridge-capabilities-demo/index.html similarity index 67% rename from src/YMhut.Box.Core/Plugins/BuiltIn/ipcheck-demo/index.html rename to src/YMhut.Box.Core/Plugins/BuiltIn/bridge-capabilities-demo/index.html index f04a172..fdb96f5 100644 --- a/src/YMhut.Box.Core/Plugins/BuiltIn/ipcheck-demo/index.html +++ b/src/YMhut.Box.Core/Plugins/BuiltIn/bridge-capabilities-demo/index.html @@ -3,13 +3,13 @@ - IPCheck 安全网络概览 + Bridge 能力示例
-

MANIFEST V3 SAMPLE

IPCheck 安全网络概览

等待检测

+

MANIFEST V3 SAMPLE

Bridge 能力示例

等待检测

公网 IP--未读取
@@ -20,6 +20,14 @@
活动接口未授权
尚无结果
+
+ + + + + + +