diff --git a/ShadowsocksX-NG2/App/HomeView+Cards.swift b/ShadowsocksX-NG2/App/HomeView+Cards.swift index 8f3df707..1e6ea82a 100644 --- a/ShadowsocksX-NG2/App/HomeView+Cards.swift +++ b/ShadowsocksX-NG2/App/HomeView+Cards.swift @@ -217,7 +217,7 @@ struct TerminalProxyEnvironmentCard: View { } ) .onAppear { - control.refreshCommandAddresses() + control.refreshTerminalCommands() } .onDisappear { feedbackTask?.cancel() @@ -261,7 +261,7 @@ struct TerminalProxyEnvironmentCard: View { private func copy(_ shell: TerminalCommandShell) { // 复制前刷新(issue #72):失效选择回退后的命令即剪贴板内容,与按钮 // 提示保持同一选择。 - let commands = control.refreshCommandAddresses() + let commands = control.refreshTerminalCommands() do { try clipboard.write(shell.command(from: commands)) feedbackTask?.cancel() diff --git a/ShadowsocksX-NG2/App/ProxyControlWorkflow/ProxyControlAdapters.swift b/ShadowsocksX-NG2/App/ProxyControlWorkflow/ProxyControlAdapters.swift index 8aef11c6..1746b034 100644 --- a/ShadowsocksX-NG2/App/ProxyControlWorkflow/ProxyControlAdapters.swift +++ b/ShadowsocksX-NG2/App/ProxyControlWorkflow/ProxyControlAdapters.swift @@ -81,8 +81,8 @@ final class ControllerProxyRuntimeAdapter: ProxyRuntimeAdapting { } } -/// HTTP 导出能力的唯一派生点:监听设置 → 本机终端使用的可复制导出行。 -/// 环境变量始终指向回环地址,避免将命令误用于其他设备。 +/// HTTP 导出能力的唯一派生点:监听设置 → 可复制的 http/https 双导出行。 +/// 此导出行始终指向回环地址(与首页完整命令的命令地址选择无关,issue #72)。 extension HTTPExportCapability { init(listen: SslocalListenSettings) { let urlHost = listen.proxyLoopbackURLHost diff --git a/ShadowsocksX-NG2/App/ProxyControlWorkflow/ProxyControlWorkflow.swift b/ShadowsocksX-NG2/App/ProxyControlWorkflow/ProxyControlWorkflow.swift index 58aa4c44..4271f0a0 100644 --- a/ShadowsocksX-NG2/App/ProxyControlWorkflow/ProxyControlWorkflow.swift +++ b/ShadowsocksX-NG2/App/ProxyControlWorkflow/ProxyControlWorkflow.swift @@ -226,7 +226,7 @@ final class ProxyControlWorkflow: ObservableObject { /// 事实变化通知自动刷新)。返回刷新后的两种 shell 命令供剪贴板副作用 /// 使用——失效选择已在本次观察中回退,提示投影与返回值一致。 @discardableResult - func refreshCommandAddresses() -> TerminalProxyEnvironmentCommands { + func refreshTerminalCommands() -> TerminalProxyEnvironmentCommands { republish().terminalProxyEnvironmentCommands } diff --git a/ShadowsocksX-NG2/App/ProxyControlWorkflow/SystemInterfaceFactsProvider.swift b/ShadowsocksX-NG2/App/ProxyControlWorkflow/SystemInterfaceFactsProvider.swift index 6c7429fa..ef21b275 100644 --- a/ShadowsocksX-NG2/App/ProxyControlWorkflow/SystemInterfaceFactsProvider.swift +++ b/ShadowsocksX-NG2/App/ProxyControlWorkflow/SystemInterfaceFactsProvider.swift @@ -28,6 +28,8 @@ final class SystemInterfaceFactsProvider: LocalInterfaceFactsReading { } private func startObserving() { + // 变化通知是尽力而为的增强:监视创建失败(罕见)只失去推送刷新, + // 进入首页与复制前刷新仍保证候选新鲜。 var context = SCDynamicStoreContext( version: 0, info: Unmanaged.passUnretained(observation).toOpaque(), diff --git a/ShadowsocksX-NG2/App/ProxyControlWorkflow/TerminalCommandAddress.swift b/ShadowsocksX-NG2/App/ProxyControlWorkflow/TerminalCommandAddress.swift index ce2ae35d..9b043383 100644 --- a/ShadowsocksX-NG2/App/ProxyControlWorkflow/TerminalCommandAddress.swift +++ b/ShadowsocksX-NG2/App/ProxyControlWorkflow/TerminalCommandAddress.swift @@ -85,24 +85,19 @@ enum TerminalCommandAddressPolicy { return loopbacks + interfaceCandidates(mode: mode, interfaces: interfaces) } - /// 当前监听方式的默认回环地址(与 ListenerMode.proxyLoopbackAddress 一致)。 + /// 当前监听方式的默认回环地址(Domain 回环清单首项)。 static func defaultLoopback(for mode: ListenerMode) -> TerminalCommandAddress { - TerminalCommandAddress(bsdName: "lo0", address: mode.proxyLoopbackAddress, displayName: "lo0") + TerminalCommandAddress( + bsdName: "lo0", address: mode.proxyLoopbackAddress, displayName: "lo0") } // MARK: - 派生细节 + /// 回环候选(置顶)直接来自 Domain 的监听方式回环清单,展示名固定 lo0。 private static func loopbackCandidates(for mode: ListenerMode) -> [TerminalCommandAddress] { - let addresses: [String] - switch mode { - case .localhost, .allIPv4Interfaces: - addresses = ["127.0.0.1"] - case .allIPv4AndIPv6Interfaces: - addresses = ["127.0.0.1", "::1"] - case .allIPv6Interfaces: - addresses = ["::1"] + mode.compatibleLoopbackAddresses.map { + TerminalCommandAddress(bsdName: "lo0", address: $0, displayName: "lo0") } - return addresses.map { TerminalCommandAddress(bsdName: "lo0", address: $0, displayName: "lo0") } } /// 待排序的接口分组:显示名 + BSD 名 + 该接口的地址候选。 diff --git a/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift b/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift index 845ae8bf..8739b76d 100644 --- a/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift +++ b/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift @@ -25,8 +25,19 @@ enum ListenerMode: String, Codable, CaseIterable, Equatable, Sendable { } } + /// 此监听方式可接受的回环地址(默认地址在前):IPv4/仅本机只接受 + /// 127.0.0.1,双栈通配绑定同时接受两个回环族,仅 IPv6 只接受 ::1。 + var compatibleLoopbackAddresses: [String] { + switch self { + case .localhost, .allIPv4Interfaces: ["127.0.0.1"] + case .allIPv4AndIPv6Interfaces: ["127.0.0.1", "::1"] + case .allIPv6Interfaces: ["::1"] + } + } + + /// 健康探测、系统代理与 HTTP 导出行使用的默认兼容回环地址。 var proxyLoopbackAddress: String { - self == .allIPv6Interfaces ? "::1" : "127.0.0.1" + compatibleLoopbackAddresses[0] } var bindingHint: String { diff --git a/ShadowsocksX-NG2/Tests/ProxyCommandAddressWorkflowTests.swift b/ShadowsocksX-NG2/Tests/ProxyCommandAddressWorkflowTests.swift index d63d43ba..a130e483 100644 --- a/ShadowsocksX-NG2/Tests/ProxyCommandAddressWorkflowTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyCommandAddressWorkflowTests.swift @@ -233,7 +233,7 @@ final class ProxyCommandAddressLifecycleTests: XCTestCase { let wifi = workflow.snapshot.commandAddressPicker.candidates[1] _ = workflow.selectCommandAddress(wifi) - _ = workflow.refreshCommandAddresses() // 离开再进入首页 + _ = workflow.refreshTerminalCommands() // 离开再进入首页 XCTAssertEqual( workflow.snapshot.commandAddressPicker.selected.identity, wifi.identity, @@ -330,7 +330,7 @@ final class ProxyCommandAddressLifecycleTests: XCTestCase { // 复制前地址消失且变化通知丢失:复制前刷新发现失效。 interfaceFacts.interfaces = [] - let prepared = workflow.refreshCommandAddresses() + let prepared = workflow.refreshTerminalCommands() XCTAssertFalse( prepared.zshBash.contains("192.168.1.10"), "复制内容不包含已消失的地址") @@ -348,7 +348,7 @@ final class ProxyCommandAddressLifecycleTests: XCTestCase { runtime.runtimeFacts = ProxyRuntimeFacts(status: .off, isOn: false) runtime.emitChange() - let prepared = workflow.refreshCommandAddresses() + let prepared = workflow.refreshTerminalCommands() XCTAssertTrue(prepared.zshBash.contains("127.0.0.1"), "代理未运行时命令仍可准备") XCTAssertEqual(runtime.resyncCount, 0, "刷新不触碰运行时生命周期")