605 lines
17 KiB
Markdown
605 lines
17 KiB
Markdown
# 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<RDAIIndexState>
|
||
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 语义。
|
||
|