605 lines
15 KiB
Swift
605 lines
15 KiB
Swift
import Foundation
|
|
|
|
// MARK: - Auth
|
|
|
|
struct AuthUser: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var username: String
|
|
var displayName: String?
|
|
var role: String
|
|
}
|
|
|
|
struct AuthSession: Codable, Equatable, Sendable {
|
|
var authRequired: Bool
|
|
var authenticated: Bool
|
|
var demoMode: Bool
|
|
var user: AuthUser?
|
|
var sessionToken: String?
|
|
}
|
|
|
|
struct LoginInput: Encodable, Sendable {
|
|
var username: String
|
|
var password: String
|
|
var deviceId: String
|
|
}
|
|
|
|
struct MobileSession: Codable, Equatable, Sendable {
|
|
var baseUrl: String
|
|
var token: String
|
|
var user: AuthUser?
|
|
}
|
|
|
|
struct InstanceStorageDiagnostics: Codable, Equatable, Sendable {
|
|
var database: String?
|
|
var resources: String?
|
|
}
|
|
|
|
struct InstanceHealth: Codable, Equatable, Sendable {
|
|
var ok: Bool
|
|
var name: String
|
|
var runtime: String?
|
|
var containerImageSource: String?
|
|
var authMode: String?
|
|
var build: String?
|
|
var deploymentVersionCreatedAt: String?
|
|
var migration: String?
|
|
var storage: InstanceStorageDiagnostics?
|
|
var objectStorageProvider: String?
|
|
}
|
|
|
|
struct InstanceRelease: Codable, Equatable, Sendable {
|
|
var version: String
|
|
}
|
|
|
|
struct LoginDeviceSession: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var userAgent: String?
|
|
var label: String?
|
|
var ipAddress: String?
|
|
var ipCountry: String?
|
|
var ipRegion: String?
|
|
var isCurrent: Bool
|
|
var createdAt: String
|
|
var lastSeenAt: String
|
|
var expiresAt: String
|
|
}
|
|
|
|
// MARK: - Notebook / Memo
|
|
|
|
struct Notebook: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var parentId: String?
|
|
var name: String
|
|
var slug: String?
|
|
var icon: String?
|
|
var color: String?
|
|
var sortOrder: Int
|
|
var memoCount: Int
|
|
var lastMemoUpdatedAt: String?
|
|
var createdAt: String
|
|
var updatedAt: String
|
|
}
|
|
|
|
struct MemoSummary: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var notebookId: String
|
|
var title: String?
|
|
var excerpt: String
|
|
var tags: [String]
|
|
var isPinned: Bool
|
|
var isArchived: Bool
|
|
var isDeleted: Bool
|
|
var revision: Int
|
|
var createdAt: String
|
|
var updatedAt: String
|
|
var deletedAt: String?
|
|
}
|
|
|
|
struct MemoDetail: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var notebookId: String
|
|
var title: String?
|
|
var excerpt: String
|
|
var tags: [String]
|
|
var isPinned: Bool
|
|
var isArchived: Bool
|
|
var isDeleted: Bool
|
|
var revision: Int
|
|
var createdAt: String
|
|
var updatedAt: String
|
|
var deletedAt: String?
|
|
var contentJson: JSONValue
|
|
var contentMarkdown: String
|
|
var contentText: String
|
|
var contentHash: String
|
|
var sourceMemoIds: [String]
|
|
var mergeSourceCount: Int
|
|
var mergedIntoMemoId: String?
|
|
|
|
var displayTitle: String {
|
|
let t = title?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
return t.isEmpty ? "无标题笔记" : t
|
|
}
|
|
|
|
func asSummary() -> MemoSummary {
|
|
MemoSummary(
|
|
id: id,
|
|
notebookId: notebookId,
|
|
title: title,
|
|
excerpt: excerpt,
|
|
tags: tags,
|
|
isPinned: isPinned,
|
|
isArchived: isArchived,
|
|
isDeleted: isDeleted,
|
|
revision: revision,
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt,
|
|
deletedAt: deletedAt
|
|
)
|
|
}
|
|
}
|
|
|
|
struct MemoEditSession: Codable, Equatable, Sendable {
|
|
var id: String
|
|
var memoId: String
|
|
var baseRevision: Int
|
|
var baseContentHash: String
|
|
var expiresAt: String
|
|
}
|
|
|
|
// MARK: - AI note processing
|
|
|
|
enum AiAction: String, Codable, CaseIterable, Sendable, Identifiable {
|
|
case summarize
|
|
case extractKeyPoints = "extract-key-points"
|
|
case extractTodos = "extract-todos"
|
|
case rewriteProofread = "rewrite-proofread"
|
|
case translate
|
|
case improveWriting = "improve-writing"
|
|
case fixSpellingGrammar = "fix-spelling-grammar"
|
|
case makeShorter = "make-shorter"
|
|
case makeLonger = "make-longer"
|
|
case simplifyLanguage = "simplify-language"
|
|
case changeTone = "change-tone"
|
|
case continueWriting = "continue-writing"
|
|
case custom
|
|
|
|
var id: String { rawValue }
|
|
}
|
|
|
|
struct AiGenerateInput: Encodable, Sendable {
|
|
var action: AiAction
|
|
var promptId: String? = nil
|
|
var locale: String? = nil
|
|
var title: String
|
|
var contentMarkdown: String
|
|
var targetLanguage: String?
|
|
var tone: String? = nil
|
|
var instruction: String? = nil
|
|
}
|
|
|
|
struct AiGenerationResultBoundary: Decodable, Sendable {
|
|
var start: String
|
|
var end: String
|
|
}
|
|
|
|
struct AiPreparedGeneration: Decodable, Sendable {
|
|
var provider: String
|
|
var baseUrl: String
|
|
var apiKey: String
|
|
var modelId: String
|
|
var system: String
|
|
var prompt: String
|
|
var maxOutputTokens: Int
|
|
var resultBoundary: AiGenerationResultBoundary
|
|
}
|
|
|
|
struct AiPreparedTagSuggestions: Decodable, Sendable {
|
|
var provider: String
|
|
var baseUrl: String
|
|
var apiKey: String
|
|
var modelId: String
|
|
var system: String
|
|
var prompt: String
|
|
var maxOutputTokens: Int
|
|
var currentTags: [String]
|
|
var canonicalTags: [String: String]
|
|
}
|
|
|
|
struct AiTagSuggestionsInput: Encodable, Sendable {
|
|
var title: String
|
|
var contentMarkdown: String
|
|
var currentTags: [String]
|
|
var locale: String
|
|
}
|
|
|
|
struct AiTagSuggestion: Codable, Equatable, Sendable, Identifiable {
|
|
var name: String
|
|
var existing: Bool
|
|
|
|
var id: String { name }
|
|
}
|
|
|
|
struct AiTagSuggestionsResponse: Codable, Equatable, Sendable {
|
|
var suggestions: [AiTagSuggestion]
|
|
}
|
|
|
|
enum AiPromptParameterKind: String, Codable, Sendable {
|
|
case none
|
|
case targetLanguage = "target-language"
|
|
case tone
|
|
}
|
|
|
|
enum AiPromptResultMode: String, Codable, Sendable {
|
|
case append
|
|
case replace
|
|
case both
|
|
}
|
|
|
|
struct AiAssistantLastActionPreference: Codable, Equatable, Sendable {
|
|
var action: AiAction
|
|
var promptId: String?
|
|
var seedKey: String?
|
|
var targetLanguage: String?
|
|
var tone: String?
|
|
}
|
|
|
|
struct AiAssistantLastActionStore: Codable, Equatable, Sendable {
|
|
var selected: AiAssistantLastActionPreference? = nil
|
|
var wholeNote: AiAssistantLastActionPreference? = nil
|
|
}
|
|
|
|
struct AiPromptTemplate: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var origin: String
|
|
var seedKey: String?
|
|
var action: AiAction
|
|
var parameterKind: AiPromptParameterKind
|
|
var resultMode: AiPromptResultMode
|
|
var nameCustomized: Bool
|
|
var descriptionCustomized: Bool
|
|
var instructionCustomized: Bool
|
|
var name: String
|
|
var description: String?
|
|
var instruction: String
|
|
var createdAt: String
|
|
var updatedAt: String
|
|
}
|
|
|
|
struct AiStreamEvent: Decodable, Sendable {
|
|
var type: String
|
|
var text: String?
|
|
var code: String?
|
|
var message: String?
|
|
var finishReason: String?
|
|
var inputTokens: Int?
|
|
var outputTokens: Int?
|
|
}
|
|
|
|
struct Resource: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var memoId: String
|
|
var originalMemoId: String?
|
|
var kind: String
|
|
var mimeType: String?
|
|
var filename: String?
|
|
var byteSize: Int
|
|
var sha256: String?
|
|
var width: Int?
|
|
var height: Int?
|
|
var createdAt: String
|
|
var updatedAt: String
|
|
var url: String
|
|
}
|
|
|
|
struct ApiToken: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var name: String
|
|
var token: String?
|
|
var scopes: [String]
|
|
var lastUsedAt: String?
|
|
var expiresAt: String?
|
|
var isRevoked: Bool
|
|
var createdAt: String
|
|
}
|
|
|
|
struct CreatedApiToken: Codable, Equatable, Sendable {
|
|
var token: String
|
|
var apiToken: ApiToken
|
|
}
|
|
|
|
struct TagSummary: Codable, Equatable, Sendable {
|
|
var name: String
|
|
var memoCount: Int
|
|
var updatedAt: String?
|
|
}
|
|
|
|
struct MemoRevision: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var memoId: String
|
|
var revision: Int
|
|
var title: String?
|
|
var tags: [String]
|
|
var contentMarkdown: String
|
|
var contentText: String
|
|
var contentHash: String
|
|
var createdBy: String
|
|
var createdAt: String
|
|
}
|
|
|
|
struct MemoShare: Codable, Equatable, Sendable {
|
|
var memoId: String
|
|
var token: String
|
|
var createdAt: String
|
|
var updatedAt: String
|
|
var passwordProtected: Bool?
|
|
}
|
|
|
|
struct InstanceUser: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var username: String
|
|
var displayName: String?
|
|
var role: String
|
|
var isDisabled: Bool
|
|
var lastLoginAt: String?
|
|
var createdAt: String
|
|
}
|
|
|
|
// MARK: - Sync wire types
|
|
|
|
struct MobileSyncBootstrapPage: Codable, Sendable {
|
|
var notebooks: [Notebook]
|
|
var memos: [MemoDetail]
|
|
var snapshotCursor: Int
|
|
var syncIdentity: String?
|
|
var totalCount: Int?
|
|
var nextAfterId: String?
|
|
}
|
|
|
|
struct MobileSyncChange: Codable, Sendable {
|
|
var cursor: Int
|
|
var entityType: String
|
|
var entityId: String
|
|
var operation: String
|
|
var notebook: Notebook?
|
|
var memo: MemoDetail?
|
|
}
|
|
|
|
struct MobileSyncChangesPage: Codable, Sendable {
|
|
var changes: [MobileSyncChange]
|
|
var cursor: Int
|
|
var hasMore: Bool
|
|
var serverCursor: Int?
|
|
var syncIdentity: String?
|
|
}
|
|
|
|
// MARK: - Envelope helpers
|
|
|
|
struct MemoResponse: Codable, Sendable {
|
|
var memo: MemoDetail
|
|
}
|
|
|
|
struct NotebookResponse: Codable, Sendable {
|
|
var notebook: Notebook
|
|
}
|
|
|
|
struct NotebooksResponse: Codable, Sendable {
|
|
var notebooks: [Notebook]
|
|
}
|
|
|
|
struct EditSessionResponse: Codable, Sendable {
|
|
var editSession: MemoEditSession
|
|
}
|
|
|
|
struct ResourceResponse: Codable, Sendable {
|
|
var resource: Resource
|
|
}
|
|
|
|
struct OkResponse: Codable, Sendable {
|
|
var ok: Bool
|
|
}
|
|
|
|
struct SessionsResponse: Codable, Sendable {
|
|
var sessions: [LoginDeviceSession]
|
|
}
|
|
|
|
struct ApiTokensResponse: Codable, Sendable {
|
|
var apiTokens: [ApiToken]
|
|
var availableScopes: [String]
|
|
}
|
|
|
|
struct TagsResponse: Codable, Sendable {
|
|
var tags: [TagSummary]
|
|
}
|
|
|
|
struct AiPromptsResponse: Codable, Sendable {
|
|
var prompts: [AiPromptTemplate]
|
|
}
|
|
|
|
// MARK: - Outbox / drafts domain
|
|
|
|
enum MemoSortMode: String, CaseIterable, Sendable {
|
|
case updatedDesc = "updated-desc"
|
|
case createdDesc = "created-desc"
|
|
case titleAsc = "title-asc"
|
|
case titleDesc = "title-desc"
|
|
}
|
|
|
|
enum MemoFilterMode: String, CaseIterable, Sendable {
|
|
case all
|
|
case tagged
|
|
case untagged
|
|
case pinned
|
|
}
|
|
|
|
struct MemoCreatePayload: Codable, Equatable, Sendable {
|
|
var memoId: String
|
|
var title: String
|
|
var contentMarkdown: String
|
|
/// TipTap JSON when available (preserves image width attrs markdown drops).
|
|
var contentJson: String? = nil
|
|
var notebookId: String
|
|
var tags: [String]
|
|
var createdAt: String
|
|
}
|
|
|
|
struct MemoUpdatePayload: Codable, Equatable, Sendable {
|
|
var memoId: String
|
|
var expectedRevision: Int
|
|
var expectedContentHash: String
|
|
var title: String
|
|
var contentMarkdown: String
|
|
/// TipTap JSON when available (preserves image width attrs markdown drops).
|
|
var contentJson: String? = nil
|
|
var notebookId: String
|
|
var tags: [String]
|
|
}
|
|
|
|
enum OutboxKind: String, Codable, Sendable {
|
|
case memoCreate = "memo.create"
|
|
case memoUpdate = "memo.update"
|
|
}
|
|
|
|
enum OutboxStatus: String, Codable, Sendable {
|
|
case pending
|
|
case syncing
|
|
case conflict
|
|
case error
|
|
}
|
|
|
|
struct OutboxItem: Codable, Equatable, Sendable, Identifiable {
|
|
var id: String
|
|
var kind: OutboxKind
|
|
var memoId: String
|
|
var status: OutboxStatus
|
|
var payloadJSON: String
|
|
var attemptCount: Int
|
|
var lastError: String?
|
|
var nextAttemptAt: String?
|
|
var createdAt: String
|
|
var updatedAt: String
|
|
var version: Int
|
|
|
|
func createPayload() throws -> MemoCreatePayload {
|
|
try EdgeEverJSON.decoder.decode(MemoCreatePayload.self, from: Data(payloadJSON.utf8))
|
|
}
|
|
|
|
func updatePayload() throws -> MemoUpdatePayload {
|
|
try EdgeEverJSON.decoder.decode(MemoUpdatePayload.self, from: Data(payloadJSON.utf8))
|
|
}
|
|
}
|
|
|
|
struct MemoDraft: Codable, Equatable, Sendable {
|
|
var draftKey: String
|
|
var title: String
|
|
var contentMarkdown: String
|
|
var contentJson: String?
|
|
var notebookId: String
|
|
var tagsText: String
|
|
var expectedRevision: Int?
|
|
var updatedAt: String
|
|
}
|
|
|
|
struct SyncRunResult: Equatable, Sendable {
|
|
var attempted = 0
|
|
var synced = 0
|
|
var failed = 0
|
|
var conflicted = 0
|
|
}
|
|
|
|
struct BootstrapProgress: Equatable, Sendable {
|
|
var loadedCount: Int
|
|
var totalCount: Int
|
|
}
|
|
|
|
// MARK: - Flexible JSON for TipTap docs
|
|
|
|
enum JSONValue: Codable, Equatable, Sendable {
|
|
case string(String)
|
|
case number(Double)
|
|
case bool(Bool)
|
|
case object([String: JSONValue])
|
|
case array([JSONValue])
|
|
case null
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
if container.decodeNil() {
|
|
self = .null
|
|
} else if let value = try? container.decode(Bool.self) {
|
|
self = .bool(value)
|
|
} else if let value = try? container.decode(Double.self) {
|
|
self = .number(value)
|
|
} else if let value = try? container.decode(String.self) {
|
|
self = .string(value)
|
|
} else if let value = try? container.decode([String: JSONValue].self) {
|
|
self = .object(value)
|
|
} else if let value = try? container.decode([JSONValue].self) {
|
|
self = .array(value)
|
|
} else {
|
|
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON")
|
|
}
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
switch self {
|
|
case .string(let value): try container.encode(value)
|
|
case .number(let value): try container.encode(value)
|
|
case .bool(let value): try container.encode(value)
|
|
case .object(let value): try container.encode(value)
|
|
case .array(let value): try container.encode(value)
|
|
case .null: try container.encodeNil()
|
|
}
|
|
}
|
|
|
|
static var emptyDoc: JSONValue {
|
|
.object([
|
|
"type": .string("doc"),
|
|
"content": .array([.object(["type": .string("paragraph")])]),
|
|
])
|
|
}
|
|
|
|
func jsonString() throws -> String {
|
|
let data = try EdgeEverJSON.encoder.encode(self)
|
|
return String(data: data, encoding: .utf8) ?? "{}"
|
|
}
|
|
|
|
static func parse(_ string: String) throws -> JSONValue {
|
|
try EdgeEverJSON.decoder.decode(JSONValue.self, from: Data(string.utf8))
|
|
}
|
|
}
|
|
|
|
extension MemoDetail {
|
|
static func localPlaceholder(
|
|
id: String,
|
|
notebookId: String,
|
|
title: String,
|
|
contentMarkdown: String,
|
|
tags: [String],
|
|
createdAt: String
|
|
) -> MemoDetail {
|
|
MemoDetail(
|
|
id: id,
|
|
notebookId: notebookId,
|
|
title: title,
|
|
excerpt: String(contentMarkdown.prefix(160)),
|
|
tags: tags,
|
|
isPinned: false,
|
|
isArchived: false,
|
|
isDeleted: false,
|
|
revision: 0,
|
|
createdAt: createdAt,
|
|
updatedAt: createdAt,
|
|
deletedAt: nil,
|
|
contentJson: .emptyDoc,
|
|
contentMarkdown: contentMarkdown,
|
|
contentText: contentMarkdown,
|
|
contentHash: "",
|
|
sourceMemoIds: [],
|
|
mergeSourceCount: 0,
|
|
mergedIntoMemoId: nil
|
|
)
|
|
}
|
|
}
|