更新阅读器功能与示例
This commit is contained in:
@@ -0,0 +1,604 @@
|
||||
# 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 语义。
|
||||
|
||||
Reference in New Issue
Block a user