refactor(sysproxy): 观察机写面收成意图命令,字段退回实现

This commit is contained in:
Qiu Yuzhou
2026-10-03 20:29:15 +08:00
parent f39e32ecd7
commit 31e9f6e95f
4 changed files with 243 additions and 83 deletions
@@ -69,10 +69,11 @@ extension ProxyRuntimeController {
} else {
let cleanupOutcome = await cascadeSystemProxyOffForAgentOff()
await stopAgent()
if let cleanupOutcome, cleanupOutcome.hasOperationFailure {
systemProxyObserver.systemProxyState = cleanupOutcome
if let cleanupOutcome {
systemProxyObserver.presentCleanupOutcome(cleanupOutcome)
} else {
systemProxyObserver.updateSystemProxyActions()
}
systemProxyObserver.updateSystemProxyActions()
}
}
@@ -104,47 +105,24 @@ extension ProxyRuntimeController {
guard persistSettings(next) else { return }
settings = next
if enabled {
systemProxyObserver.systemProxyInitialApplyPending = true
systemProxyObserver.systemProxyWasUnavailable = false
await systemProxyObserver.convergeSystemProxy()
systemProxyObserver.startEnabledSystemProxyObservation()
await systemProxyObserver.enableSystemProxyIntent()
} else {
systemProxyObserver.systemProxyApprovalRequired = false
systemProxyObserver.systemProxyInitialApplyPending = false
systemProxyObserver.systemProxyReadGeneration += 1
systemProxyObserver.systemProxyState =
await systemProxyObserver.clearAndStopSystemProxyObservation()
systemProxyObserver.updateSystemProxyActions()
await systemProxyObserver.disableSystemProxyIntent()
}
}
/// GUI startup restores the Agent. Healthy proxy intent only inspects; unhealthy
/// proxy intent suspends; disabled intent never writes SystemConfiguration.
func resyncOnLaunch() async {
systemProxyObserver.systemProxyStartupInProgress = true
systemProxyObserver.beginStartupResync()
guard settings.agentEnabled else {
await stopAgent()
systemProxyObserver.systemProxyStartupInProgress = false
if settings.systemProxyEnabled {
await systemProxyObserver.suspendSystemProxy()
systemProxyObserver.startEnabledSystemProxyObservation()
}
await systemProxyObserver.settleLaunchWithAgentOff()
return
}
let catalog = catalogSnapshotReader.catalogSnapshot
await convergeToReexpanded(catalog)
systemProxyObserver.systemProxyStartupInProgress = false
if settings.systemProxyEnabled {
systemProxyObserver.lastDesiredSystemProxyConfiguration = desiredSystemProxyConfiguration
if systemProxyExitAvailable {
await systemProxyObserver.recheckSystemProxy()
} else {
await systemProxyObserver.suspendSystemProxy()
}
systemProxyObserver.startEnabledSystemProxyObservation()
} else {
systemProxyObserver.systemProxyState = .idle
}
await systemProxyObserver.settleLaunchAfterAgentConverged()
}
// MARK: - 动作执行
@@ -159,23 +137,12 @@ extension ProxyRuntimeController {
/// 系统代理状态按意图事实收尾——级联后意图已关闭为空闲;残留的「意图开
/// 启 + agent 关闭」组合保持待应用(不静默清理,也无需网络观察)。
func stopAgent() async {
systemProxyNetworkChangeMonitor.stop()
systemProxyObserver.systemProxyHealthTask?.cancel()
systemProxyObserver.systemProxyHealthTask = nil
systemProxyObserver.systemProxyObservationMode = .stopped
systemProxyObserver.agentDidStop()
_ = await execute(.stop, document: nil)
state = .off
lastDocument = nil
skippedServers = []
lastActivationFailure = nil
if settings.systemProxyEnabled {
systemProxyObserver.systemProxyState = .pending
} else {
if !systemProxyState.hasOperationFailure {
systemProxyObserver.systemProxyState = .idle
}
systemProxyObserver.updateSystemProxyActions()
}
}
/// 无活动目标时 agent 仍提供本地监听(issue #60):空服务器列表文档,
@@ -50,7 +50,7 @@ extension ProxyRuntimeController {
guard settings.agentEnabled, state != .off,
let currentDocument = lastDocument ?? runtimeFileStore.loadDocument()
else {
if settings.systemProxyEnabled { systemProxyObserver.systemProxyState = .pending }
systemProxyObserver.modeTransitionAwaitingRuntime()
return
}
+140 -36
View File
@@ -14,23 +14,30 @@ final class SystemProxyObserver: ObservableObject {
}
/// Current configuration or operation result, independent of persisted intent.
@Published var systemProxyState: SystemProxyApplicationFacts = .idle
@Published private(set) var systemProxyState: SystemProxyApplicationFacts = .idle
/// Approval remains actionable even after an off-intent clear failure.
@Published var systemProxyApprovalRequired = false
@Published var systemProxyInspection = SystemProxyInspectionFacts()
@Published private(set) var systemProxyApprovalRequired = false
@Published private(set) var systemProxyInspection = SystemProxyInspectionFacts()
var lastDesiredSystemProxyConfiguration: SystemProxyConfiguration?
var knownSystemProxyServices: Set<SystemProxyServiceIdentifier> = []
var systemProxyOperationInProgress = false
var systemProxyStartupInProgress = false
var systemProxyInitialApplyPending = false
var systemProxyWasUnavailable = false
// 写面唯一入口是下方「意图命令」;private(set) 的读面仅开给健康循环体
// 与观察机测试,其余实现细节一律 private。
private var lastDesiredSystemProxyConfiguration: SystemProxyConfiguration?
private var knownSystemProxyServices: Set<SystemProxyServiceIdentifier> = []
private var systemProxyOperationInProgress = false
/// 读面:健康循环体在 launch 重同步抑制期跳过巡检。
private(set) var systemProxyStartupInProgress = false
/// 读面:健康循环的收敛条件。
private(set) var systemProxyInitialApplyPending = false
/// 读面:健康循环的收敛条件。
private(set) var systemProxyWasUnavailable = false
/// 过渡:任务柄创建仍在控制器侧,柄归属收回观察机后转 private。
var systemProxyHealthTask: Task<Void, Never>?
var systemProxyReadGeneration = 0
var systemProxyObservationMode = SystemProxyObservationMode.stopped
var systemProxyCleanupRescanRequested = false
var systemProxyCleanupTask: Task<SystemProxyApplicationFacts, Never>?
var systemProxyInspectionScheduled = false
private var systemProxyReadGeneration = 0
/// 读面:观察机测试。
private(set) var systemProxyObservationMode = SystemProxyObservationMode.stopped
private var systemProxyCleanupRescanRequested = false
private var systemProxyCleanupTask: Task<SystemProxyApplicationFacts, Never>?
private var systemProxyInspectionScheduled = false
let systemProxy: SystemProxyControlling
let systemProxyHelper: SystemProxyHelperServicing
@@ -216,28 +223,6 @@ final class SystemProxyObserver: ObservableObject {
}
}
func refreshSystemProxyApproval() {
systemProxyApprovalRequired =
systemProxyHelper.status != .approved
&& (isIntentEnabled() || systemProxyState.hasOperationFailure)
}
func updateSystemProxyActions() {
refreshSystemProxyApproval()
let available = !systemProxyOperationInProgress && !systemProxyApprovalRequired
systemProxyInspection.canRepair =
available && isIntentEnabled()
&& isExitAvailable() && systemProxyInspection.readFailure == nil
&& (!systemProxyInspection.differences.isEmpty || systemProxyState.isApplyFailure)
systemProxyInspection.canRetryClear = available && systemProxyState.isClearFailure
if systemProxyState.hasOperationFailure {
startSystemProxyHealthObservation()
} else if !isIntentEnabled() {
systemProxyHealthTask?.cancel()
systemProxyHealthTask = nil
}
}
func suspendSystemProxy() async {
guard isIntentEnabled(), !systemProxyStartupInProgress else { return }
guard !systemProxyWasUnavailable else {
@@ -296,6 +281,125 @@ final class SystemProxyObserver: ObservableObject {
}
}
// MARK: - 呈现动作与批准刷新
extension SystemProxyObserver {
func refreshSystemProxyApproval() {
systemProxyApprovalRequired =
systemProxyHelper.status != .approved
&& (isIntentEnabled() || systemProxyState.hasOperationFailure)
}
func updateSystemProxyActions() {
refreshSystemProxyApproval()
let available = !systemProxyOperationInProgress && !systemProxyApprovalRequired
systemProxyInspection.canRepair =
available && isIntentEnabled()
&& isExitAvailable() && systemProxyInspection.readFailure == nil
&& (!systemProxyInspection.differences.isEmpty || systemProxyState.isApplyFailure)
systemProxyInspection.canRetryClear = available && systemProxyState.isClearFailure
if systemProxyState.hasOperationFailure {
startSystemProxyHealthObservation()
} else if !isIntentEnabled() {
systemProxyHealthTask?.cancel()
systemProxyHealthTask = nil
}
}
}
// MARK: - 意图命令(控制器写面唯一入口)
extension SystemProxyObserver {
/// 系统代理意图开启(setSystemProxyEnabled on):本轮必须真实应用一次——
/// 清理期可能已把系统设置清空,不能凭 lastDesired 相等跳过;收敛后转入
/// enabled 观察与网络变化巡检。
func enableSystemProxyIntent() async {
systemProxyInitialApplyPending = true
systemProxyWasUnavailable = false
await convergeSystemProxy()
startEnabledSystemProxyObservation()
}
/// 系统代理意图关闭(setSystemProxyEnabled off):撤销待批准/待应用标记,
/// 读取代际推进作废在途读,无条件清理并停观察,结果原样呈现。
func disableSystemProxyIntent() async {
systemProxyApprovalRequired = false
systemProxyInitialApplyPending = false
systemProxyReadGeneration += 1
systemProxyState = await clearAndStopSystemProxyObservation()
updateSystemProxyActions()
}
/// agent 停机的观察收尾(stopAgent):停网络监视与健康循环,观察模式归
/// 零。呈现按代理意图分流——意图仍在则保持待应用(等 agent 恢复后收敛,
/// issue #71),意图已关则空闲。
func agentDidStop() {
systemProxyNetworkChangeMonitor.stop()
systemProxyHealthTask?.cancel()
systemProxyHealthTask = nil
systemProxyObservationMode = .stopped
if isIntentEnabled() {
systemProxyState = .pending
} else {
if !systemProxyState.hasOperationFailure {
systemProxyState = .idle
}
updateSystemProxyActions()
}
}
/// agent 关闭级联的清理结果呈现(setAgentEnabled off):typed 失败原样
/// 上屏(清理不重试),成功不覆盖既有呈现。
func presentCleanupOutcome(_ outcome: SystemProxyApplicationFacts) {
if outcome.hasOperationFailure {
systemProxyState = outcome
}
updateSystemProxyActions()
}
/// mode 切换发现无可收敛运行时(agent 关或无活动文档):代理意图仍开时
/// 保持待应用,等运行时恢复后由健康循环收敛(+Mode 提前出口)。
func modeTransitionAwaitingRuntime() {
if isIntentEnabled() {
systemProxyState = .pending
}
}
/// launch 重同步开始(resyncOnLaunch 入口):抑制期 converge/suspend 拒绝
/// 执行、健康循环跳过巡检;须由 settle 命令解除。
func beginStartupResync() {
systemProxyStartupInProgress = true
}
/// launch 重同步收尾(agent 意图关闭分支):解除抑制(抑制期 suspend 的
/// 守卫会拒绝执行);代理意图仍开则挂起残留配置并转入观察。
func settleLaunchWithAgentOff() async {
systemProxyStartupInProgress = false
if isIntentEnabled() {
await suspendSystemProxy()
startEnabledSystemProxyObservation()
}
}
/// launch 重同步收尾(agent 已收敛分支):解除抑制;代理意图开则播种期望
/// 配置(健康 GUI 启动只巡检不强推应用),按出口可用性巡检或挂起,转入
/// 观察;意图关呈现空闲。
func settleLaunchAfterAgentConverged() async {
systemProxyStartupInProgress = false
if isIntentEnabled() {
lastDesiredSystemProxyConfiguration = desiredConfiguration()
if isExitAvailable() {
await recheckSystemProxy()
} else {
await suspendSystemProxy()
}
startEnabledSystemProxyObservation()
} else {
systemProxyState = .idle
}
}
}
// MARK: - helper 门禁(issue #71)
extension SystemProxyObserver {
@@ -34,7 +34,6 @@ final class SystemProxyObserverTests: XCTestCase {
func testConvergeWaitsForInFlightOperationInsteadOfSkipping() async throws {
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
let observer = makeObserver(proxy: proxy, cleanupSettleNanoseconds: 1_000_000)
observer.systemProxyInitialApplyPending = true
// 拉长首次读服务的窗口,制造进行中的操作。
proxy.beforeRead = { _ = try? await Task.sleep(nanoseconds: 30_000_000) }
@@ -46,11 +45,101 @@ final class SystemProxyObserverTests: XCTestCase {
XCTAssertEqual(proxy.applied.count, 2, "并发收敛必须等操作收尾后执行而非被跳过")
}
func testEnableIntentAppliesFreshAndStartsObservation() async throws {
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
let monitor = ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor()
let observer = makeObserver(proxy: proxy, monitor: monitor, cleanupSettleNanoseconds: 1_000_000)
await observer.enableSystemProxyIntent()
XCTAssertEqual(proxy.applied.count, 1, "开启意图必须真实应用一次(清理期可能已清空系统设置)")
XCTAssertEqual(observer.systemProxyState, .applied)
XCTAssertEqual(observer.systemProxyObservationMode, .enabled)
XCTAssertEqual(monitor.startCount, 1)
}
func testDisableIntentClearsStopsObservationAndPublishesOutcome() async throws {
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
let monitor = ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor()
let observer = makeObserver(proxy: proxy, monitor: monitor, cleanupSettleNanoseconds: 1_000_000)
await observer.enableSystemProxyIntent()
await observer.disableSystemProxyIntent()
XCTAssertEqual(proxy.clearCount, 1)
XCTAssertEqual(observer.systemProxyState, .idle)
XCTAssertEqual(observer.systemProxyObservationMode, .stopped)
XCTAssertEqual(monitor.stopCount, 1)
XCTAssertFalse(observer.systemProxyApprovalRequired)
}
func testAgentStopPresentationFollowsProxyIntent() async throws {
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
let monitor = ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor()
var intentEnabled = true
let observer = makeObserver(
proxy: proxy, monitor: monitor, cleanupSettleNanoseconds: 1_000_000,
isIntentEnabled: { intentEnabled })
observer.agentDidStop()
XCTAssertEqual(observer.systemProxyState, .pending, "代理意图仍在开时,agent 停机保持待应用(issue #71)")
XCTAssertEqual(observer.systemProxyObservationMode, .stopped)
XCTAssertEqual(monitor.stopCount, 1)
intentEnabled = false
observer.agentDidStop()
XCTAssertEqual(observer.systemProxyState, .idle, "代理意图已关时,agent 停机呈现空闲")
}
func testModeTransitionAwaitingRuntimeStampsPendingOnlyUnderIntent() async throws {
var intentEnabled = false
let observer = makeObserver(
proxy: ProxyRuntimeFixture.FakeSystemProxy(), cleanupSettleNanoseconds: 1_000_000,
isIntentEnabled: { intentEnabled })
observer.modeTransitionAwaitingRuntime()
XCTAssertEqual(observer.systemProxyState, .idle, "代理意图关闭时不打待应用图章")
intentEnabled = true
observer.modeTransitionAwaitingRuntime()
XCTAssertEqual(observer.systemProxyState, .pending)
}
func testSettleLaunchAfterConvergedInspectsWithoutReapplying() async throws {
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
let observer = makeObserver(proxy: proxy, cleanupSettleNanoseconds: 1_000_000)
await observer.settleLaunchAfterAgentConverged()
XCTAssertEqual(proxy.applied.count, 0, "健康的 GUI 启动只巡检,不强推应用")
XCTAssertGreaterThanOrEqual(proxy.readCount, 1)
XCTAssertEqual(observer.systemProxyObservationMode, .enabled)
XCTAssertFalse(observer.systemProxyStartupInProgress)
// 播种 lastDesired 后,后续收敛(如网络变化回调)不得因相等而重新应用。
await observer.convergeSystemProxy()
XCTAssertEqual(proxy.applied.count, 0, "播种 lastDesired 后收敛不得重新应用")
}
func testStartupSuppressionBlocksConvergeUntilSettled() async throws {
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
let observer = makeObserver(proxy: proxy, cleanupSettleNanoseconds: 1_000_000)
observer.beginStartupResync()
await observer.convergeSystemProxy(forceApply: true)
XCTAssertEqual(proxy.applied.count, 0, "launch 重同步抑制期 converge 必须被拒绝")
await observer.settleLaunchWithAgentOff()
XCTAssertFalse(observer.systemProxyStartupInProgress)
XCTAssertEqual(observer.systemProxyState, .paused, "抑制解除后按意图挂起残留配置")
XCTAssertEqual(observer.systemProxyObservationMode, .enabled)
}
private func makeObserver(
proxy: ProxyRuntimeFixture.FakeSystemProxy,
monitor: SystemProxyNetworkChangeMonitoring =
ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor(),
cleanupSettleNanoseconds: UInt64
cleanupSettleNanoseconds: UInt64,
isIntentEnabled: @escaping @MainActor () -> Bool = { true }
) -> SystemProxyObserver {
SystemProxyObserver(
systemProxy: proxy,
@@ -59,7 +148,7 @@ final class SystemProxyObserverTests: XCTestCase {
appBundle: AppArtifact.bundle,
helperRefreshDelayNanoseconds: 1_000_000,
cleanupSettleNanoseconds: cleanupSettleNanoseconds,
isIntentEnabled: { true },
isIntentEnabled: isIntentEnabled,
isExitAvailable: { true },
desiredConfiguration: {
try? ProxyMode.direct.systemProxyConfiguration(