From dd1c9fc4c531f1aceaa6d64bbee919ec464655b7 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Tue, 09 Jun 2026 20:09:32 +0800
Subject: [PATCH] Release 7.8.26 helper app filtering

---
 src/CHANGELOG.md                        |    7 +++
 src/TODO.md                             |    6 +-
 src/Apptag/Info.plist                   |    4 +-
 src/Apptag/DataLayer.swift              |   30 ++++++++++++---
 src/Scripts/quick_search_app_name_qa.sh |   43 +++++++++++++++++++--
 src/Apptag/QuickSearch.swift            |    2 
 6 files changed, 75 insertions(+), 17 deletions(-)

diff --git a/src/Apptag/DataLayer.swift b/src/Apptag/DataLayer.swift
index d6cd436..ccbec1a 100644
--- a/src/Apptag/DataLayer.swift
+++ b/src/Apptag/DataLayer.swift
@@ -334,7 +334,7 @@
             }
         }
 
-        return deduplicated(apps).sorted {
+        return deduplicated(apps.filter { !isInternalHelperAppPath($0.path) }).sorted {
             $0.name.localizedStandardCompare($1.name) == .orderedAscending
         }
     }
@@ -363,10 +363,10 @@
         guard url.pathExtension.lowercased() == "app" else { return }
 
         let displayURL = url.standardizedFileURL
-        guard !isNestedInsideAppBundle(displayURL) else { return }
+        guard !isInternalHelperAppPath(displayURL) else { return }
 
         let resolvedURL = displayURL.resolvingSymlinksInPath().standardizedFileURL
-        guard !isNestedInsideAppBundle(resolvedURL) else { return }
+        guard !isInternalHelperAppPath(resolvedURL) else { return }
         guard seenResolvedPaths.insert(resolvedURL.path).inserted else { return }
 
         let bundle = Bundle(url: displayURL) ?? Bundle(url: resolvedURL)
@@ -624,11 +624,29 @@
     }
 
     static func isNestedInsideAppBundle(_ url: URL) -> Bool {
-        let components = url.standardizedFileURL.pathComponents
-        guard let lastAppIndex = components.lastIndex(where: { $0.lowercased().hasSuffix(".app") }) else {
+        isNestedInsideAppBundlePath(url.standardizedFileURL.path)
+    }
+
+    static func isInternalHelperAppPath(_ url: URL) -> Bool {
+        let path = url.standardizedFileURL.path
+        let lowercasedPath = path.lowercased()
+        return isNestedInsideAppBundlePath(path)
+            || lowercasedPath.contains("/contents/helpers/")
+            || lowercasedPath.contains("/contents/xpcservices/")
+            || lowercasedPath.contains("/wrapper/")
+    }
+
+    private static func isNestedInsideAppBundlePath(_ path: String) -> Bool {
+        let lowercasedPath = path.lowercased()
+        guard lowercasedPath.hasSuffix(".app") else { return false }
+        let components = lowercasedPath.split(separator: "/", omittingEmptySubsequences: true)
+        guard let lastAppIndex = components.lastIndex(where: { $0.hasSuffix(".app") }) else {
             return false
         }
-        return components[..<lastAppIndex].contains { $0.lowercased().hasSuffix(".app") }
+        if components[..<lastAppIndex].contains(where: { $0.hasSuffix(".app") }) {
+            return true
+        }
+        return lowercasedPath.range(of: ".app/", options: .caseInsensitive) != nil
     }
 
     private static func deduplicated(_ apps: [AppInfo]) -> [AppInfo] {
diff --git a/src/Apptag/Info.plist b/src/Apptag/Info.plist
index e3c01a5..dbe7ddb 100644
--- a/src/Apptag/Info.plist
+++ b/src/Apptag/Info.plist
@@ -19,9 +19,9 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>7.8.25</string>
+	<string>7.8.26</string>
 	<key>CFBundleVersion</key>
-	<string>20260609.1741</string>
+	<string>20260609.2004</string>
 	<key>LSApplicationCategoryType</key>
 	<string>public.app-category.utilities</string>
 	<key>LSMinimumSystemVersion</key>
diff --git a/src/Apptag/QuickSearch.swift b/src/Apptag/QuickSearch.swift
index 1fecfb4..ccbf454 100644
--- a/src/Apptag/QuickSearch.swift
+++ b/src/Apptag/QuickSearch.swift
@@ -295,7 +295,7 @@
 enum QuickSearchEngine {
     static func makeDocuments(apps: [AppInfo], store: TagDatabase.Store) -> [QuickSearchDocument] {
         apps
-            .filter { !AppIndexer.isNestedInsideAppBundle($0.path) }
+            .filter { !AppIndexer.isInternalHelperAppPath($0.path) }
             .map { app in
                 let localizedNames = uniqueOrdered(AppDisplayNameResolver.searchAliases(for: app))
                 let internalBundleNames = internalBundleNames(for: app)
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 0257aa3..d7b078d 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -1,5 +1,12 @@
 # TagLauncher Changelog
 
+## [7.8.26] — 2026-06-09
+
+- 加固 Quick Search wrapper/helper 过滤:除 URL 组件判断外,直接识别 `.app/.../*.app`、`/Wrapper/`、`/Contents/Helpers/`、`/Contents/XPCServices/` 等内部 app 路径,避免内层 helper 继续作为独立搜索结果显示
+- App 扫描层和 Quick Search 文档层同时应用内部 helper 过滤,确保“贝锐向日葵被控 / isunclient”这类 iOS-on-Mac wrapper 不再漏出内部英文技术名
+- Quick Search 名称 QA 增加 helper path fixture,覆盖 `/Wrapper/`、`/Contents/Helpers/` 和 XPCServices 内部 app
+- 版本号更新为 `7.8.26`,Build 更新为 `20260609.2004`
+
 ## [7.8.25] — 2026-06-09
 
 - 新增统一 App 显示名解析策略:App Grid 和 Quick Search 标题优先使用当前语种的官方本地化名,再按同语系、系统/Finder/Spotlight 可见名、英文官方名和基础 bundle 名逐级 fallback
diff --git a/src/Scripts/quick_search_app_name_qa.sh b/src/Scripts/quick_search_app_name_qa.sh
index e93b47d..f3da8c8 100755
--- a/src/Scripts/quick_search_app_name_qa.sh
+++ b/src/Scripts/quick_search_app_name_qa.sh
@@ -11,9 +11,10 @@
 rg -Fq 'bundleDisplayNames' "$DATA_LAYER"
 rg -Fq '.localizedNameKey' "$DATA_LAYER"
 rg -Fq 'isNestedInsideAppBundle' "$DATA_LAYER"
+rg -Fq 'isInternalHelperAppPath' "$DATA_LAYER"
 rg -Fq '.skipsPackageDescendants' "$DATA_LAYER"
 rg -Fq 'AppDisplayNameResolver.searchAliases' "$QUICK_SEARCH"
-rg -Fq 'AppIndexer.isNestedInsideAppBundle($0.path)' "$QUICK_SEARCH"
+rg -Fq 'AppIndexer.isInternalHelperAppPath($0.path)' "$QUICK_SEARCH"
 
 swift - "$SUNLOGIN_APP" <<'SWIFT'
 import Foundation
@@ -189,11 +190,29 @@
 }
 
 func isNestedInsideAppBundle(_ url: URL) -> Bool {
-    let components = url.standardizedFileURL.pathComponents
-    guard let lastAppIndex = components.lastIndex(where: { $0.lowercased().hasSuffix(".app") }) else {
+    isNestedInsideAppBundlePath(url.standardizedFileURL.path)
+}
+
+func isInternalHelperAppPath(_ url: URL) -> Bool {
+    let path = url.standardizedFileURL.path
+    let lowercasedPath = path.lowercased()
+    return isNestedInsideAppBundlePath(path)
+        || lowercasedPath.contains("/contents/helpers/")
+        || lowercasedPath.contains("/contents/xpcservices/")
+        || lowercasedPath.contains("/wrapper/")
+}
+
+func isNestedInsideAppBundlePath(_ path: String) -> Bool {
+    let lowercasedPath = path.lowercased()
+    guard lowercasedPath.hasSuffix(".app") else { return false }
+    let components = lowercasedPath.split(separator: "/", omittingEmptySubsequences: true)
+    guard let lastAppIndex = components.lastIndex(where: { $0.hasSuffix(".app") }) else {
         return false
     }
-    return components[..<lastAppIndex].contains { $0.lowercased().hasSuffix(".app") }
+    if components[..<lastAppIndex].contains(where: { $0.hasSuffix(".app") }) {
+        return true
+    }
+    return lowercasedPath.range(of: ".app/", options: .caseInsensitive) != nil
 }
 
 func spotlightDisplayName(for url: URL) -> String? {
@@ -215,7 +234,7 @@
 
 func makeDocumentPaths(appPaths: [URL]) -> [String] {
     appPaths
-        .filter { !isNestedInsideAppBundle($0) }
+        .filter { !isInternalHelperAppPath($0) }
         .map(\.path)
 }
 
@@ -290,11 +309,25 @@
     guard isNestedInsideAppBundle(nestedURL) else {
         fail("nested helper app was not classified as nested: \(nestedURL.path)")
     }
+    guard isInternalHelperAppPath(nestedURL) else {
+        fail("nested helper app was not classified as internal helper: \(nestedURL.path)")
+    }
     guard bundleDisplayName(for: nestedURL) == expectedDisplayName else {
         fail("nested helper bundle display name did not expose localized name")
     }
 }
 
+let helperPathFixtures = [
+    "/Applications/Outer.app/Contents/Helpers/Inner.app",
+    "/Applications/Outer.app/Contents/XPCServices/Inner.app",
+    "/Applications/Outer.app/Wrapper/Inner.app"
+]
+for helperPath in helperPathFixtures {
+    guard isInternalHelperAppPath(URL(fileURLWithPath: helperPath)) else {
+        fail("helper path fixture was not filtered: \(helperPath)")
+    }
+}
+
 var enumeratedHits: [String] = []
 if let enumerator = FileManager.default.enumerator(
     at: URL(fileURLWithPath: "/Applications"),
diff --git a/src/TODO.md b/src/TODO.md
index 81984bb..08a61ca 100644
--- a/src/TODO.md
+++ b/src/TODO.md
@@ -24,10 +24,10 @@
 ## Done
 
 - [2026-06-09] 多语种 App 显示名通用加固。
-  - 结果: 新增统一 `AppDisplayNameResolver`;App Grid 与 Quick Search 标题优先走当前语种/同语系/系统可见名/英文官方名/基础 bundle 名 fallback,内部 bundle 名和 bundle id 只作为搜索别名。
-  - 范围: `AppInfo` 增加系统显示名与 bundle 显示名候选;Quick Search 文档使用 resolver 生成别名并二次过滤嵌套 `.app`;版本更新为 `7.8.25 (20260609.1741)`。
+  - 结果: 新增统一 `AppDisplayNameResolver`;App Grid 与 Quick Search 标题优先走当前语种/同语系/系统可见名/英文官方名/基础 bundle 名 fallback,内部 bundle 名和 bundle id 只作为搜索别名;`7.8.26` 进一步加固 `/Wrapper/`、`/Contents/Helpers/`、`.app/.../*.app` 内部 helper 过滤。
+  - 范围: `AppInfo` 增加系统显示名与 bundle 显示名候选;App 扫描层和 Quick Search 文档层都过滤内部 helper;Quick Search 文档使用 resolver 生成别名;版本更新为 `7.8.26 (20260609.2004)`。
   - 不改范围: 未做第三方 App 名称机器翻译,未改 Quick Search 排名算法、App Grid 布局、窗口层级或 Smart Start 分类规则。
-  - 验证: `APP_BUILD=20260609.1741 bash build.sh`、`codesign --verify --deep --strict src/build/TagLauncher.app`、`Scripts/quick_search_app_name_qa.sh`、`Scripts/quick_search_system_app_qa.sh` 通过;构建产物 Info.plist 为 `7.8.25 (20260609.1741)`。
+  - 验证: `APP_BUILD=20260609.2004 bash build.sh`、`codesign --verify --deep --strict src/build/TagLauncher.app`、`Scripts/quick_search_app_name_qa.sh`、`Scripts/quick_search_system_app_qa.sh` 通过;构建产物 Info.plist 为 `7.8.26 (20260609.2004)`。
 
 - [2026-06-09] 修复首安 Smart Start 提示确认后 App Grid 消失的问题。
   - 结果: Smart Start 自动整理完成提示出现期间会抑制 AppKit 级 backdrop dismiss;点击“OK/好的”只关闭提示弹窗,不再关闭整个 App Grid。

--
Gitblit v1.9.3