feat(ng2): add Nushell and Windows proxy environment commands

This commit is contained in:
Qiu Yuzhou
2026-10-14 12:27:25 +08:00
parent 1353dc6292
commit 7cf04544ce
9 changed files with 288 additions and 49 deletions
+1 -1
View File
@@ -219,7 +219,7 @@ _避免用词_:监听范围(Listen scope)。
仅设置本机 HTTP/HTTPS 代理环境变量的终端命令,目标为本地监听入口。
**终端代理环境命令(Terminal proxy environment commands)**:
适用于 zsh/bash 和 fish 的命令,通过 HTTP 与 SOCKS 入口设置代理环境变量,并设置本地主机例外。它只影响遵循这些环境变量的子进程。
按目标 shell 语法设置 HTTP 与 SOCKS 代理环境变量及本地主机例外的命令。它只影响遵循这些环境变量的子进程。
**终端命令地址(Terminal command address)**:
复制的终端代理环境命令中,HTTP 与 SOCKS 入口共同使用的 IP 地址。它是当前应用会话内的选择,与监听设置和系统代理设置独立。
@@ -108,8 +108,8 @@ extension HTTPExportCapability {
}
/// 终端代理环境变量命令的唯一派生点:已保存监听事实 + 生效
/// 命令地址 → 两种 shell 的可复制命令。HTTP/HTTPS 用已保存 HTTP 端口,
/// SOCKS 用已保存 SOCKS 端口;变量集合、小写变量名、单引号转义、socks5
/// 命令地址 → 各种 shell 的可复制命令。HTTP/HTTPS 用已保存 HTTP 端口,
/// SOCKS 用已保存 SOCKS 端口;变量集合、小写变量名、socks5
/// 协议与 no_proxy 内容保持不变;IPv6 URL 使用方括号。
extension TerminalProxyEnvironmentCommands {
init(
@@ -140,30 +140,38 @@ extension TerminalProxyEnvironmentCommands {
}
userInfo = "\(username):\(encodedPassword)@"
} catch {
self.init(zshBash: "", fish: "", failure: .credentialUnreadable)
self.init(
zshBash: "", fish: "", nushell: "", windowsCMD: "", windowsPowerShell: "",
failure: .credentialUnreadable)
return
}
}
let httpEndpoint = "'http://\(userInfo)\(urlHost):\(httpPort)'"
let socksEndpoint = "'socks5://\(userInfo)\(urlHost):\(socksPort)'"
let noProxy = "'localhost,127.0.0.1,::1,.local'"
let zshBash =
[
"export http_proxy=\(httpEndpoint)",
"export https_proxy=\(httpEndpoint)",
"export all_proxy=\(socksEndpoint)",
"export no_proxy=\(noProxy)",
].joined(separator: "; ") + ";"
let fish =
[
"set -gx http_proxy \(httpEndpoint)",
"set -gx https_proxy \(httpEndpoint)",
"set -gx all_proxy \(socksEndpoint)",
"set -gx no_proxy \(noProxy)",
].joined(separator: "; ") + ";"
let httpEndpoint = "http://\(userInfo)\(urlHost):\(httpPort)"
let socksEndpoint = "socks5://\(userInfo)\(urlHost):\(socksPort)"
let variables = [
(name: "http_proxy", value: httpEndpoint),
(name: "https_proxy", value: httpEndpoint),
(name: "all_proxy", value: socksEndpoint),
(name: "no_proxy", value: "localhost,127.0.0.1,::1,.local"),
]
let windowsAssignments = variables.map { "set \"\($0.name)=\($0.value)\"" }
// CMD 会把 %XX% 当作环境变量展开。用交互 FOR 变量插入字面百分号,
// 不写临时环境变量;逐行赋值使最长允许凭据仍不超过单行长度限制。
let windowsCMD =
userInfo.contains("%")
? windowsAssignments.map { assignment in
assignment.contains("%")
? "for %P in (\"%\") do \(assignment.replacingOccurrences(of: "%", with: "%~P"))"
: assignment
}.joined(separator: "\r\n")
: windowsAssignments.joined(separator: " & ")
self.init(
zshBash: zshBash,
fish: fish)
zshBash: variables.map { "export \($0.name)='\($0.value)'" }.joined(separator: "; ") + ";",
fish: variables.map { "set -gx \($0.name) '\($0.value)'" }.joined(separator: "; ") + ";",
nushell: variables.map { "$env.\($0.name) = '\($0.value)'" }.joined(separator: "; ") + ";",
windowsCMD: windowsCMD,
windowsPowerShell:
variables.map { "$env:\($0.name) = '\($0.value)'" }.joined(separator: "; ") + ";")
}
}
@@ -13,14 +13,34 @@ struct HTTPExportCapability: Equatable, Sendable {
/// 首页可复制的 shell 环境变量设置命令。不同 shell 的语法由 workflow 一次投影,
/// 呈现层只选择格式并执行剪贴板副作用。
enum TerminalCommandFailure: Error, Equatable, Sendable { case credentialUnreadable }
enum TerminalCommandFailure: Error, Equatable, Sendable {
case credentialUnreadable
case addressUnavailable
}
struct TerminalProxyEnvironmentCommands: Equatable, Sendable {
let zshBash: String
let fish: String
let nushell: String
let windowsCMD: String
let windowsPowerShell: String
var failure: TerminalCommandFailure?
}
enum TerminalCommandShell: CaseIterable, Hashable, Sendable {
case zshBash, fish, nushell, windowsCMD, windowsPowerShell
func command(from commands: TerminalProxyEnvironmentCommands) -> String {
switch self {
case .zshBash: commands.zshBash
case .fish: commands.fish
case .nushell: commands.nushell
case .windowsCMD: commands.windowsCMD
case .windowsPowerShell: commands.windowsPowerShell
}
}
}
/// 活动目标安全事实:存在性 + 可展示路径摘要。身份是不透明
/// `NodeID`(级联勾选用);不含凭据、原始配置 URL 或目录树结构。路径摘要
/// 是目录 projection 的显示名(导入缺名的服务器叶子以地址端口为名称,与目录树
@@ -65,9 +85,15 @@ struct ProxyControlSnapshot: Equatable, Sendable {
let httpExport: HTTPExportCapability
/// 首页命令地址选择器:可见性、候选与生效选择。
let commandAddressPicker: TerminalCommandAddressPicker
/// 首页终端代理环境变量命令,分别适用于 zsh/bash 与 fish;端点地址为生效
/// 首页终端代理环境变量命令;端点地址为生效
/// 命令地址选择,端口取已保存监听事实。
let terminalProxyEnvironmentCommands: TerminalProxyEnvironmentCommands
/// Windows 格式供其他设备使用,只在局域网入口与网络地址都有效时提供。
var terminalCommandShells: [TerminalCommandShell] {
commandAddressPicker.isVisible && commandAddressPicker.selected.address != "127.0.0.1"
? TerminalCommandShell.allCases : [.zshBash, .fish, .nushell]
}
}
// MARK: - 适配缝
@@ -248,13 +274,25 @@ final class ProxyControlWorkflow: ObservableObject {
}
/// 刷新命令地址候选并整体重发布(进入首页与复制前调用;网络变化经接口
/// 事实变化通知自动刷新)。返回刷新后的两种 shell 命令供剪贴板副作用
/// 使用——失效选择已在本次观察中回退,提示投影与返回值一致。
/// 事实变化通知自动刷新)。失效选择回退到本地地址。
@discardableResult
func refreshTerminalCommands() -> TerminalProxyEnvironmentCommands {
republish().terminalProxyEnvironmentCommands
}
/// 复制前核实按钮呈现时的 IP 仍有效;回退只更新界面,不改写剪贴板。
/// 显式传入原地址身份,防止网络通知先回退 snapshot 后复制旧按钮。
func prepareTerminalCommandsForCopy(expectedAddress: TerminalCommandAddressIdentity) throws
-> TerminalProxyEnvironmentCommands
{
let commands = refreshTerminalCommands()
guard snapshot.commandAddressPicker.selected.identity == expectedAddress else {
throw TerminalCommandFailure.addressUnavailable
}
if let failure = commands.failure { throw failure }
return commands
}
// MARK: - 整体观察(implementation,UI 不可见)
/// 一次 observation 内整体拼装 snapshot;任何单一事实变化后整体替换,
@@ -67,6 +67,11 @@ extension AppPresentation {
return systemProxy(SystemProxyOperationFailure(error))
case let error as TextClipboardFailure:
return textClipboard(error)
case let error as TerminalCommandFailure:
switch error {
case .credentialUnreadable: return "局域网身份验证密码不可读,请重试。"
case .addressUnavailable: return "命令地址已失效,请重新选择 IP。"
}
case let error as ImageClipboardFailure:
return imageClipboard(error)
case let error as QrImageSaveFailure:
@@ -1,7 +1,7 @@
import SwiftUI
/// 「复制代理环境变量设置命令」卡:按所选 shell 格式复制 HTTP 与 SOCKS 环境变量。
/// 局域网开启时以单选下拉框选择命令地址,两种 shell
/// 局域网开启时以单选下拉框选择命令地址,各种 shell
/// 的全部代理端点共用该选择;局域网关闭时隐藏下拉框并使用本地入口。
struct TerminalProxyEnvironmentCard: View {
@ObservedObject var control: ProxyControlWorkflow
@@ -59,8 +59,9 @@ struct TerminalProxyEnvironmentCard: View {
Text("局域网身份验证密码不可读,请重试。")
.font(.caption).foregroundStyle(.red)
}
commandButton(for: .zshBash)
commandButton(for: .fish)
ForEach(control.snapshot.terminalCommandShells, id: \.self) { shell in
commandButton(for: shell)
}
}
.padding(.top, 6)
}
@@ -84,8 +85,9 @@ struct TerminalProxyEnvironmentCard: View {
private func commandButton(for shell: TerminalCommandShell) -> some View {
let isCopied = copiedShell == shell
let expectedAddress = control.snapshot.commandAddressPicker.selected.identity
return Button {
copy(shell)
copy(shell, expectedAddress: expectedAddress)
} label: {
HStack(spacing: 10) {
Image(systemName: isCopied ? "checkmark" : "doc.on.doc")
@@ -109,12 +111,10 @@ struct TerminalProxyEnvironmentCard: View {
.help(shell.command(from: control.snapshot.terminalProxyEnvironmentCommands))
}
private func copy(_ shell: TerminalCommandShell) {
// 复制前刷新:失效选择回退后的命令即剪贴板内容,与按钮
// 提示保持同一选择。
let commands = control.refreshTerminalCommands()
private func copy(_ shell: TerminalCommandShell, expectedAddress: TerminalCommandAddressIdentity)
{
do {
if let failure = commands.failure { throw failure }
let commands = try control.prepareTerminalCommandsForCopy(expectedAddress: expectedAddress)
try clipboard.write(shell.command(from: commands))
feedbackTask?.cancel()
copiedShell = shell
@@ -133,21 +133,14 @@ struct TerminalProxyEnvironmentCard: View {
}
}
private enum TerminalCommandShell: Equatable {
case zshBash
case fish
var title: String {
extension TerminalCommandShell {
fileprivate var title: String {
switch self {
case .zshBash: "zsh / bash"
case .fish: "fish"
}
}
func command(from commands: TerminalProxyEnvironmentCommands) -> String {
switch self {
case .zshBash: commands.zshBash
case .fish: commands.fish
case .nushell: "Nushell"
case .windowsCMD: "Windows:CMD"
case .windowsPowerShell: "Windows:PowerShell"
}
}
}
@@ -5,6 +5,53 @@ import XCTest
@MainActor
struct LANCommandEnablementTests {
@Test(arguments: [false, true])
func copyRejectsLostIPIncludingAButtonRenderedBeforeNetworkNotification(
notificationDelivered: Bool
) {
let runtime = FakeProxyRuntime()
let interfaces = FakeLocalInterfaceFacts()
runtime.listenFacts.lan.isEnabled = true
interfaces.interfaces = [FakeLocalInterfaceFacts.wifi(addresses: [.ipv4("192.0.2.2")])]
let workflow = ProxyControlWorkflow(
runtime: runtime, targetFacts: FakeTargetFacts(), interfaceFacts: interfaces)
let address = workflow.snapshot.commandAddressPicker.candidates[1]
workflow.selectCommandAddress(address)
interfaces.interfaces = []
if notificationDelivered { interfaces.emitChange() }
#expect(throws: TerminalCommandFailure.addressUnavailable) {
try workflow.prepareTerminalCommandsForCopy(expectedAddress: address.identity)
}
#expect(workflow.snapshot.commandAddressPicker.selected.address == "127.0.0.1")
#expect(workflow.snapshot.terminalCommandShells == [.zshBash, .fish, .nushell])
#expect(runtime.agentCommands.isEmpty)
#expect(runtime.systemProxyCommands.isEmpty)
}
@Test func windowsFormatsRequireEnabledLANAndSelectedNetworkIPWithoutRequiringRunningProxy() {
let runtime = FakeProxyRuntime()
let interfaces = FakeLocalInterfaceFacts()
let workflow = ProxyControlWorkflow(
runtime: runtime, targetFacts: FakeTargetFacts(), interfaceFacts: interfaces)
#expect(workflow.snapshot.terminalCommandShells == [.zshBash, .fish, .nushell])
runtime.listenFacts.lan.isEnabled = true
interfaces.interfaces = [FakeLocalInterfaceFacts.wifi(addresses: [.ipv4("192.0.2.2")])]
runtime.emitChange()
#expect(workflow.snapshot.terminalCommandShells == [.zshBash, .fish, .nushell])
workflow.selectCommandAddress(workflow.snapshot.commandAddressPicker.candidates[1])
runtime.runtimeFacts = ProxyRuntimeFacts(status: .off, isOn: false)
runtime.emitChange()
#expect(
workflow.snapshot.terminalCommandShells == [
.zshBash, .fish, .nushell, .windowsCMD, .windowsPowerShell,
])
runtime.listenFacts.lan.isEnabled = false
runtime.emitChange()
#expect(workflow.snapshot.terminalCommandShells == [.zshBash, .fish, .nushell])
#expect(runtime.agentCommands.isEmpty)
#expect(runtime.systemProxyCommands.isEmpty)
}
@Test func disablingLANRetainsIPv6ModeAndFallsBackFromSelectedNetworkAddress() {
let runtime = FakeProxyRuntime()
let interfaces = FakeLocalInterfaceFacts()
@@ -115,7 +115,22 @@ final class ProxyControlWorkflowTests: XCTestCase {
"set -gx http_proxy 'http://127.0.0.1:11087'; "
+ "set -gx https_proxy 'http://127.0.0.1:11087'; "
+ "set -gx all_proxy 'socks5://127.0.0.1:11086'; "
+ "set -gx no_proxy 'localhost,127.0.0.1,::1,.local';")
+ "set -gx no_proxy 'localhost,127.0.0.1,::1,.local';",
nushell:
"$env.http_proxy = 'http://127.0.0.1:11087'; "
+ "$env.https_proxy = 'http://127.0.0.1:11087'; "
+ "$env.all_proxy = 'socks5://127.0.0.1:11086'; "
+ "$env.no_proxy = 'localhost,127.0.0.1,::1,.local';",
windowsCMD:
"set \"http_proxy=http://127.0.0.1:11087\" & "
+ "set \"https_proxy=http://127.0.0.1:11087\" & "
+ "set \"all_proxy=socks5://127.0.0.1:11086\" & "
+ "set \"no_proxy=localhost,127.0.0.1,::1,.local\"",
windowsPowerShell:
"$env:http_proxy = 'http://127.0.0.1:11087'; "
+ "$env:https_proxy = 'http://127.0.0.1:11087'; "
+ "$env:all_proxy = 'socks5://127.0.0.1:11086'; "
+ "$env:no_proxy = 'localhost,127.0.0.1,::1,.local';")
// MARK: - 整体 snapshot
@@ -4,6 +4,57 @@ import Testing
@testable import ShadowsocksX_NG2
struct SeparateListenerTests {
@Test func newFormatsPreserveEncodedLANCredentialsAndBracketIPv6() {
var settings = SslocalListenSettings(listenerMode: .allIPv4AndIPv6Interfaces)
settings.lan.authenticationEnabled = true
settings.lan.username = "u @"
let commands = TerminalProxyEnvironmentCommands(
listen: RuntimeListenFacts(listen: settings),
commandAddress: TerminalCommandAddress(
bsdName: "en0", address: "2001:db8::2", displayName: "Wi-Fi"),
password: { "p':@/$%!\"&^()" })
let http = "http://u%20%40:p%27%3A%40%2F%24%25%21%22%26%5E%28%29@[2001:db8::2]:11089"
#expect(commands.nushell.contains("$env.http_proxy = '\(http)'"))
#expect(commands.windowsPowerShell.contains("$env:http_proxy = '\(http)'"))
let cmdUserInfo = "u%~P20%~P40:p%~P27%~P3A%~P40%~P2F%~P24%~P25%~P21%~P22%~P26%~P5E%~P28%~P29"
#expect(
commands.windowsCMD
== "for %P in (\"%\") do set \"http_proxy=http://\(cmdUserInfo)@[2001:db8::2]:11089\"\r\n"
+ "for %P in (\"%\") do set \"https_proxy=http://\(cmdUserInfo)@[2001:db8::2]:11089\"\r\n"
+ "for %P in (\"%\") do set \"all_proxy=socks5://\(cmdUserInfo)@[2001:db8::2]:11088\"\r\n"
+ "set \"no_proxy=localhost,127.0.0.1,::1,.local\"")
#expect(commands.failure == nil)
}
@Test func windowsFormatsSetFourSessionVariablesUsingLANPorts() {
let commands = TerminalProxyEnvironmentCommands(
listen: RuntimeListenFacts(listen: SslocalListenSettings(listenerMode: .allIPv4Interfaces)),
commandAddress: TerminalCommandAddress(
bsdName: "en0", address: "192.0.2.2", displayName: "Wi-Fi"))
#expect(
commands.windowsCMD == "set \"http_proxy=http://192.0.2.2:11089\" & "
+ "set \"https_proxy=http://192.0.2.2:11089\" & "
+ "set \"all_proxy=socks5://192.0.2.2:11088\" & "
+ "set \"no_proxy=localhost,127.0.0.1,::1,.local\"")
#expect(
commands.windowsPowerShell == "$env:http_proxy = 'http://192.0.2.2:11089'; "
+ "$env:https_proxy = 'http://192.0.2.2:11089'; "
+ "$env:all_proxy = 'socks5://192.0.2.2:11088'; "
+ "$env:no_proxy = 'localhost,127.0.0.1,::1,.local';")
}
@Test func nushellSetsFourSessionVariablesUsingLocalPorts() {
let commands = TerminalProxyEnvironmentCommands(
listen: RuntimeListenFacts(listen: SslocalListenSettings()),
commandAddress: TerminalCommandAddress(
bsdName: "lo0", address: "127.0.0.1", displayName: "lo0"))
#expect(
commands.nushell == "$env.http_proxy = 'http://127.0.0.1:11087'; "
+ "$env.https_proxy = 'http://127.0.0.1:11087'; "
+ "$env.all_proxy = 'socks5://127.0.0.1:11086'; "
+ "$env.no_proxy = 'localhost,127.0.0.1,::1,.local';")
}
@Test func disabledIPv6ConfigurationRoundTripsWithoutAffectingEffectiveRuntime() throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
@@ -0,0 +1,82 @@
# 首页终端代理环境命令格式扩展
日期:2026-10-14。
本文记录本次设计访谈与实现验证,不作为持续更新的现状依据。当前地址、端口和身份验证规则以代码与 ADR-0034 为准。
## 需求
- 首页复制代理环境变量设置命令支持 Nushell。
- 局域网监听开启且选择局域网 IP 时,支持 Windows:CMD 和 Windows:PowerShell。
## 已确认的决策
用户在第一轮访谈中选择全部推荐项。
- Nushell 始终显示,支持本地地址与局域网地址。
- Windows 格式仅在局域网监听开启且选择局域网 IP 时显示;条件不满足时隐藏。
- 代理停止时仍允许复制,沿用已保存的监听设置。
- 命令供用户直接粘贴到当前终端会话执行;不增加持久化命令,不支持 CMD `.bat` 文件。
- 新格式沿用四个小写变量:`http_proxy`、`https_proxy`、`all_proxy`、`no_proxy`。
- `no_proxy` 沿用 `localhost,127.0.0.1,::1,.local`。
- 局域网命令沿用现有端口与凭据规则;IPv6 URL 使用方括号。
- 保留纵向复制按钮。顺序为 `zsh / bash`、`fish`、`Nushell`、`Windows:CMD`、`Windows:PowerShell`,最多五个按钮。
- 保留完整命令 tooltip 和按钮内“已复制”反馈。
- 第二轮用户指定:复制前刷新发现所选 IP 失效时,所有格式统一停止本次复制,保留剪贴板,并提示重新选择 IP。不得把回退后的本地地址静默写入剪贴板。地址候选刷新和 Windows 格式显隐仍按最新监听事实更新。
- 第二轮用户选择本地验证:执行本地测试与构建,覆盖格式生成、显示条件、凭据编码和地址失效。真实 Windows 执行与跨设备代理连接标为未验证,不列入本次完成条件。
## 验收条件
- 本地地址下提供 zsh/bash、fish、Nushell 三个按钮。
- 局域网监听开启且选择有效局域网 IP 时,额外提供两个 Windows 格式按钮;代理停止不影响这一条件。
- 新格式复制四个小写变量,HTTP/HTTPS 与 SOCKS 使用对应入口,`no_proxy` 内容不变。
- 启用局域网认证时,URL 保留完整且正确编码的凭据;读取失败停止复制。
- 所选 IP 在复制前失效时,每一种格式都停止本次复制,不改写剪贴板,不显示“已复制”,提示“命令地址已失效,请重新选择 IP。”。
- tooltip 与对应格式的复制内容一致。
- 本地测试与构建通过;终端执行与跨设备连接按实际证据分别汇报。
## 实现前代码事实
- `HomeView+Cards.swift` 提供 zsh/bash 和 fish 复制按钮;复制前刷新命令地址。
- `TerminalCommandAddress.swift` 在地址失效时回退 `127.0.0.1`。
- `ProxyControlAdapters.swift` 按命令地址选择本地或局域网端口;认证 URL 对凭据中的特殊字符作百分号编码。
- 凭据读取失败时,现有命令返回失败,界面禁用复制。
- ADR-0034 规定 tooltip 与复制内容均包含完整命令和真实密码。
## 实现约束与验证边界
- CMD 交互命令必须保留认证 URL 的百分号编码,不能让已有环境变量改变凭据。生成方式属于实现问题,不降低认证要求。
- Nushell 与 PowerShell 使用各自的环境变量赋值语法;CMD 使用其交互语法,不采用 `.bat` 百分号转义规则。
- 当前环境未找到 `nu` 和 `pwsh`。`wine --version` 执行失败,输出为 `zsh:1: bad CPU type in executable: wine`。这些事实不代表目标终端执行通过。
- CMD 对含百分号编码的 URL 使用交互 `for %P in ("%") do set "NAME=VALUE"`,以 `%~P` 插入百分号;各变量分行赋值,不写临时环境变量。普通 URL 使用四个 `set` 以 ` & ` 连接。分行方案限制最长允许凭据下的单行长度。
- 上述 CMD 输出通过命令字符串测试与静态审查;官方文档说明 `%~P` 移除外围引号、环境变量展开不递归。原生 Windows 的完整解析行为与同名变量干扰仍未验证,不把字符串断言视为终端执行证据。
- 用户调用 `implement` 技能,授权按本文实现、审查并提交到当前分支。
## 测试边界
本地验证沿用第一轮与第二轮确认的格式、显示条件、凭据编码和地址失效范围:
- `TerminalProxyEnvironmentCommands` 投影:精确命令、监听端口、IPv6 与凭据编码。
- `ProxyControlWorkflow` 的 snapshot 与复制前准备:格式显隐、代理关闭、网络/监听变化及旧 IP 失效。
- 测试通过公开投影与工作流动作观察结果,不调用私有生成器或 SwiftUI 私有方法。
## 文档来源
- [Nushell 环境变量](https://www.nushell.sh/book/environment.html):`$env.NAME = 'value'`。
- [PowerShell 环境变量](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables):`$env:NAME = 'value'`。
- [CMD set](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1):当前命令解释器的环境变量赋值。
- [CMD](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd):命令连接与变量展开。
- [CMD for](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/for):交互循环变量与引号移除修饰符。
- [CMD 命令行长度限制](https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/command-line-string-limitation):单行最大长度为 8191 个字符。
本变更为可逆的命令格式与界面扩展,不新建 ADR。
## 本次实现与验证
- 实现五种命令格式、按地址条件显示 Windows 按钮,以及所有格式共用的复制前地址身份检查。
- Standards 与 Spec 两项只读代码审查均未发现问题。
- 使用 `CODE_SIGN_IDENTITY=-` 执行 macOS arm64 全量 hostless 测试:1,059 个测试、1,162 次执行,0 失败、0 跳过。
- 同配置 Debug 构建通过。
- 外部二进制离线检查 5 个测试、规则快照检查 4 个测试、规则转换行为检查 17 个测试通过。
- 修改的 Swift 文件通过严格格式检查;仓库 SwiftLint 返回 31 条警告、0 条严重问题。
- 真实 Nushell、Windows CMD、Windows PowerShell、跨设备代理连接与原生界面交互未验证。剪贴板保留和失败反馈路径经静态审查,不将其描述为原生交互测试。