import Foundation /// The single application presentation edge. Domain and workflow modules expose /// typed errors/facts; this file is the only place that turns them into text. extension Error { var presentableMessage: String { AppPresentation.message(for: self) } } enum AppPresentation { static let unknownError = String(localized: "操作失败,请重试或检查设置") } extension AppPresentation { // The presentation edge deliberately dispatches across the app's typed // failure families; keeping this switch explicit makes the whitelist auditable. // swiftlint:disable:next cyclomatic_complexity function_body_length static func message(for error: Error) -> String { switch error { case let error as CatalogError: return catalog(error) case let error as ActivationFailure: return activation(error) case let error as ServerFormError: return serverForm(error) case let error as SsUriError: return ssURI(error) case let error as CredentialKeyStoreError: return credential(error) case let error as SubscriptionFormError: return subscriptionForm(error) case let error as SubscriptionFetchError: return subscriptionFetch(error) case let error as SubscriptionParseError: return subscriptionParse(error) case let error as SettingsWorkflowFailure: return message(for: error) case let error as RuntimeFailureFacts: return message(for: error) case let error as ImportLineFailureReason: return importLine(error) case let error as ImportSourceFailure: return importSource(error) case let error as LegacySkippedRecord.Reason: return reason(error) case let error as PortSettingError: return port(error) case let error as ProxyListeningStoreError: return listeningStore(error) case let error as LegacyImportError: return legacyImport(error) case let error as SystemProxyError: return systemProxy(SystemProxyOperationFailure(error)) case let error as TextClipboardFailure: return textClipboard(error) case let error as TerminalCommandFailure: switch error { case .credentialUnreadable: return String(localized: "局域网身份验证密码不可读,请重试。") case .addressUnavailable: return String(localized: "命令地址已失效,请重新选择 IP。") } case let error as ImageClipboardFailure: return imageClipboard(error) case let error as QrImageSaveFailure: return qrImageSave(error) case let error as DiagnosticReportFailure: return diagnosticReport(error) case let error as DiagnosticReportExportFailure: return diagnosticReportExport(error) case let error as ConfigurationGroupExportFailure: return configurationGroupExport(error) case let error as ConfigurationGroupFileExportFailure: return error == .writeFailed ? String(localized: "SIP-008 文件写入失败") : String(localized: "URI 链接列表文本文件写入失败") default: return unknownError } } // swiftlint:disable:next cyclomatic_complexity static func message(for failure: SubscriptionRefreshFailure) -> String { switch failure { case .invalidURL: return String(localized: "订阅地址无效") case .unsupportedScheme: return String(localized: "订阅地址必须是 HTTPS") case .insecureRedirect: return String(localized: "订阅地址重定向到了非 HTTPS 站点,已拒绝") case .transport(let category): switch category { case .timedOut: return String(localized: "订阅获取超时") case .tls: return String(localized: "订阅安全连接失败") case .connection: return String(localized: "订阅连接失败") case .unknown: return String(localized: "订阅获取失败") } case .httpStatus(let code): return String(localized: "订阅服务器返回 HTTP \(code)") case .contentType(let category): return category == .missing ? String(localized: "订阅响应缺少 JSON Content-Type") : String(localized: "订阅响应类型不是 JSON") case .decodingFailure: return String(localized: "订阅内容不是合法的 SIP-008 JSON 文档") case .unsupportedSchemaVersion: return String(localized: "订阅文档版本不受支持") case .missingServers: return String(localized: "订阅文档缺少 servers 列表") case .recordValidation(let index, let field): return String(localized: "订阅第 \(index + 1) 条服务器记录的\(recordFieldName(field))无效") case .duplicateIdentity: return String(localized: "订阅包含重复的服务器身份") case .commit: return String(localized: "订阅快照保存失败") case .unknown: return unknownError } } static func message(for issue: SettingsFieldIssue) -> String { switch issue { case .port(_, let error): return message(for: error) } } static func message(for failure: SettingsWorkflowFailure) -> String { switch failure { case .store(let error): return listeningStore(error) case .unknown: return unknownError } } // 显式白名单 switch,与 message(for:) 同一可审计惯例。 // swiftlint:disable:next cyclomatic_complexity private static func catalog(_ error: CatalogError) -> String { switch error { case .nodeNotFound: return String(localized: "节点不存在(可能已被删除)") case .parentNotFound: return String(localized: "目标分组不存在") case .parentNotAGroup: return String(localized: "目标位置不是分组") case .notAServer: return String(localized: "该节点不是服务器") case .notAGroup: return String(localized: "该节点不是分组") case .duplicateID: return String(localized: "节点身份冲突") case .crossSourcePlacement(_, let container): return container == .subscription ? String(localized: "不能把手动节点移入订阅子树") : String(localized: "不能把订阅节点移入手动子树") case .subscriptionNodeImmutable: return String(localized: "订阅节点由远端管理:结构、连接字段与名称只读") case .subscriptionServerAtRoot: return String(localized: "订阅服务器不能移出其订阅分组") case .cycleDetected: return String(localized: "不能把分组移动进它自己的子树") case .indexOutOfRange: return String(localized: "插入位置超出范围") case .invalidStructure: return String(localized: "配置目录结构不一致") } } static func activation(_ error: ActivationFailure) -> String { switch error { case .noActiveTarget: return String(localized: "尚未激活任何服务器或分组,请先选择一个目标") case .targetNotFound: return String(localized: "活动目标不存在,已清除目标;本地监听不受影响") case .targetExpandsToNothing: return String(localized: "目标没有可激活的服务器") case .invalidLeaf(let node, let reason): return String(localized: "服务器不可用:\(leaf(reason))(\(shortNodeID(node)))") } } private static func leaf(_ reason: LeafInvalidationReason) -> String { switch reason { case .invalidAddress: return String(localized: "地址无效") case .invalidPort: return String(localized: "端口无效") case .missingEncryptionMethod: return String(localized: "缺少加密方法") case .unsupportedEncryptionMethod: return String(localized: "加密方法不受支持") case .pluginNotProvided: return String(localized: "插件未提供") case .missingPassword: return String(localized: "密码缺失或为空") } } private static func serverForm(_ error: ServerFormError) -> String { switch error { case .invalidAddress: return String(localized: "服务器地址不能为空") case .invalidPort: return String(localized: "端口必须是 1–65535 之间的整数") case .missingEncryptionMethod: return String(localized: "必须选择加密方法") case .unsupportedEncryptionMethod: return String(localized: "当前 sslocal 不支持该加密方法") case .invalidPassword: return String(localized: "密码不能为空") case .emptyName: return String(localized: "名称不能为空") case .pluginUnknown: return String(localized: "本版本未提供所选插件") } } private static func ssURI(_ error: SsUriError) -> String { switch error { case .notSsUri: return String(localized: "不是 ss:// 链接") case .malformed: return String(localized: "无法解析 ss:// 链接") } } private static func credential(_ error: CredentialKeyStoreError) -> String { switch error { case .keychainStatus: return String(localized: "钥匙串操作失败") } } private static func subscriptionForm(_ error: SubscriptionFormError) -> String { switch error { case .invalidURL: return String(localized: "订阅地址必须是有效的 HTTPS URL") case .notFound: return String(localized: "订阅不存在(可能已被删除)") } } private static func subscriptionFetch(_ error: SubscriptionFetchError) -> String { message(for: SubscriptionRefreshFailure.from(fetchError: error)) } private static func subscriptionParse(_ error: SubscriptionParseError) -> String { message(for: SubscriptionRefreshFailure.from(parseError: error)) } private static func importLine(_ error: ImportLineFailureReason) -> String { switch error { case .decode(let error): return ssURI(error) } } /// 统一导入的来源级失败:解析失败复用订阅解析文案族,提交失败复用 /// 提交错误经过统一呈现,其余点名具体原因。 private static func importSource(_ error: ImportSourceFailure) -> String { switch error { case .parse(let error): return subscriptionParse(error) case .undecodableImage: return String(localized: "图片无法识别,请使用常见图片格式") case .qrPayloadNotFound: return String(localized: "未识别到 ss:// 二维码") case .noImportableLines: return String(localized: "没有可导入的 ss:// 链接") case .commit(let error): return message(for: error) } } private static func reason(_ error: LegacySkippedRecord.Reason) -> String { switch error { case .notDictionary: return String(localized: "记录不是字典") case .invalidAddress: return String(localized: "服务器地址缺失或无效") case .invalidPort: return String(localized: "服务器端口缺失或无效") case .invalidEncryptionMethod: return String(localized: "加密方式缺失或无效") case .missingPassword: return String(localized: "密码缺失或为空") } } private static func port(_ error: PortSettingError) -> String { switch error { case .portOutOfRange(let endpoint, let portNumber): let port = portNumber.formatted(.number.grouping(.never)) return String( localized: "\(endpoint.displayName) 端口 \(port) 无效,必须是 1025–65535 之间的整数" ) case .duplicatePort(let endpoint, let otherEndpoint, let portNumber): let port = portNumber.formatted(.number.grouping(.never)) return String( localized: "\(endpoint.displayName) 端口与 \(otherEndpoint.displayName) 端口冲突(都是 \(port)),请为每个端点配置不同的端口" ) } } private static func listeningStore(_ error: ProxyListeningStoreError) -> String { switch error { case .corrupt: return String(localized: "偏好文件损坏") case .invalid(let errors): return errors.map(port).joined(separator: ";") case .ioFailure: return String(localized: "偏好文件读写失败") case .credentialFailure(let detail): return String(localized: "凭据操作失败:\(detail)") case .encryptedFile: return String(localized: "监听配置无法解密或格式不受支持") } } private static func legacyImport(_ error: LegacyImportError) -> String { switch error { case .noSnapshot: return String(localized: "没有发现可导入的 Legacy 配置") case .alreadyCompleted: return String(localized: "Legacy 配置已经导入;如需再次导入,请明确选择再次导入") case .malformedSnapshot: return String(localized: "Legacy 快照无效") case .commitFailed: return String(localized: "Legacy 导入未完成,2.0 写入未完成") } } private static func textClipboard(_ error: TextClipboardFailure) -> String { switch error { case .writeFailed: return String(localized: "剪贴板写入失败,请重试") } } private static func imageClipboard(_ error: ImageClipboardFailure) -> String { switch error { case .writeFailed: return String(localized: "剪贴板写入失败,请重试") } } private static func qrImageSave(_ error: QrImageSaveFailure) -> String { switch error { case .writeFailed: return String(localized: "二维码图片写入失败,请选择其他位置或重试") } } private static func diagnosticReport(_ error: DiagnosticReportFailure) -> String { switch error { case .encodingFailed: return String(localized: "诊断报告无法安全构造,请重试") } } private static func diagnosticReportExport(_ error: DiagnosticReportExportFailure) -> String { switch error { case .writeFailed: return String(localized: "诊断报告写入失败,请选择其他位置或重试") } } private static func configurationGroupExport(_ error: ConfigurationGroupExportFailure) -> String { switch error { case .groupNotFound: return String(localized: "分组不存在(可能已被删除)") case .targetIsNotGroup: return String(localized: "所选节点不是分组") case .noServers: return String(localized: "分组中没有服务器配置") case .invalidServer(_, let problem): return invalidServerExport(problem) case .pluginOptionsWithoutProgram: return String(localized: "服务器配置了插件参数但没有插件名称,无法导出 SIP-008 文件") case .documentEncodingFailed: return String(localized: "SIP-008 文档生成失败") } } private static func invalidServerExport(_ problem: ConfigurationGroupExportFailure.RecordProblem) -> String { switch problem { case .invalidAddress: return String(localized: "服务器地址为空,无法导出 SIP-008 文件") case .invalidPort: return String(localized: "服务器端口无效,无法导出 SIP-008 文件") case .missingEncryptionMethod: return String(localized: "服务器缺少加密方式,无法导出 SIP-008 文件") case .missingPassword: return String(localized: "服务器密码为空,无法导出 SIP-008 文件") } } private static func recordFieldName(_ field: SubscriptionRefreshFailure.RecordField) -> String { switch field { case .address: return String(localized: "服务器地址") case .port: return String(localized: "服务器端口") case .method: return String(localized: "加密方法") case .password: return String(localized: "密码") case .identity: return String(localized: "身份") } } private static func shortNodeID(_ id: NodeID) -> String { String(id.rawValue.suffix(8)) } }