feat: localize interface and enrich magic notes
This commit is contained in:
@@ -44,7 +44,7 @@ import {
|
||||
type RuntimeSelectionRepairSettings
|
||||
} from '../../shared/runtime-selection-contracts'
|
||||
import {
|
||||
MAGIC_NOTE_MAX_TOTAL_IMAGE_BYTES,
|
||||
MAGIC_NOTE_MAX_NOTE_EMBED_BYTES,
|
||||
type MagicNoteComment,
|
||||
type MagicNoteDetail,
|
||||
type MagicNoteEntry,
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
import type { ComputerControlAuditEvent } from '../computer-control/audit'
|
||||
import {
|
||||
magicNoteChecklistItems,
|
||||
magicNoteEmbeddedBytes,
|
||||
magicNoteImageBytes,
|
||||
magicNotePlainText,
|
||||
magicNotePreview,
|
||||
@@ -1917,7 +1918,7 @@ export class AssistantDatabase {
|
||||
const now = new Date().toISOString()
|
||||
database.exec('BEGIN IMMEDIATE')
|
||||
try {
|
||||
this.assertMagicNoteImageBudget(input.noteId, input.content)
|
||||
this.assertMagicNoteEmbedBudget(input.noteId, input.content)
|
||||
const noteResult = database
|
||||
.prepare(
|
||||
`UPDATE magic_notes
|
||||
@@ -1943,7 +1944,7 @@ export class AssistantDatabase {
|
||||
input.plainText,
|
||||
now,
|
||||
now,
|
||||
magicNoteImageBytes(input.content)
|
||||
magicNoteEmbeddedBytes(input.content)
|
||||
)
|
||||
this.syncMagicNoteTodos(
|
||||
database,
|
||||
@@ -1976,7 +1977,7 @@ export class AssistantDatabase {
|
||||
const now = new Date().toISOString()
|
||||
database.exec('BEGIN IMMEDIATE')
|
||||
try {
|
||||
this.assertMagicNoteImageBudget(
|
||||
this.assertMagicNoteEmbedBudget(
|
||||
existing.note_id,
|
||||
input.content,
|
||||
input.entryId
|
||||
@@ -1993,7 +1994,7 @@ export class AssistantDatabase {
|
||||
JSON.stringify(input.content),
|
||||
input.plainText,
|
||||
now,
|
||||
magicNoteImageBytes(input.content),
|
||||
magicNoteEmbeddedBytes(input.content),
|
||||
input.entryId,
|
||||
input.expectedRevision
|
||||
)
|
||||
@@ -4256,7 +4257,7 @@ export class AssistantDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
private assertMagicNoteImageBudget(
|
||||
private assertMagicNoteEmbedBudget(
|
||||
noteId: string,
|
||||
content: MagicNoteRichContent,
|
||||
excludedEntryId?: string
|
||||
@@ -4269,10 +4270,10 @@ export class AssistantDatabase {
|
||||
)
|
||||
.get(noteId, excludedEntryId ?? '') as { image_bytes: number }
|
||||
if (
|
||||
existing.image_bytes + magicNoteImageBytes(content) >
|
||||
MAGIC_NOTE_MAX_TOTAL_IMAGE_BYTES
|
||||
existing.image_bytes + magicNoteEmbeddedBytes(content) >
|
||||
MAGIC_NOTE_MAX_NOTE_EMBED_BYTES
|
||||
) {
|
||||
throw new Error('一篇笔记中的图片总大小不能超过 8 MB')
|
||||
throw new Error('一篇笔记中的图片、视频和附件总大小不能超过 64 MB')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
magicNoteChecklistItems,
|
||||
magicNoteEmbeddedBytes,
|
||||
magicNoteImageBytes,
|
||||
magicNotePlainText,
|
||||
setMagicNoteChecklistCompletion,
|
||||
@@ -10,6 +11,13 @@ import {
|
||||
const pngDataUrl = `data:image/png;base64,${Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
|
||||
]).toString('base64')}`
|
||||
const mp4Bytes = Buffer.from([
|
||||
0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d
|
||||
])
|
||||
const mp4DataUrl = `data:video/mp4;base64,${mp4Bytes.toString('base64')}`
|
||||
const attachmentBytes = Buffer.from('release notes')
|
||||
const attachmentDataUrl =
|
||||
`data:text/plain;base64,${attachmentBytes.toString('base64')}`
|
||||
|
||||
describe('magic note rich content', () => {
|
||||
it('accepts bounded text formats and signature-checked local images', () => {
|
||||
@@ -59,6 +67,86 @@ describe('magic note rich content', () => {
|
||||
).toThrow('图片内容与声明的格式不一致')
|
||||
})
|
||||
|
||||
it('accepts bounded font formats, local videos, and attachments', () => {
|
||||
const content = validateMagicNoteRichContent({
|
||||
version: 1,
|
||||
ops: [
|
||||
{
|
||||
insert: '重点',
|
||||
attributes: { size: 'large', color: '#e60000' }
|
||||
},
|
||||
{ insert: '\n' },
|
||||
{
|
||||
insert: {
|
||||
localVideo: {
|
||||
name: 'demo.mp4',
|
||||
mimeType: 'video/mp4',
|
||||
size: mp4Bytes.length,
|
||||
dataUrl: mp4DataUrl
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
insert: {
|
||||
attachment: {
|
||||
name: 'notes.txt',
|
||||
mimeType: 'text/plain',
|
||||
size: attachmentBytes.length,
|
||||
dataUrl: attachmentDataUrl
|
||||
}
|
||||
}
|
||||
},
|
||||
{ insert: '\n' }
|
||||
]
|
||||
})
|
||||
|
||||
expect(magicNotePlainText(content)).toBe(
|
||||
'重点\n[视频:demo.mp4][附件:notes.txt]'
|
||||
)
|
||||
expect(magicNoteImageBytes(content)).toBe(0)
|
||||
expect(magicNoteEmbeddedBytes(content)).toBe(
|
||||
mp4Bytes.length + attachmentBytes.length
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects spoofed videos and mismatched attachment metadata', () => {
|
||||
expect(() =>
|
||||
validateMagicNoteRichContent({
|
||||
version: 1,
|
||||
ops: [
|
||||
{
|
||||
insert: {
|
||||
localVideo: {
|
||||
name: 'demo.mp4',
|
||||
mimeType: 'video/mp4',
|
||||
size: attachmentBytes.length,
|
||||
dataUrl: `data:video/mp4;base64,${attachmentBytes.toString('base64')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
).toThrow('视频内容与声明的格式不一致')
|
||||
|
||||
expect(() =>
|
||||
validateMagicNoteRichContent({
|
||||
version: 1,
|
||||
ops: [
|
||||
{
|
||||
insert: {
|
||||
attachment: {
|
||||
name: 'notes.txt',
|
||||
mimeType: 'text/plain',
|
||||
size: attachmentBytes.length + 1,
|
||||
dataUrl: attachmentDataUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
).toThrow('附件内容与声明的大小不一致')
|
||||
})
|
||||
|
||||
it('rejects more than twelve images in one record', () => {
|
||||
expect(() =>
|
||||
validateMagicNoteRichContent({
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
MAGIC_NOTE_MAX_ATTACHMENT_BYTES,
|
||||
MAGIC_NOTE_MAX_IMAGE_BYTES,
|
||||
magicNoteImageDataBytes,
|
||||
MAGIC_NOTE_MAX_VIDEO_BYTES,
|
||||
MAGIC_NOTE_VIDEO_TYPES,
|
||||
magicNoteDataBytes,
|
||||
magicNoteRichContentSchema,
|
||||
type MagicNoteRichContent
|
||||
} from '../../shared/magic-notes-contracts'
|
||||
@@ -52,6 +55,67 @@ function validateImage(dataUrl: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
type EmbeddedFile = {
|
||||
name: string
|
||||
mimeType: string
|
||||
size: number
|
||||
dataUrl: string
|
||||
}
|
||||
|
||||
function decodeEmbeddedFile(
|
||||
file: EmbeddedFile,
|
||||
maxBytes: number
|
||||
): Buffer {
|
||||
const separatorIndex = file.dataUrl.indexOf(',')
|
||||
const prefix = file.dataUrl.slice(0, separatorIndex)
|
||||
const payload = file.dataUrl.slice(separatorIndex + 1)
|
||||
if (prefix !== `data:${file.mimeType};base64`) {
|
||||
throw new Error('附件内容与声明的类型不一致')
|
||||
}
|
||||
const bytes = Buffer.from(payload, 'base64')
|
||||
if (
|
||||
bytes.length === 0 ||
|
||||
bytes.length > maxBytes ||
|
||||
bytes.length !== file.size
|
||||
) {
|
||||
throw new Error('附件内容与声明的大小不一致')
|
||||
}
|
||||
if (bytes.toString('base64') !== payload) {
|
||||
throw new Error('附件数据格式无效')
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
function validateVideo(file: EmbeddedFile): void {
|
||||
const bytes = decodeEmbeddedFile(file, MAGIC_NOTE_MAX_VIDEO_BYTES)
|
||||
const hasIsoBaseMediaSignature =
|
||||
bytes.length >= 12 &&
|
||||
bytes.subarray(4, 8).toString('ascii') === 'ftyp'
|
||||
const signatureMatches =
|
||||
(file.mimeType === 'video/mp4' && hasIsoBaseMediaSignature) ||
|
||||
(file.mimeType === 'video/quicktime' && hasIsoBaseMediaSignature) ||
|
||||
(file.mimeType === 'video/webm' &&
|
||||
bytes.length >= 4 &&
|
||||
bytes.subarray(0, 4).equals(
|
||||
Buffer.from([0x1a, 0x45, 0xdf, 0xa3])
|
||||
)) ||
|
||||
(file.mimeType === 'video/ogg' &&
|
||||
bytes.length >= 4 &&
|
||||
bytes.subarray(0, 4).toString('ascii') === 'OggS')
|
||||
if (
|
||||
!MAGIC_NOTE_VIDEO_TYPES.includes(
|
||||
file.mimeType as (typeof MAGIC_NOTE_VIDEO_TYPES)[number]
|
||||
) ||
|
||||
!signatureMatches
|
||||
) {
|
||||
throw new Error('视频内容与声明的格式不一致')
|
||||
}
|
||||
}
|
||||
|
||||
function validateAttachment(file: EmbeddedFile): void {
|
||||
decodeEmbeddedFile(file, MAGIC_NOTE_MAX_ATTACHMENT_BYTES)
|
||||
}
|
||||
|
||||
export function validateMagicNoteRichContent(
|
||||
input: unknown
|
||||
): MagicNoteRichContent {
|
||||
@@ -61,9 +125,15 @@ export function validateMagicNoteRichContent(
|
||||
continue
|
||||
}
|
||||
if (operation.attributes !== undefined) {
|
||||
throw new Error('图片嵌入不支持行内格式')
|
||||
throw new Error('嵌入内容不支持行内格式')
|
||||
}
|
||||
if ('image' in operation.insert) {
|
||||
validateImage(operation.insert.image)
|
||||
} else if ('localVideo' in operation.insert) {
|
||||
validateVideo(operation.insert.localVideo)
|
||||
} else {
|
||||
validateAttachment(operation.insert.attachment)
|
||||
}
|
||||
validateImage(operation.insert.image)
|
||||
}
|
||||
return content
|
||||
}
|
||||
@@ -75,7 +145,11 @@ export function magicNotePlainText(
|
||||
.map((operation) =>
|
||||
typeof operation.insert === 'string'
|
||||
? operation.insert
|
||||
: '[图片]'
|
||||
: 'image' in operation.insert
|
||||
? '[图片]'
|
||||
: 'localVideo' in operation.insert
|
||||
? `[视频:${operation.insert.localVideo.name}]`
|
||||
: `[附件:${operation.insert.attachment.name}]`
|
||||
)
|
||||
.join('')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
@@ -89,7 +163,29 @@ export function magicNoteImageBytes(
|
||||
if (typeof operation.insert === 'string') {
|
||||
return total
|
||||
}
|
||||
return total + magicNoteImageDataBytes(operation.insert.image)
|
||||
return (
|
||||
total +
|
||||
('image' in operation.insert
|
||||
? magicNoteDataBytes(operation.insert.image)
|
||||
: 0)
|
||||
)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
export function magicNoteEmbeddedBytes(
|
||||
content: MagicNoteRichContent
|
||||
): number {
|
||||
return content.ops.reduce((total, operation) => {
|
||||
if (typeof operation.insert === 'string') {
|
||||
return total
|
||||
}
|
||||
if ('image' in operation.insert) {
|
||||
return total + magicNoteDataBytes(operation.insert.image)
|
||||
}
|
||||
if ('localVideo' in operation.insert) {
|
||||
return total + magicNoteDataBytes(operation.insert.localVideo.dataUrl)
|
||||
}
|
||||
return total + magicNoteDataBytes(operation.insert.attachment.dataUrl)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
@@ -119,7 +215,12 @@ export function magicNoteChecklistItems(
|
||||
let sourceIndex = 0
|
||||
for (const operation of content.ops) {
|
||||
if (typeof operation.insert !== 'string') {
|
||||
line += '[图片]'
|
||||
line +=
|
||||
'image' in operation.insert
|
||||
? '[图片]'
|
||||
: 'localVideo' in operation.insert
|
||||
? `[视频:${operation.insert.localVideo.name}]`
|
||||
: `[附件:${operation.insert.attachment.name}]`
|
||||
continue
|
||||
}
|
||||
const segments = operation.insert.split(/(\n)/u)
|
||||
|
||||
Reference in New Issue
Block a user