feat: add magic note tags and a two-pane notes layout

Keep the notes list beside the open note, compact the detail view with a full-width sticky toolbar, and make Back to overview return to a full-width all-notes grid. Add manual note tags with AND filtering, a tag manager for rename, merge and delete, schema 51 tag tables, and tag support in the note Agent tools.
This commit is contained in:
mesalogo
2026-09-30 19:04:47 +08:00
parent ab85e72f01
commit 6edfc0046b
28 changed files with 2736 additions and 370 deletions
@@ -903,6 +903,18 @@ describe('KnowledgeMcpGateway', () => {
expect(canvasEntry).toMatchObject({ content: 'Canvas text', contentKind: 'paged-canvas', contentVersion: 2, plainTextEditable: false })
expect(() => gateway.updateMagicNoteEntry(writeToken, { entryId: canvasEntry.id, expectedRevision: canvasEntry.revision, content: 'overwrite' })).toThrow('画布记录不能')
expect(database.getMagicNote(canvas.id)).toEqual(canvas)
// Tags: normalized on write, AND-filtered on list, replaced as a whole on update.
const tagged = gateway.createMagicNote(writeToken, { title: 'Tagged', tags: [' Work ', 'work', 'Q4'] })
expect(tagged.tags).toEqual(['Work', 'Q4'])
expect(gateway.listMagicNotes(readToken, { tags: ['WORK', 'q4'] })).toEqual([
expect.objectContaining({ id: tagged.id, tags: ['Work', 'Q4'] })
])
expect(gateway.listMagicNotes(readToken, { tags: ['Work', 'missing'] })).toEqual([])
expect(() => gateway.createMagicNote(writeToken, { title: 'Bad', tags: ['a,b'] })).toThrow()
const cleared = gateway.updateMagicNote(writeToken, { noteId: tagged.id, tags: [], expectedRevision: tagged.revision })
expect(cleared.tags).toEqual([])
expect(database.listMagicNoteTags()).toEqual([])
})
it('searches more than ten notes over MCP and returns recoverable argument errors', async () => {
+16 -5
View File
@@ -62,6 +62,7 @@ import type {
MagicNoteSearchResult,
MagicNoteSummary
} from '../../shared/magic-notes-contracts'
import { magicNoteTagListSchema } from '../../shared/magic-notes-contracts'
import {
magicNotePlainText,
validateMagicNoteRichContent
@@ -136,18 +137,20 @@ const {
} = magicNoteScopedDataToolCatalog
export type MagicNotesDatabase = {
listMagicNotes(): MagicNoteSummary[]
listMagicNotes(input?: { tags?: string[] }): MagicNoteSummary[]
getMagicNote(noteId: string): MagicNoteDetail
getMagicNoteEntry(entryId: string): MagicNoteEntry
searchMagicNotes(query: string, limit: number): MagicNoteSearchResult[]
createMagicNote(input: {
title: string
content?: MagicNoteContent
tags?: string[]
}): MagicNoteDetail
updateMagicNote(input: {
noteId: string
title?: string
pinned?: boolean
tags?: string[]
expectedRevision: number
}): MagicNoteDetail
deleteMagicNote(noteId: string): void
@@ -173,6 +176,7 @@ export type MagicNoteToolSummary = {
preview: string
entryCount: number
pinned: boolean
tags: string[]
revision: number
createdAt: string
updatedAt: string
@@ -278,6 +282,7 @@ function toMagicNoteToolSummary(
preview: note.preview.slice(0, 500),
entryCount: note.entryCount,
pinned: note.pinned,
tags: note.tags ?? [],
revision: note.revision,
createdAt: note.createdAt,
updatedAt: note.updatedAt
@@ -1376,9 +1381,11 @@ export class KnowledgeMcpGateway {
input: unknown = {}
): MagicNoteToolSummary[] {
const { database } = this.requireMagicNotes(token, 'read')
const { limit } = magicNoteListTool.inputSchema.parse(input)
const { limit, tags } = magicNoteListTool.inputSchema.parse(input)
const filter = tags ? magicNoteTagListSchema.parse(tags) : undefined
const notes: MagicNoteToolSummary[] = []
for (const note of database.listMagicNotes().slice(0, limit)) {
const listed = database.listMagicNotes(filter?.length ? { tags: filter } : {})
for (const note of listed.slice(0, limit)) {
const item = toMagicNoteToolSummary(note)
if (
Buffer.byteLength(JSON.stringify({ notes: [...notes, item] })) >
@@ -1467,7 +1474,8 @@ export class KnowledgeMcpGateway {
{
noteId: database.createMagicNote({
title: parsed.title,
...(content ? { content } : {})
...(content ? { content } : {}),
...(parsed.tags ? { tags: magicNoteTagListSchema.parse(parsed.tags) } : {})
}).id
}
)
@@ -1476,7 +1484,10 @@ export class KnowledgeMcpGateway {
updateMagicNote(token: string, input: unknown): MagicNoteToolDetail {
const { database } = this.requireMagicNotes(token, 'write')
const parsed = magicNoteUpdateTool.inputSchema.parse(input)
database.updateMagicNote(parsed)
database.updateMagicNote({
...parsed,
...(parsed.tags ? { tags: magicNoteTagListSchema.parse(parsed.tags) } : {})
})
return this.getMagicNote(token, { noteId: parsed.noteId })
}