release: v1.0.07 (build B1006)

This commit is contained in:
baiqing committed 2026-04-28 17:37:18 +08:00
1 parent dd46bd9807
commit e029f0bb17
9 files changed
+258 -2

No files matched your search

+5
View File
@@ -85,6 +85,11 @@ let package = Package(
dependencies: ["OpenLessPolish"],
path: "Tests/OpenLessPolishTests"
),
.testTarget(
name: "OpenLessAppTests",
dependencies: ["OpenLessApp"],
path: "Tests/OpenLessAppTests"
),
],
// 暂留 Swift 5 语言模式,避免一次性吞掉 Swift 6 严格并发的全部改造。
// 升 .v6 时需要重新审视 Recorder / VolcengineStreamingASR 等 @unchecked Sendable 类。
+2
View File
@@ -11,6 +11,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var updaterController: UpdaterController?
func applicationDidFinishLaunching(_ notification: Notification) {
VersionedPermissionRefresh().resetIfNeeded()
// 不依赖 UserPreferences flag:每次启动直接查实际权限。
// ad-hoc 签名下每次 rebuild 二进制 hash 都变,TCC 会自动失效,
// 必须在那种情况下重新进引导,否则用户看不到 menu bar 也就罢了,
@@ -0,0 +1,46 @@
import Foundation
struct TCCPermissionResetter {
static let services = [
"Accessibility",
"Microphone",
"AppleEvents",
"ListenEvent",
]
let bundleIdentifier: String
let resetService: (String, String) -> Void
init(
bundleIdentifier: String = Bundle.main.bundleIdentifier ?? "com.openless.app",
resetService: @escaping (String, String) -> Void = TCCPermissionResetter.runTCCUtilReset
) {
self.bundleIdentifier = bundleIdentifier
self.resetService = resetService
}
func resetAll() {
for service in Self.services {
resetService(service, bundleIdentifier)
}
}
private static func runTCCUtilReset(service: String, bundleIdentifier: String) {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/tccutil")
process.arguments = ["reset", service, bundleIdentifier]
do {
try process.run()
process.waitUntilExit()
if process.terminationStatus == 0 {
Log.write("[permissions] reset TCC \(service) for \(bundleIdentifier)")
} else {
Log.write("[permissions] tccutil reset \(service) for \(bundleIdentifier) exited with \(process.terminationStatus)")
}
} catch {
Log.write("[permissions] failed to run tccutil for \(service): \(error.localizedDescription)")
}
}
}
@@ -0,0 +1,58 @@
import Foundation
struct VersionedPermissionRefresh {
static let lastResetVersionKey = "openless.permissions.last_tcc_reset_version"
let currentVersion: String
let defaults: UserDefaults
let resetter: TCCPermissionResetter
let isRunningFromAppBundle: Bool
init(
currentVersion: String = VersionedPermissionRefresh.currentBundleVersion(),
defaults: UserDefaults = .standard,
resetter: TCCPermissionResetter = TCCPermissionResetter(),
isRunningFromAppBundle: Bool = VersionedPermissionRefresh.isRunningFromAppBundle()
) {
self.currentVersion = currentVersion
self.defaults = defaults
self.resetter = resetter
self.isRunningFromAppBundle = isRunningFromAppBundle
}
func resetIfNeeded() {
guard isRunningFromAppBundle else {
Log.write("[permissions] skip versioned TCC reset outside app bundle")
return
}
guard defaults.string(forKey: Self.lastResetVersionKey) != currentVersion else {
return
}
Log.write("[permissions] bundle version changed; clearing old TCC approvals before permission onboarding")
resetter.resetAll()
defaults.set(currentVersion, forKey: Self.lastResetVersionKey)
}
static func currentBundleVersion(bundle: Bundle = .main) -> String {
let info = bundle.infoDictionary
let shortVersion = info?["CFBundleShortVersionString"] as? String
let buildNumber = info?["CFBundleVersion"] as? String
switch (shortVersion, buildNumber) {
case let (.some(shortVersion), .some(buildNumber)):
return "\(shortVersion)#\(buildNumber)"
case let (.some(shortVersion), .none):
return shortVersion
case let (.none, .some(buildNumber)):
return buildNumber
case (.none, .none):
return "unknown"
}
}
static func isRunningFromAppBundle(bundle: Bundle = .main) -> Bool {
bundle.bundleURL.pathExtension == "app"
}
}
@@ -0,0 +1,24 @@
import XCTest
@testable import OpenLessApp
final class TCCPermissionResetterTests: XCTestCase {
func test_resetAllClearsEveryTCCServiceForBundleIdentifier() {
var resetCalls: [(service: String, bundleIdentifier: String)] = []
let resetter = TCCPermissionResetter(
bundleIdentifier: "com.example.openless",
resetService: { service, bundleIdentifier in
resetCalls.append((service, bundleIdentifier))
}
)
resetter.resetAll()
XCTAssertEqual(resetCalls.map(\.service), [
"Accessibility",
"Microphone",
"AppleEvents",
"ListenEvent",
])
XCTAssertEqual(resetCalls.map(\.bundleIdentifier), Array(repeating: "com.example.openless", count: 4))
}
}
@@ -0,0 +1,64 @@
import XCTest
@testable import OpenLessApp
final class VersionedPermissionRefreshTests: XCTestCase {
func test_resetIfNeededClearsPermissionsAndStoresCurrentVersionWhenVersionChanged() {
let defaults = makeDefaults()
defaults.set("1.0.0#1", forKey: VersionedPermissionRefresh.lastResetVersionKey)
var didReset = false
let refresh = VersionedPermissionRefresh(
currentVersion: "1.0.1#2",
defaults: defaults,
resetter: TCCPermissionResetter(resetService: { _, _ in didReset = true }),
isRunningFromAppBundle: true
)
refresh.resetIfNeeded()
XCTAssertTrue(didReset)
XCTAssertEqual(defaults.string(forKey: VersionedPermissionRefresh.lastResetVersionKey), "1.0.1#2")
}
func test_resetIfNeededDoesNothingWhenVersionWasAlreadyHandled() {
let defaults = makeDefaults()
defaults.set("1.0.1#2", forKey: VersionedPermissionRefresh.lastResetVersionKey)
var resetCount = 0
let refresh = VersionedPermissionRefresh(
currentVersion: "1.0.1#2",
defaults: defaults,
resetter: TCCPermissionResetter(resetService: { _, _ in resetCount += 1 }),
isRunningFromAppBundle: true
)
refresh.resetIfNeeded()
XCTAssertEqual(resetCount, 0)
XCTAssertEqual(defaults.string(forKey: VersionedPermissionRefresh.lastResetVersionKey), "1.0.1#2")
}
func test_resetIfNeededSkipsWhenNotRunningFromAppBundle() {
let defaults = makeDefaults()
var resetCount = 0
let refresh = VersionedPermissionRefresh(
currentVersion: "1.0.1#2",
defaults: defaults,
resetter: TCCPermissionResetter(resetService: { _, _ in resetCount += 1 }),
isRunningFromAppBundle: false
)
refresh.resetIfNeeded()
XCTAssertEqual(resetCount, 0)
XCTAssertNil(defaults.string(forKey: VersionedPermissionRefresh.lastResetVersionKey))
}
private func makeDefaults() -> UserDefaults {
let suiteName = "OpenLessAppTests-\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
return defaults
}
}
+35
View File
@@ -5,6 +5,41 @@
<link>https://raw.githubusercontent.com/appergb/openless/main/appcast.xml</link>
<description>OpenLess update feed</description>
<item>
<title>OpenLess 1.0.07</title>
<pubDate>Tue, 28 Apr 2026 09:36:07 +0000</pubDate>
<sparkle:version>B1006</sparkle:version>
<sparkle:shortVersionString>1.0.07</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[# OpenLess 1.0.07
build B1006 — 修复更新后权限不会重新进入授权流程的问题。
## 修复
- **新版本安装后自动清理旧权限**。OpenLess 从 `.app` bundle 启动时会比较当前版本和上次已处理版本;发现新版本后,先自动清理旧的 Accessibility、Microphone、AppleEvents、ListenEvent TCC 授权记录。
- **清理后自动进入权限请求流程**。旧权限清掉后,启动流程继续按实际授权状态判断;缺少辅助功能或麦克风权限时,会直接显示现有授权引导窗口,让用户为新版本重新授权。
- **同一版本不重复清理**。授权完成后再次打开同一版本不会反复 reset;下一个版本安装后会再次执行同样逻辑。
- **开发运行更安全**。只有从 `.app` bundle 启动时才触发版本化权限清理,避免 `swift run` 等开发调试误清理已安装应用的权限。
## 安装
**老用户**:启动 OpenLess 后自动检查更新。安装新版本并重新打开后,会按新版本权限流程重新授权。
**新用户**:到 [Releases](https://github.com/appergb/openless/releases) 下载 zip,解压拖到 `/Applications`,跑一次:
```bash
xattr -dr com.apple.quarantine /Applications/OpenLess.app
```
之后双击启动。]]></description>
<enclosure
url="https://github.com/appergb/openless/releases/download/v1.0.07/OpenLess-1.0.07.zip"
length="2803125"
type="application/octet-stream"
sparkle:edSignature="hgh2Bvjq0vYGYCgEhDm99wWgN0rnL12eKn3NwFRIY91Gb8nOdxelRcbSFXwj8CfIllHxECpbyJ3Yokvel889AQ==" />
</item>
<item>
<title>OpenLess 1.0.06</title>
<pubDate>Tue, 28 Apr 2026 05:36:01 +0000</pubDate>
+22
View File
@@ -0,0 +1,22 @@
# OpenLess 1.0.07
build B1006 — 修复更新后权限不会重新进入授权流程的问题。
## 修复
- **新版本安装后自动清理旧权限**。OpenLess 从 `.app` bundle 启动时会比较当前版本和上次已处理版本;发现新版本后,先自动清理旧的 Accessibility、Microphone、AppleEvents、ListenEvent TCC 授权记录。
- **清理后自动进入权限请求流程**。旧权限清掉后,启动流程继续按实际授权状态判断;缺少辅助功能或麦克风权限时,会直接显示现有授权引导窗口,让用户为新版本重新授权。
- **同一版本不重复清理**。授权完成后再次打开同一版本不会反复 reset;下一个版本安装后会再次执行同样逻辑。
- **开发运行更安全**。只有从 `.app` bundle 启动时才触发版本化权限清理,避免 `swift run` 等开发调试误清理已安装应用的权限。
## 安装
**老用户**:启动 OpenLess 后自动检查更新。安装新版本并重新打开后,会按新版本权限流程重新授权。
**新用户**:到 [Releases](https://github.com/appergb/openless/releases) 下载 zip,解压拖到 `/Applications`,跑一次:
```bash
xattr -dr com.apple.quarantine /Applications/OpenLess.app
```
之后双击启动。
+2 -2
View File
@@ -6,8 +6,8 @@ cd "$(dirname "$0")/.."
APP_NAME="OpenLess"
BUNDLE_ID="com.openless.app"
APP_VERSION="1.0.06"
BUILD_NUMBER="B1005"
APP_VERSION="1.0.07"
BUILD_NUMBER="B1006"
BUILD_DIR="build"
APP_DIR="${BUILD_DIR}/${APP_NAME}.app"
BIN_DIR="${APP_DIR}/Contents/MacOS"