# RDAIReaderView 公共 API 设计 **文档状态:** Proposal 0.1 **最后更新:** 2026-07-25 **目标版本:** RDAIReaderView 1.0 ## 1. API 设计原则 - Core 最低支持 iOS 15,不直接依赖 UIKit、PDFKit、DTCoreText 或 FoundationModels。 - 公共模型优先使用值类型,并遵循 `Codable`、`Sendable`、`Equatable`。 - 文本范围统一使用 UTF-16 偏移,与现有 PDF、EPUB、NSString 和 TTS 范围保持一致。 - Reader Adapter 负责格式转换,Core 不识别 PDF 页视图或 EPUB 排版对象。 - Foundation Models 通过 Provider 协议接入,不能泄漏到基础 API。 - 公开 API 在 1.0 后遵循语义化版本;新增字段必须有解码默认值。 本文中的 Swift 定义是实现合同,允许在不改变语义的前提下调整文件组织和内部实现。 ## 2. 标识符与基础范围 ```swift import Foundation public struct RDAIDocumentIdentifier: RawRepresentable, Codable, Hashable, Sendable { public let rawValue: String public init(rawValue: String) { self.rawValue = rawValue } } public struct RDAIResourceIdentifier: RawRepresentable, Codable, Hashable, Sendable { public let rawValue: String public init(rawValue: String) { self.rawValue = rawValue } } public struct RDAITextRange: Codable, Hashable, Sendable { public var location: Int public var length: Int public init(location: Int, length: Int) { self.location = max(0, location) self.length = max(0, length) } public var upperBound: Int { location + length } } ``` 约束: - `RDAIDocumentIdentifier` 必须与宿主书籍 ID 一致并保持稳定。 - `RDAIResourceIdentifier` 在 PDF 中使用页索引字符串,在 EPUB 中使用规范化 `href`。 - 所有文本范围均针对资源原始文本,不针对规范化搜索文本。 ## 3. 定位与引用 ### 3.1 归一化矩形 Core 使用自定义矩形,避免公共存储格式依赖 UIKit: ```swift public struct RDAINormalizedRect: Codable, Hashable, Sendable { public var x: Double public var y: Double public var width: Double public var height: Double public init(x: Double, y: Double, width: Double, height: Double) { self.x = x self.y = y self.width = width self.height = height } } ``` 所有值应限制在 `0...1`。Reader Adapter 负责与 `CGRect` 转换。 ### 3.2 格式 Anchor ```swift public struct RDAIPDFAnchor: Codable, Hashable, Sendable { public enum TextSource: String, Codable, Sendable { case native case ocr } public var pageIndex: Int public var rects: [RDAINormalizedRect] public var textSource: TextSource public var readingOrder: Int? } public struct RDAIEPUBAnchor: Codable, Hashable, Sendable { public var href: String public var cfi: String? public var rangeCFI: String? public var progression: Double? } public enum RDAIAnchor: Codable, Hashable, Sendable { case pdf(RDAIPDFAnchor) case epub(RDAIEPUBAnchor) } ``` `RDAIAnchor` 必须实现显式 Codable discriminator,例如 `type: "pdf"`,未知类型解码为明确错误,不能误当成其他格式。 ### 3.3 通用定位 ```swift public struct RDAILocator: Codable, Hashable, Sendable { public var documentIdentifier: RDAIDocumentIdentifier public var resourceIdentifier: RDAIResourceIdentifier public var textRange: RDAITextRange public var anchor: RDAIAnchor public var sourceHash: String } public struct RDAICitation: Codable, Hashable, Sendable, Identifiable { public let id: String public let passageIdentifier: String public let quote: String public let locator: RDAILocator } ``` 引用恢复流程: 1. 校验文档和资源存在。 2. 校验 `sourceHash`。 3. 优先按格式 Anchor 恢复。 4. Anchor 失败时使用文本范围和 quote 搜索。 5. 仍失败则返回 `staleCitation`,不得跳转到近似但未验证的位置。 ## 4. 文档与资源 ```swift public enum RDAIDocumentFormat: String, Codable, Sendable { case pdf case epub } public struct RDAIDocumentDescriptor: Codable, Equatable, Sendable { public let identifier: RDAIDocumentIdentifier public let title: String public let format: RDAIDocumentFormat public let contentRevision: String } public struct RDAIResourceDescriptor: Codable, Equatable, Sendable { public let identifier: RDAIResourceIdentifier public let title: String? public let order: Int public let estimatedUTF16Length: Int? } public struct RDAIResourceSnapshot: Sendable { public let descriptor: RDAIResourceDescriptor public let sourceText: String public let sourceHash: String public let locatorRuns: [RDAILocatorRun] } public struct RDAILocatorRun: Sendable { public let textRange: RDAITextRange public let anchor: RDAIAnchor } ``` `contentRevision` 由宿主提供;若宿主没有版本号,Adapter 使用资源哈希汇总生成。 ## 5. 内容提供协议 ```swift @MainActor public protocol RDAIContentProvider: AnyObject { func aiDocumentDescriptor() -> RDAIDocumentDescriptor func aiResources() async throws -> [RDAIResourceDescriptor] func aiResourceSnapshot( for identifier: RDAIResourceIdentifier ) async throws -> RDAIResourceSnapshot func aiNavigate(to locator: RDAILocator, animated: Bool) async throws func aiShowCitationHighlight(_ citation: RDAICitation) async throws func aiClearCitationHighlight() } ``` 协议标记为 `@MainActor`,因为现有 Reader Controller 和页面缓存均由主线程管理。实现必须只在主线程获取快照引用和 UI 状态;OCR、分块、分析和数据库写入移交后台 actor。 可选读取范围协议: ```swift public struct RDAIReadScope: Codable, Equatable, Sendable { public let upperBound: RDAILocator? public let includesWholeDocument: Bool } @MainActor public protocol RDAIReadScopeProviding: AnyObject { func aiCurrentReadScope() -> RDAIReadScope } ``` 未实现时,默认只允许当前资源及之前的资源,不能默认整本书。 ## 6. Passage 与分析结果 ```swift public enum RDAIPassageKind: String, Codable, Sendable { case title case paragraph case list case table case code case footnote case unknown } public struct RDAIPassage: Codable, Equatable, Sendable, Identifiable { public let id: String public let documentIdentifier: RDAIDocumentIdentifier public let resourceIdentifier: RDAIResourceIdentifier public let text: String public let languageCode: String? public let kind: RDAIPassageKind public let locator: RDAILocator public let contentHash: String public let order: Int } public enum RDAIEntityKind: String, Codable, Sendable { case person case place case organization case other } public struct RDAIEntityMention: Codable, Equatable, Sendable, Identifiable { public let id: String public let normalizedName: String public let surfaceText: String public let kind: RDAIEntityKind public let confidence: Double public let locator: RDAILocator } ``` Natural Language 的标签不是最终人物事实,只是 `RDAIEntityMention` 候选。 ## 7. 索引 API ```swift public enum RDAIIndexState: Equatable, Sendable { case notStarted case indexing(completedResources: Int, totalResources: Int) case paused case ready case failed(RDAIError) } public struct RDAIIndexOptions: Sendable { public var scope: RDAIReadScope public var priorityResource: RDAIResourceIdentifier? public var allowsEmbeddingAssetDownload: Bool } public protocol RDAIIndexing: AnyObject, Sendable { func prepareIndex(options: RDAIIndexOptions) async throws func pauseIndexing() async func resumeIndexing() async func indexState() async -> RDAIIndexState func stateUpdates() async -> AsyncStream func removeIndex() async throws } ``` 要求: - `prepareIndex` 幂等。 - 重复调用只能扩大范围或提高优先级,不能创建重复 Job。 - `removeIndex` 删除索引、实体和生成缓存,但不删除原书或用户笔记。 ## 8. 能力与可用性 ```swift public enum RDAICapability: String, Codable, Sendable { case languageAnalysis case entityExtraction case lexicalSearch case semanticSearch case summarization case questionAnswering case characterRelationships } public enum RDAIUnavailableReason: Equatable, Sendable { case operatingSystemUnsupported case deviceNotEligible case appleIntelligenceNotEnabled case modelNotReady case languageUnsupported(String?) case embeddingAssetsUnavailable case providerNotInstalled case unknown(String) } public enum RDAICapabilityAvailability: Equatable, Sendable { case available case degraded(reason: RDAIUnavailableReason) case unavailable(reason: RDAIUnavailableReason) } public protocol RDAICapabilityProviding: Sendable { func availability( for capability: RDAICapability, locale: Locale? ) async -> RDAICapabilityAvailability } ``` UI 只能根据枚举状态展示文案,不能匹配本地化 Error 字符串。 ## 9. 检索 API ```swift public struct RDAIRetrievalOptions: Sendable { public var maximumResults: Int public var scope: RDAIReadScope public var minimumScore: Double } public struct RDAIRetrievalMatch: Sendable, Identifiable { public let id: String public let passage: RDAIPassage public let score: Double public let lexicalScore: Double? public let semanticScore: Double? } public protocol RDAIRetrieving: Sendable { func retrieve( query: String, options: RDAIRetrievalOptions ) async throws -> [RDAIRetrievalMatch] } ``` 检索分数只用于同一索引版本内排序,不承诺跨版本数值稳定。 ## 10. 生成结果 ### 10.1 摘要 ```swift public enum RDAISummaryLength: String, Codable, Sendable { case brief case standard case detailed } public struct RDAISummary: Codable, Sendable { public let title: String public let overview: String public let keyPoints: [RDAISourcedStatement] public let citations: [RDAICitation] public let metadata: RDAIGenerationMetadata } ``` ### 10.2 问答 ```swift public enum RDAIAnswerStatus: String, Codable, Sendable { case answered case insufficientEvidence case unsupportedLanguage case unavailable } public struct RDAIAnswer: Codable, Sendable { public let status: RDAIAnswerStatus public let text: String public let statements: [RDAISourcedStatement] public let citations: [RDAICitation] public let metadata: RDAIGenerationMetadata } public struct RDAISourcedStatement: Codable, Sendable, Identifiable { public let id: String public let text: String public let citationIdentifiers: [String] } ``` ### 10.3 人物关系 ```swift public enum RDAIRelationshipStatus: String, Codable, Sendable { case confirmed case possible case conflicting } public struct RDAICharacter: Codable, Sendable, Identifiable { public let id: String public let displayName: String public let aliases: [String] public let description: String public let firstAppearance: RDAICitation? public let evidence: [RDAICitation] } public struct RDAIRelationship: Codable, Sendable, Identifiable { public let id: String public let sourceCharacterIdentifier: String public let targetCharacterIdentifier: String public let label: String public let status: RDAIRelationshipStatus public let evidence: [RDAICitation] } ``` ### 10.4 生成元数据 ```swift public struct RDAIGenerationMetadata: Codable, Sendable { public let providerIdentifier: String public let modelVersion: String? public let promptIdentifier: String public let promptVersion: Int public let generatedAt: Date public let scopeHash: String } ``` 元数据用于缓存失效和问题追踪,不向普通用户展示内部 Prompt。 ## 11. 高层服务 ```swift public protocol RDAIReaderServicing: AnyObject, Sendable { func prepare(options: RDAIIndexOptions) async throws func summarize( scope: RDAIReadScope, length: RDAISummaryLength ) async throws -> RDAISummary func answer( question: String, scope: RDAIReadScope ) async throws -> RDAIAnswer func characters( scope: RDAIReadScope ) async throws -> [RDAICharacter] func relationships( scope: RDAIReadScope ) async throws -> [RDAIRelationship] func removeAllAIData() async throws } ``` 建议具体实现为 actor。若用户开始新的同类请求,UI 层负责决定取消旧请求或并行;同一 Foundation Models session 不允许并行请求。 ## 12. Provider 协议 ```swift public struct RDAIGenerationRequest: Sendable { public let task: RDAIGenerationTask public let userText: String? public let passages: [RDAIPassage] public let locale: Locale public let scope: RDAIReadScope } public enum RDAIGenerationTask: Sendable { case summary(RDAISummaryLength) case answer case characters case relationships } public protocol RDAIGenerativeProvider: Sendable { var identifier: String { get } func availability(locale: Locale) async -> RDAICapabilityAvailability func generate(_ request: RDAIGenerationRequest) async throws -> RDAIGeneratedArtifact } ``` `RDAIGeneratedArtifact` 是 Core 内部或受控公共枚举,用于把 Provider 输出转换为第 10 节模型。Provider 不能直接保存结果或操作 Reader UI。 ## 13. 错误模型 ```swift public enum RDAIError: Error, Equatable, Sendable { case invalidDocument case resourceUnavailable(RDAIResourceIdentifier) case staleCitation case indexingFailed(code: String) case modelUnavailable(RDAIUnavailableReason) case unsupportedLanguage(String?) case contextLimitExceeded case invalidGeneratedStructure case invalidCitation case insufficientEvidence case cancelled case storageFailure(code: String) } ``` 公共错误不携带原文或数据库底层错误字符串。内部错误映射为稳定 code,并通过本地诊断系统保存脱敏详情。 ## 14. PDF Adapter 建议公开: ```swift public extension RDPDFReaderViewController { func makeAIContentProvider() -> RDPDFAIContentProvider func makeAIReaderService( configuration: RDAIReaderConfiguration = .default ) throws -> RDAIReaderServicing } ``` 映射要求: - 页面资源 ID 为十进制页索引。 - 文本顺序使用 `RDPDFReaderTextRun.readingOrder`。 - `normalizedRects` 转换为 `RDAINormalizedRect`。 - 原生文本标记为 `.native`,Vision OCR 标记为 `.ocr`。 - AI 高亮复用或泛化现有 speech highlight,不同时维护两个相互覆盖的临时层。 ## 15. EPUB Adapter 建议公开: ```swift public extension RDEPUBReaderController { func makeAIContentProvider() -> RDEPUBAIContentProvider func makeAIReaderService( configuration: RDAIReaderConfiguration = .default ) throws -> RDAIReaderServicing } ``` 映射要求: - 资源 ID 使用 ResourceResolver 规范化后的 `href`。 - Passage 范围从章节 attributed content 的原始字符串计算。 - 使用现有 index table 生成 `cfi` 和 `rangeCFI`。 - 导航复用 `go(to:)`/位置恢复流程。 - 固定版式或无法提取文本的章节返回明确 unavailable,不制造空 Passage。 ## 16. TTS 集成 AI 层不依赖 RDSpeechReaderView。宿主可以把摘要或回答转换为临时 `RDSpeechContentProvider`。 后续可增加桥接 Pod: ```ruby pod 'RDSpeechReaderView/AI' ``` 桥接只负责朗读 AI 结果;Natural Language 的分句与语言识别实现应抽取为共享内部组件,避免同一文本产生不同范围。 ## 17. 配置 ```swift public struct RDAIReaderConfiguration: Sendable { public var spoilerPolicy: RDAISpoilerPolicy public var maximumRetrievedPassages: Int public var allowsEmbeddingAssetDownload: Bool public var storesGeneratedArtifacts: Bool public var diagnosticsLevel: RDAIDiagnosticsLevel public static let `default`: RDAIReaderConfiguration } ``` 默认值: - `spoilerPolicy = .readContentOnly` - `maximumRetrievedPassages = 4` - `allowsEmbeddingAssetDownload = false` - `storesGeneratedArtifacts = true` - `diagnosticsLevel = .metadataOnly` ## 18. API 演进规则 - 1.0 前可以调整命名,但每次调整同步更新五份设计文档。 - 1.0 后删除或改变语义需要主版本升级。 - Codable 枚举新增 case 时必须实现向后兼容策略。 - 数据库 Schema 版本与 SDK 版本独立。 - Prompt 版本与 SDK 版本独立。 - Reader Adapter 可以增加格式能力,但不能改变 Core Locator 的 UTF-16 语义。