17 KiB
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. 标识符与基础范围
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:
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
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 通用定位
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
}
引用恢复流程:
- 校验文档和资源存在。
- 校验
sourceHash。 - 优先按格式 Anchor 恢复。
- Anchor 失败时使用文本范围和 quote 搜索。
- 仍失败则返回
staleCitation,不得跳转到近似但未验证的位置。
4. 文档与资源
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. 内容提供协议
@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。
可选读取范围协议:
public struct RDAIReadScope: Codable, Equatable, Sendable {
public let upperBound: RDAILocator?
public let includesWholeDocument: Bool
}
@MainActor
public protocol RDAIReadScopeProviding: AnyObject {
func aiCurrentReadScope() -> RDAIReadScope
}
未实现时,默认只允许当前资源及之前的资源,不能默认整本书。
6. Passage 与分析结果
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
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<RDAIIndexState>
func removeIndex() async throws
}
要求:
prepareIndex幂等。- 重复调用只能扩大范围或提高优先级,不能创建重复 Job。
removeIndex删除索引、实体和生成缓存,但不删除原书或用户笔记。
8. 能力与可用性
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
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 摘要
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 问答
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 人物关系
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 生成元数据
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. 高层服务
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 协议
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. 错误模型
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
建议公开:
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
建议公开:
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:
pod 'RDSpeechReaderView/AI'
桥接只负责朗读 AI 结果;Natural Language 的分句与语言识别实现应抽取为共享内部组件,避免同一文本产生不同范围。
17. 配置
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 = .readContentOnlymaximumRetrievedPassages = 4allowsEmbeddingAssetDownload = falsestoresGeneratedArtifacts = truediagnosticsLevel = .metadataOnly
18. API 演进规则
- 1.0 前可以调整命名,但每次调整同步更新五份设计文档。
- 1.0 后删除或改变语义需要主版本升级。
- Codable 枚举新增 case 时必须实现向后兼容策略。
- 数据库 Schema 版本与 SDK 版本独立。
- Prompt 版本与 SDK 版本独立。
- Reader Adapter 可以增加格式能力,但不能改变 Core Locator 的 UTF-16 语义。