feat: add remote channels and richer notes

This commit is contained in:
lofyer
2026-08-09 15:48:52 +08:00
parent 417a9fccb6
commit 6c891f3522
69 changed files with 13012 additions and 499 deletions
+262
View File
@@ -0,0 +1,262 @@
import { z } from 'zod'
export const MAGIC_NOTE_MAX_IMAGES = 12
export const MAGIC_NOTE_MAX_IMAGE_BYTES = 2 * 1024 * 1024
export const MAGIC_NOTE_MAX_TOTAL_IMAGE_BYTES = 8 * 1024 * 1024
export const MAGIC_NOTE_MAX_TEXT_BYTES = 500 * 1024
export function magicNoteImageDataBytes(dataUrl: string): number {
const payload = dataUrl.slice(dataUrl.indexOf(',') + 1)
const padding = payload.endsWith('==')
? 2
: payload.endsWith('=')
? 1
: 0
return Math.floor((payload.length * 3) / 4) - padding
}
const magicNoteIdSchema = z.string().uuid()
const imageDataUrlSchema = z
.string()
.max(Math.ceil((MAGIC_NOTE_MAX_IMAGE_BYTES * 4) / 3) + 128)
.regex(
/^data:image\/(?:jpeg|png|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/,
'只支持本地 JPEG、PNG、GIF 或 WebP 图片'
)
const magicNoteAttributesSchema = z
.object({
bold: z.literal(true).optional(),
italic: z.literal(true).optional(),
underline: z.literal(true).optional(),
strike: z.literal(true).optional(),
code: z.literal(true).optional(),
header: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(),
blockquote: z.literal(true).optional(),
'code-block': z.literal(true).optional(),
list: z
.enum(['ordered', 'bullet', 'checked', 'unchecked'])
.optional(),
align: z.enum(['center', 'right', 'justify']).optional(),
indent: z.number().int().min(1).max(8).optional()
})
.strict()
export const magicNoteRichContentSchema = z
.object({
version: z.literal(1),
ops: z
.array(
z
.object({
insert: z.union([
z.string().max(200_000),
z.object({ image: imageDataUrlSchema }).strict()
]),
attributes: magicNoteAttributesSchema.optional()
})
.strict()
)
.min(1)
.max(10_000)
})
.strict()
.superRefine((content, context) => {
const encoder = new TextEncoder()
let textBytes = 0
const imageData: string[] = []
for (const operation of content.ops) {
if (typeof operation.insert === 'string') {
textBytes += encoder.encode(operation.insert).byteLength
} else {
imageData.push(operation.insert.image)
}
}
if (textBytes > MAGIC_NOTE_MAX_TEXT_BYTES) {
context.addIssue({
code: 'custom',
message: '每条记录的文字内容不能超过 500 KB'
})
}
if (imageData.length > MAGIC_NOTE_MAX_IMAGES) {
context.addIssue({
code: 'custom',
message: `每条记录最多包含 ${MAGIC_NOTE_MAX_IMAGES} 张图片`
})
}
const estimatedBytes = imageData.reduce(
(total, dataUrl) => total + magicNoteImageDataBytes(dataUrl),
0
)
if (estimatedBytes > MAGIC_NOTE_MAX_TOTAL_IMAGE_BYTES) {
context.addIssue({
code: 'custom',
message: '一篇笔记中的图片总大小不能超过 8 MB'
})
}
})
export type MagicNoteRichContent = z.infer<
typeof magicNoteRichContentSchema
>
export const magicNoteScopeSchema = z
.object({
projectId: magicNoteIdSchema.optional()
})
.strict()
export const magicNoteCreateSchema = z
.object({
projectId: magicNoteIdSchema.optional(),
title: z.string().trim().min(1).max(100)
})
.strict()
export type MagicNoteCreateInput = z.infer<typeof magicNoteCreateSchema>
export const magicNoteUpdateSchema = z
.object({
noteId: magicNoteIdSchema,
title: z.string().trim().min(1).max(100).optional(),
pinned: z.boolean().optional(),
expectedRevision: z.number().int().nonnegative()
})
.strict()
.refine((input) => input.title !== undefined || input.pinned !== undefined, {
message: '没有可更新的笔记字段'
})
export type MagicNoteUpdateInput = z.infer<typeof magicNoteUpdateSchema>
export const magicNoteDeleteSchema = z
.object({
noteId: magicNoteIdSchema
})
.strict()
export const magicNoteEntryCreateSchema = z
.object({
noteId: magicNoteIdSchema,
content: magicNoteRichContentSchema
})
.strict()
export type MagicNoteEntryCreateInput = z.infer<
typeof magicNoteEntryCreateSchema
>
export const magicNoteEntryUpdateSchema = z
.object({
entryId: magicNoteIdSchema,
content: magicNoteRichContentSchema,
expectedRevision: z.number().int().nonnegative()
})
.strict()
export type MagicNoteEntryUpdateInput = z.infer<
typeof magicNoteEntryUpdateSchema
>
export const magicNoteEntryDeleteSchema = z
.object({
entryId: magicNoteIdSchema
})
.strict()
export const magicNoteAnalyzeSchema = z
.object({
entryId: magicNoteIdSchema
})
.strict()
export const magicTodoCreateSchema = z
.object({
projectId: magicNoteIdSchema.optional(),
title: z.string().trim().min(1).max(120),
instructions: z.string().trim().max(20_000)
})
.strict()
export type MagicTodoCreateInput = z.infer<typeof magicTodoCreateSchema>
export const magicTodoUpdateSchema = z
.object({
todoId: magicNoteIdSchema,
title: z.string().trim().min(1).max(120).optional(),
instructions: z.string().trim().max(20_000).optional(),
completed: z.boolean().optional(),
expectedRevision: z.number().int().nonnegative()
})
.strict()
.refine(
(input) =>
input.title !== undefined ||
input.instructions !== undefined ||
input.completed !== undefined,
{ message: '没有可更新的待办字段' }
)
export type MagicTodoUpdateInput = z.infer<typeof magicTodoUpdateSchema>
export const magicTodoIdSchema = z
.object({
todoId: magicNoteIdSchema
})
.strict()
export type MagicNoteCommentKind = 'summary' | 'suggestion' | 'warning'
export type MagicNoteComment = {
id: string
kind: MagicNoteCommentKind
content: string
}
export type MagicNoteEntry = {
id: string
noteId: string
content: MagicNoteRichContent
plainText: string
comments: MagicNoteComment[]
analyzedAt?: string
revision: number
createdAt: string
updatedAt: string
}
export type MagicNoteSummary = {
id: string
projectId?: string
title: string
preview: string
entryCount: number
pinned: boolean
revision: number
createdAt: string
updatedAt: string
}
export type MagicNoteDetail = MagicNoteSummary & {
entries: MagicNoteEntry[]
}
export type MagicNotesSnapshot = {
notes: MagicNoteSummary[]
}
export type MagicTodoItem = {
id: string
projectId?: string
noteId?: string
entryId?: string
noteTitle?: string
sourceIndex?: number
source: 'note' | 'manual'
title: string
instructions: string
completed: boolean
comments: MagicNoteComment[]
analyzedAt?: string
revision: number
createdAt: string
updatedAt: string
}
export type MagicTodosSnapshot = {
todos: MagicTodoItem[]
}