feat: globalize Magic Notes and improve runtime tools
This commit is contained in:
@@ -98,7 +98,7 @@ describe('AssistantDatabase', () => {
|
||||
database.close()
|
||||
})
|
||||
|
||||
it('migrates existing databases to schema version 15', async () => {
|
||||
it('migrates existing databases to schema version 17', async () => {
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'goodbuddy-assistant-migration-')
|
||||
)
|
||||
@@ -127,7 +127,7 @@ describe('AssistantDatabase', () => {
|
||||
user_version: number
|
||||
}
|
||||
).user_version
|
||||
).toBe(16)
|
||||
).toBe(17)
|
||||
expect(
|
||||
current
|
||||
.prepare(
|
||||
@@ -231,7 +231,7 @@ describe('AssistantDatabase', () => {
|
||||
user_version: number
|
||||
}
|
||||
).user_version
|
||||
).toBe(16)
|
||||
).toBe(17)
|
||||
expect(
|
||||
current
|
||||
.prepare(
|
||||
@@ -277,9 +277,7 @@ describe('AssistantDatabase', () => {
|
||||
const databasePath = join(directory, 'assistant.sqlite')
|
||||
const initial = new AssistantDatabase(databasePath)
|
||||
initial.initialize('C:\\Workspace')
|
||||
const project = initial.listProjects()[0]!
|
||||
const note = initial.createMagicNote({
|
||||
projectId: project.id,
|
||||
title: '迁移笔记'
|
||||
})
|
||||
initial.createMagicNoteEntry({
|
||||
@@ -304,7 +302,7 @@ describe('AssistantDatabase', () => {
|
||||
|
||||
const migrated = new AssistantDatabase(databasePath)
|
||||
migrated.initialize('C:\\Workspace')
|
||||
expect(migrated.listMagicTodos(project.id)).toEqual([
|
||||
expect(migrated.listMagicTodos()).toEqual([
|
||||
expect.objectContaining({
|
||||
noteId: note.id,
|
||||
source: 'note',
|
||||
@@ -315,6 +313,63 @@ describe('AssistantDatabase', () => {
|
||||
migrated.close()
|
||||
})
|
||||
|
||||
it('makes existing notes global and migrates manual todos into one note', async () => {
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'goodbuddy-global-magic-notes-migration-')
|
||||
)
|
||||
temporaryDirectories.push(directory)
|
||||
const databasePath = join(directory, 'assistant.sqlite')
|
||||
const initial = new AssistantDatabase(databasePath)
|
||||
initial.initialize('C:\\Workspace')
|
||||
const project = initial.listProjects()[0]!
|
||||
const note = initial.createMagicNote({ title: '原项目笔记' })
|
||||
initial.close()
|
||||
|
||||
const legacy = new DatabaseSync(databasePath)
|
||||
const now = '2026-08-10T00:00:00.000Z'
|
||||
legacy
|
||||
.prepare('UPDATE magic_notes SET project_id = ? WHERE id = ?')
|
||||
.run(project.id, note.id)
|
||||
legacy
|
||||
.prepare(
|
||||
`INSERT INTO magic_todos
|
||||
(id, project_id, note_id, entry_id, source_index, source,
|
||||
title, instructions, completed, comments_json, analyzed_at,
|
||||
revision, created_at, updated_at)
|
||||
VALUES (?, ?, NULL, NULL, NULL, 'manual', ?, ?, 1, '[]',
|
||||
NULL, 0, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
'00000000-0000-4000-8000-000000000099',
|
||||
project.id,
|
||||
'旧手动待办',
|
||||
'保留的说明',
|
||||
now,
|
||||
now
|
||||
)
|
||||
legacy.exec('PRAGMA user_version = 16')
|
||||
legacy.close()
|
||||
|
||||
const migrated = new AssistantDatabase(databasePath)
|
||||
migrated.initialize('C:\\Workspace')
|
||||
expect(migrated.listMagicNotes()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: note.id, title: '原项目笔记' }),
|
||||
expect.objectContaining({ title: '迁入的待办' })
|
||||
])
|
||||
)
|
||||
expect(migrated.listMagicTodos()).toEqual([
|
||||
expect.objectContaining({
|
||||
source: 'note',
|
||||
title: '旧手动待办',
|
||||
instructions: '保留的说明',
|
||||
completed: true,
|
||||
noteTitle: '迁入的待办'
|
||||
})
|
||||
])
|
||||
migrated.close()
|
||||
})
|
||||
|
||||
it('creates a default project and persists project updates', async () => {
|
||||
const database = await createDatabase()
|
||||
const [defaultProject] = database.listProjects()
|
||||
@@ -1719,26 +1774,24 @@ describe('AssistantDatabase', () => {
|
||||
database.close()
|
||||
})
|
||||
|
||||
it('persists scoped magic notes and AI comments without todo proposals', async () => {
|
||||
it('persists global magic notes and AI comments without todo proposals', async () => {
|
||||
const database = await createDatabase()
|
||||
const project = database.listProjects()[0]!
|
||||
const globalNote = database.createMagicNote({
|
||||
title: '全局笔记'
|
||||
})
|
||||
const projectNote = database.createMagicNote({
|
||||
projectId: project.id,
|
||||
title: '项目笔记'
|
||||
const secondNote = database.createMagicNote({
|
||||
title: '第二篇笔记'
|
||||
})
|
||||
|
||||
expect(database.listMagicNotes()).toEqual([
|
||||
expect.objectContaining({ id: globalNote.id, title: '全局笔记' })
|
||||
])
|
||||
expect(database.listMagicNotes(project.id)).toEqual([
|
||||
expect.objectContaining({ id: projectNote.id, title: '项目笔记' })
|
||||
])
|
||||
expect(database.listMagicNotes()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: globalNote.id, title: '全局笔记' }),
|
||||
expect.objectContaining({ id: secondNote.id, title: '第二篇笔记' })
|
||||
])
|
||||
)
|
||||
|
||||
const withEntry = database.createMagicNoteEntry({
|
||||
noteId: projectNote.id,
|
||||
noteId: secondNote.id,
|
||||
content: {
|
||||
version: 1,
|
||||
ops: [
|
||||
@@ -1753,6 +1806,14 @@ describe('AssistantDatabase', () => {
|
||||
entryCount: 1,
|
||||
preview: '整理发布清单'
|
||||
})
|
||||
expect(database.searchMagicNotes('发布', 5)).toEqual([
|
||||
expect.objectContaining({
|
||||
noteId: secondNote.id,
|
||||
noteTitle: '第二篇笔记',
|
||||
entryId: entry.id,
|
||||
content: '整理发布清单'
|
||||
})
|
||||
])
|
||||
|
||||
const analyzed = database.saveMagicNoteAnalysis({
|
||||
entryId: entry.id,
|
||||
@@ -1775,11 +1836,9 @@ describe('AssistantDatabase', () => {
|
||||
database.close()
|
||||
})
|
||||
|
||||
it('synchronizes note checklists and standalone magic todos bidirectionally', async () => {
|
||||
it('synchronizes derived todos when note checklists change', async () => {
|
||||
const database = await createDatabase()
|
||||
const project = database.listProjects()[0]!
|
||||
const note = database.createMagicNote({
|
||||
projectId: project.id,
|
||||
title: '发布笔记'
|
||||
})
|
||||
const withEntry = database.createMagicNoteEntry({
|
||||
@@ -1797,7 +1856,7 @@ describe('AssistantDatabase', () => {
|
||||
})
|
||||
const entry = withEntry.entries[0]!
|
||||
|
||||
const noteTodos = database.listMagicTodos(project.id)
|
||||
const noteTodos = database.listMagicTodos()
|
||||
expect(noteTodos).toEqual([
|
||||
expect.objectContaining({
|
||||
noteId: note.id,
|
||||
@@ -1813,21 +1872,6 @@ describe('AssistantDatabase', () => {
|
||||
})
|
||||
])
|
||||
|
||||
const completed = database.updateMagicTodo({
|
||||
todoId: noteTodos[0]!.id,
|
||||
completed: true,
|
||||
expectedRevision: noteTodos[0]!.revision
|
||||
})
|
||||
expect(completed.completed).toBe(true)
|
||||
expect(database.getMagicNote(note.id).entries[0]!.content.ops).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
insert: '\n',
|
||||
attributes: expect.objectContaining({ list: 'checked' })
|
||||
})
|
||||
])
|
||||
)
|
||||
|
||||
const updatedEntry = database.getMagicNote(note.id).entries[0]!
|
||||
database.updateMagicNoteEntry({
|
||||
entryId: entry.id,
|
||||
@@ -1845,7 +1889,7 @@ describe('AssistantDatabase', () => {
|
||||
},
|
||||
plainText: '新增首项\n上传构建产物\n核对发布材料'
|
||||
})
|
||||
const reordered = database.listMagicTodos(project.id)
|
||||
const reordered = database.listMagicTodos()
|
||||
expect(
|
||||
reordered.find((todo) => todo.title === '核对发布材料')
|
||||
).toMatchObject({
|
||||
@@ -1861,30 +1905,6 @@ describe('AssistantDatabase', () => {
|
||||
sourceIndex: 1
|
||||
})
|
||||
|
||||
const manual = database.createMagicTodo({
|
||||
projectId: project.id,
|
||||
title: '手动待办',
|
||||
instructions: '补充验收说明'
|
||||
})
|
||||
expect(manual).toMatchObject({
|
||||
source: 'manual',
|
||||
completed: false,
|
||||
title: '手动待办'
|
||||
})
|
||||
const edited = database.updateMagicTodo({
|
||||
todoId: manual.id,
|
||||
title: '更新后的手动待办',
|
||||
instructions: '新的说明',
|
||||
expectedRevision: manual.revision
|
||||
})
|
||||
expect(edited).toMatchObject({
|
||||
title: '更新后的手动待办',
|
||||
instructions: '新的说明'
|
||||
})
|
||||
database.deleteMagicTodo(edited.id)
|
||||
expect(
|
||||
database.listMagicTodos(project.id).some((todo) => todo.id === edited.id)
|
||||
).toBe(false)
|
||||
database.close()
|
||||
})
|
||||
|
||||
@@ -1964,7 +1984,6 @@ describe('AssistantDatabase', () => {
|
||||
cacheWrite: 1
|
||||
})
|
||||
database.createMagicNote({
|
||||
projectId: project.id,
|
||||
title: '待清除笔记'
|
||||
})
|
||||
expect(database.getTokenUsageSummary().totals.totalTokens).toBe(15)
|
||||
@@ -1978,7 +1997,7 @@ describe('AssistantDatabase', () => {
|
||||
expect(database.listHeartbeatConfigs(project.id)).toEqual([])
|
||||
expect(database.listTasks()).toEqual([])
|
||||
expect(database.listArtifacts(project.id)).toEqual([])
|
||||
expect(database.listMagicNotes(project.id)).toEqual([])
|
||||
expect(database.listMagicNotes()).toEqual([])
|
||||
expect(database.getTokenUsageSummary()).toEqual({
|
||||
totals: {
|
||||
callCount: 0,
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
type MagicNoteDetail,
|
||||
type MagicNoteEntry,
|
||||
type MagicNoteRichContent,
|
||||
type MagicNoteSearchResult,
|
||||
type MagicNoteSummary,
|
||||
type MagicTodoItem
|
||||
} from '../../shared/magic-notes-contracts'
|
||||
@@ -56,8 +57,7 @@ import type { ComputerControlAuditEvent } from '../computer-control/audit'
|
||||
import {
|
||||
magicNoteChecklistItems,
|
||||
magicNoteImageBytes,
|
||||
magicNotePreview,
|
||||
setMagicNoteChecklistCompletion
|
||||
magicNotePreview
|
||||
} from '../magic-notes/rich-content'
|
||||
import { computeNextHeartbeatRun } from './heartbeat-recurrence'
|
||||
|
||||
@@ -409,14 +409,22 @@ function toMagicNoteEntry(row: MagicNoteEntryRow): MagicNoteEntry {
|
||||
}
|
||||
|
||||
function toMagicTodo(row: MagicTodoRow): MagicTodoItem {
|
||||
if (
|
||||
row.source !== 'note' ||
|
||||
!row.note_id ||
|
||||
!row.entry_id ||
|
||||
row.source_index === null ||
|
||||
!row.note_title
|
||||
) {
|
||||
throw new Error('待办来源数据无效')
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.project_id ?? undefined,
|
||||
noteId: row.note_id ?? undefined,
|
||||
entryId: row.entry_id ?? undefined,
|
||||
noteTitle: row.note_title ?? undefined,
|
||||
sourceIndex: row.source_index ?? undefined,
|
||||
source: row.source,
|
||||
noteId: row.note_id,
|
||||
entryId: row.entry_id,
|
||||
noteTitle: row.note_title,
|
||||
sourceIndex: row.source_index,
|
||||
source: 'note',
|
||||
title: row.title,
|
||||
instructions: row.instructions,
|
||||
completed: row.completed === 1,
|
||||
@@ -431,7 +439,6 @@ function toMagicTodo(row: MagicTodoRow): MagicTodoItem {
|
||||
function toMagicNoteSummary(row: MagicNoteRow): MagicNoteSummary {
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.project_id ?? undefined,
|
||||
title: row.title,
|
||||
preview: magicNotePreview(row.latest_plain_text ?? ''),
|
||||
entryCount: row.entry_count,
|
||||
@@ -1774,7 +1781,7 @@ export class AssistantDatabase {
|
||||
}))
|
||||
}
|
||||
|
||||
listMagicNotes(projectId?: string): MagicNoteSummary[] {
|
||||
listMagicNotes(): MagicNoteSummary[] {
|
||||
const database = this.requireDatabase()
|
||||
const rows = database
|
||||
.prepare(
|
||||
@@ -1786,11 +1793,10 @@ export class AssistantDatabase {
|
||||
ORDER BY e.created_at DESC, e.rowid DESC LIMIT 1)
|
||||
AS latest_plain_text
|
||||
FROM magic_notes n
|
||||
WHERE n.project_id IS ?
|
||||
ORDER BY n.pinned DESC, n.updated_at DESC, n.rowid DESC
|
||||
LIMIT 200`
|
||||
)
|
||||
.all(projectId ?? null) as MagicNoteRow[]
|
||||
.all() as MagicNoteRow[]
|
||||
return rows.map(toMagicNoteSummary)
|
||||
}
|
||||
|
||||
@@ -1827,7 +1833,6 @@ export class AssistantDatabase {
|
||||
|
||||
getMagicNoteContext(noteId: string): {
|
||||
id: string
|
||||
projectId?: string
|
||||
title: string
|
||||
} {
|
||||
const row = this.requireDatabase()
|
||||
@@ -1844,15 +1849,11 @@ export class AssistantDatabase {
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.project_id ?? undefined,
|
||||
title: row.title
|
||||
}
|
||||
}
|
||||
|
||||
createMagicNote(input: {
|
||||
projectId?: string
|
||||
title: string
|
||||
}): MagicNoteDetail {
|
||||
createMagicNote(input: { title: string }): MagicNoteDetail {
|
||||
const id = randomUUID()
|
||||
const now = new Date().toISOString()
|
||||
this.requireDatabase()
|
||||
@@ -1861,7 +1862,7 @@ export class AssistantDatabase {
|
||||
(id, project_id, title, pinned, revision, created_at, updated_at)
|
||||
VALUES (?, ?, ?, 0, 0, ?, ?)`
|
||||
)
|
||||
.run(id, input.projectId ?? null, input.title, now, now)
|
||||
.run(id, null, input.title, now, now)
|
||||
return this.getMagicNote(id)
|
||||
}
|
||||
|
||||
@@ -2090,21 +2091,51 @@ export class AssistantDatabase {
|
||||
return this.getMagicNote(existing.note_id)
|
||||
}
|
||||
|
||||
listMagicTodos(projectId?: string): MagicTodoItem[] {
|
||||
listMagicTodos(): MagicTodoItem[] {
|
||||
return (
|
||||
this.requireDatabase()
|
||||
.prepare(
|
||||
`SELECT t.*, n.title AS note_title
|
||||
FROM magic_todos t
|
||||
LEFT JOIN magic_notes n ON n.id = t.note_id
|
||||
WHERE t.project_id IS ?
|
||||
WHERE t.source = 'note'
|
||||
ORDER BY t.completed ASC, t.updated_at DESC, t.rowid DESC
|
||||
LIMIT 500`
|
||||
)
|
||||
.all(projectId ?? null) as MagicTodoRow[]
|
||||
.all() as MagicTodoRow[]
|
||||
).map(toMagicTodo)
|
||||
}
|
||||
|
||||
searchMagicNotes(query: string, limit: number): MagicNoteSearchResult[] {
|
||||
const pattern = `%${query.replace(/[\\%_]/gu, '\\$&')}%`
|
||||
return (
|
||||
this.requireDatabase()
|
||||
.prepare(
|
||||
`SELECT n.id AS note_id, n.title AS note_title,
|
||||
e.id AS entry_id, e.plain_text, e.updated_at
|
||||
FROM magic_note_entries e
|
||||
INNER JOIN magic_notes n ON n.id = e.note_id
|
||||
WHERE n.title LIKE ? ESCAPE '\\'
|
||||
OR e.plain_text LIKE ? ESCAPE '\\'
|
||||
ORDER BY e.updated_at DESC, e.rowid DESC
|
||||
LIMIT ?`
|
||||
)
|
||||
.all(pattern, pattern, limit) as Array<{
|
||||
note_id: string
|
||||
note_title: string
|
||||
entry_id: string
|
||||
plain_text: string
|
||||
updated_at: string
|
||||
}>
|
||||
).map((row) => ({
|
||||
noteId: row.note_id,
|
||||
noteTitle: row.note_title.slice(0, 100),
|
||||
entryId: row.entry_id,
|
||||
content: row.plain_text.slice(0, 12_000),
|
||||
updatedAt: row.updated_at
|
||||
}))
|
||||
}
|
||||
|
||||
getMagicTodo(todoId: string): MagicTodoItem {
|
||||
const row = this.requireDatabase()
|
||||
.prepare(
|
||||
@@ -2120,129 +2151,6 @@ export class AssistantDatabase {
|
||||
return toMagicTodo(row)
|
||||
}
|
||||
|
||||
createMagicTodo(input: {
|
||||
projectId?: string
|
||||
title: string
|
||||
instructions: string
|
||||
}): MagicTodoItem {
|
||||
const id = randomUUID()
|
||||
const now = new Date().toISOString()
|
||||
this.requireDatabase()
|
||||
.prepare(
|
||||
`INSERT INTO magic_todos
|
||||
(id, project_id, note_id, entry_id, source_index, source,
|
||||
title, instructions, completed, comments_json, analyzed_at,
|
||||
revision, created_at, updated_at)
|
||||
VALUES (?, ?, NULL, NULL, NULL, 'manual', ?, ?, 0, '[]',
|
||||
NULL, 0, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
id,
|
||||
input.projectId ?? null,
|
||||
input.title,
|
||||
input.instructions,
|
||||
now,
|
||||
now
|
||||
)
|
||||
return this.getMagicTodo(id)
|
||||
}
|
||||
|
||||
updateMagicTodo(input: {
|
||||
todoId: string
|
||||
title?: string
|
||||
instructions?: string
|
||||
completed?: boolean
|
||||
expectedRevision: number
|
||||
}): MagicTodoItem {
|
||||
const database = this.requireDatabase()
|
||||
const existing = database
|
||||
.prepare('SELECT * FROM magic_todos WHERE id = ?')
|
||||
.get(input.todoId) as Omit<MagicTodoRow, 'note_title'> | undefined
|
||||
if (!existing) {
|
||||
throw new Error('待办不存在')
|
||||
}
|
||||
if (
|
||||
existing.source === 'note' &&
|
||||
(input.title !== undefined || input.instructions !== undefined)
|
||||
) {
|
||||
throw new Error('笔记待办的内容需要在原笔记中编辑')
|
||||
}
|
||||
const now = new Date().toISOString()
|
||||
database.exec('BEGIN IMMEDIATE')
|
||||
try {
|
||||
const result = database
|
||||
.prepare(
|
||||
`UPDATE magic_todos
|
||||
SET title = COALESCE(?, title),
|
||||
instructions = COALESCE(?, instructions),
|
||||
completed = COALESCE(?, completed),
|
||||
revision = revision + 1,
|
||||
updated_at = ?
|
||||
WHERE id = ? AND revision = ?`
|
||||
)
|
||||
.run(
|
||||
input.title ?? null,
|
||||
input.instructions ?? null,
|
||||
input.completed === undefined ? null : Number(input.completed),
|
||||
now,
|
||||
input.todoId,
|
||||
input.expectedRevision
|
||||
)
|
||||
if (result.changes !== 1) {
|
||||
throw new Error('待办已被更新,请刷新后重试')
|
||||
}
|
||||
if (
|
||||
existing.source === 'note' &&
|
||||
input.completed !== undefined &&
|
||||
existing.entry_id &&
|
||||
existing.note_id &&
|
||||
existing.source_index !== null
|
||||
) {
|
||||
const entry = database
|
||||
.prepare(
|
||||
'SELECT content_json FROM magic_note_entries WHERE id = ?'
|
||||
)
|
||||
.get(existing.entry_id) as { content_json: string } | undefined
|
||||
if (!entry) {
|
||||
throw new Error('待办来源记录不存在')
|
||||
}
|
||||
const content = setMagicNoteChecklistCompletion(
|
||||
JSON.parse(entry.content_json) as MagicNoteRichContent,
|
||||
existing.source_index,
|
||||
input.completed
|
||||
)
|
||||
database
|
||||
.prepare(
|
||||
`UPDATE magic_note_entries
|
||||
SET content_json = ?, revision = revision + 1, updated_at = ?
|
||||
WHERE id = ?`
|
||||
)
|
||||
.run(JSON.stringify(content), now, existing.entry_id)
|
||||
database
|
||||
.prepare(
|
||||
`UPDATE magic_notes
|
||||
SET revision = revision + 1, updated_at = ?
|
||||
WHERE id = ?`
|
||||
)
|
||||
.run(now, existing.note_id)
|
||||
}
|
||||
database.exec('COMMIT')
|
||||
} catch (error) {
|
||||
database.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
return this.getMagicTodo(input.todoId)
|
||||
}
|
||||
|
||||
deleteMagicTodo(todoId: string): void {
|
||||
const result = this.requireDatabase()
|
||||
.prepare("DELETE FROM magic_todos WHERE id = ? AND source = 'manual'")
|
||||
.run(todoId)
|
||||
if (result.changes !== 1) {
|
||||
throw new Error('手动待办不存在')
|
||||
}
|
||||
}
|
||||
|
||||
saveMagicTodoAnalysis(input: {
|
||||
todoId: string
|
||||
expectedRevision: number
|
||||
@@ -4082,8 +3990,8 @@ export class AssistantDatabase {
|
||||
now: string
|
||||
): void {
|
||||
const note = database
|
||||
.prepare('SELECT project_id FROM magic_notes WHERE id = ?')
|
||||
.get(noteId) as { project_id: string | null } | undefined
|
||||
.prepare('SELECT id FROM magic_notes WHERE id = ?')
|
||||
.get(noteId) as { id: string } | undefined
|
||||
if (!note) {
|
||||
throw new Error('笔记不存在')
|
||||
}
|
||||
@@ -4197,7 +4105,7 @@ export class AssistantDatabase {
|
||||
if (!matched) {
|
||||
insertTodo.run(
|
||||
randomUUID(),
|
||||
note.project_id,
|
||||
null,
|
||||
noteId,
|
||||
entryId,
|
||||
item.sourceIndex,
|
||||
@@ -4214,7 +4122,7 @@ export class AssistantDatabase {
|
||||
const positionChanged =
|
||||
matched.source_index !== item.sourceIndex
|
||||
const scopeChanged =
|
||||
matched.project_id !== note.project_id ||
|
||||
matched.project_id !== null ||
|
||||
matched.note_id !== noteId
|
||||
if (
|
||||
!titleChanged &&
|
||||
@@ -4225,7 +4133,7 @@ export class AssistantDatabase {
|
||||
continue
|
||||
}
|
||||
updateTodo.run(
|
||||
note.project_id,
|
||||
null,
|
||||
noteId,
|
||||
item.sourceIndex,
|
||||
item.title,
|
||||
@@ -4262,12 +4170,12 @@ export class AssistantDatabase {
|
||||
const version = database
|
||||
.prepare('PRAGMA user_version')
|
||||
.get() as { user_version: number }
|
||||
if (version.user_version > 16) {
|
||||
if (version.user_version > 17) {
|
||||
throw new Error(
|
||||
`当前 GoodBuddy 不支持助理数据库版本 ${version.user_version},请升级应用后重试`
|
||||
)
|
||||
}
|
||||
if (version.user_version === 16) {
|
||||
if (version.user_version === 17) {
|
||||
return
|
||||
}
|
||||
if (version.user_version < 1) {
|
||||
@@ -4997,6 +4905,118 @@ export class AssistantDatabase {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
if (version.user_version < 17) {
|
||||
database.exec('BEGIN IMMEDIATE')
|
||||
try {
|
||||
database.exec(`
|
||||
UPDATE magic_notes SET project_id = NULL
|
||||
WHERE project_id IS NOT NULL;
|
||||
UPDATE magic_todos SET project_id = NULL
|
||||
WHERE source = 'note' AND project_id IS NOT NULL;
|
||||
`)
|
||||
const manualTodos = database
|
||||
.prepare(
|
||||
`SELECT id, title, instructions, completed, comments_json,
|
||||
analyzed_at, revision, created_at, updated_at
|
||||
FROM magic_todos
|
||||
WHERE source = 'manual'
|
||||
ORDER BY created_at ASC, rowid ASC`
|
||||
)
|
||||
.all() as Array<{
|
||||
id: string
|
||||
title: string
|
||||
instructions: string
|
||||
completed: number
|
||||
comments_json: string
|
||||
analyzed_at: string | null
|
||||
revision: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}>
|
||||
if (manualTodos.length > 0) {
|
||||
const noteId = randomUUID()
|
||||
const createdAt = manualTodos[0]!.created_at
|
||||
const updatedAt = manualTodos.at(-1)!.updated_at
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO magic_notes
|
||||
(id, project_id, title, pinned, revision,
|
||||
created_at, updated_at)
|
||||
VALUES (?, NULL, '迁入的待办', 0, 0, ?, ?)`
|
||||
)
|
||||
.run(noteId, createdAt, updatedAt)
|
||||
const insertEntry = database.prepare(
|
||||
`INSERT INTO magic_note_entries
|
||||
(id, note_id, content_json, plain_text, comments_json,
|
||||
actions_json, analyzed_at, revision, created_at, updated_at,
|
||||
image_bytes)
|
||||
VALUES (?, ?, ?, ?, ?, '[]', ?, ?, ?, ?, 0)`
|
||||
)
|
||||
const updateMigratedTodo = database.prepare(
|
||||
`UPDATE magic_todos
|
||||
SET instructions = ?, comments_json = ?, analyzed_at = ?,
|
||||
revision = ?
|
||||
WHERE entry_id = ? AND source = 'note'`
|
||||
)
|
||||
const deleteManualTodo = database.prepare(
|
||||
`DELETE FROM magic_todos
|
||||
WHERE id = ? AND source = 'manual'`
|
||||
)
|
||||
for (const todo of manualTodos) {
|
||||
const entryId = randomUUID()
|
||||
const content: MagicNoteRichContent = {
|
||||
version: 1,
|
||||
ops: [
|
||||
{ insert: todo.title },
|
||||
{
|
||||
insert: '\n',
|
||||
attributes: {
|
||||
list: todo.completed ? 'checked' : 'unchecked'
|
||||
}
|
||||
},
|
||||
...(todo.instructions
|
||||
? [
|
||||
{ insert: todo.instructions },
|
||||
{ insert: '\n' }
|
||||
]
|
||||
: [])
|
||||
]
|
||||
}
|
||||
insertEntry.run(
|
||||
entryId,
|
||||
noteId,
|
||||
JSON.stringify(content),
|
||||
[todo.title, todo.instructions].filter(Boolean).join('\n'),
|
||||
todo.comments_json,
|
||||
todo.analyzed_at,
|
||||
todo.revision,
|
||||
todo.created_at,
|
||||
todo.updated_at
|
||||
)
|
||||
this.syncMagicNoteTodos(
|
||||
database,
|
||||
noteId,
|
||||
entryId,
|
||||
content,
|
||||
todo.updated_at
|
||||
)
|
||||
updateMigratedTodo.run(
|
||||
todo.instructions,
|
||||
todo.comments_json,
|
||||
todo.analyzed_at,
|
||||
todo.revision,
|
||||
entryId
|
||||
)
|
||||
deleteManualTodo.run(todo.id)
|
||||
}
|
||||
}
|
||||
database.exec('PRAGMA user_version = 17')
|
||||
database.exec('COMMIT')
|
||||
} catch (error) {
|
||||
database.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private requireDatabase(): DatabaseSync {
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('AssistantDatabase heartbeat persistence', () => {
|
||||
).count
|
||||
check.close()
|
||||
migrated.close()
|
||||
expect(version).toBe(16)
|
||||
expect(version).toBe(17)
|
||||
expect(heartbeatTableCount).toBe(3)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user