Files
ReadViewSDK/Doc/RDAIReaderView/RDAIReaderView-API.md
T
2026-07-27 21:43:13 +08:00

17 KiB
Raw Blame History

RDAIReaderView 公共 API 设计

文档状态: Proposal 0.1
最后更新: 2026-07-25
目标版本: RDAIReaderView 1.0

1. API 设计原则

  • Core 最低支持 iOS 15,不直接依赖 UIKit、PDFKit、DTCoreText 或 FoundationModels。
  • 公共模型优先使用值类型,并遵循 CodableSendableEquatable
  • 文本范围统一使用 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
}

引用恢复流程:

  1. 校验文档和资源存在。
  2. 校验 sourceHash
  3. 优先按格式 Anchor 恢复。
  4. Anchor 失败时使用文本范围和 quote 搜索。
  5. 仍失败则返回 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
  • 原生文本标记为 .nativeVision 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 生成 cfirangeCFI
  • 导航复用 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 = .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 语义。