feat: make magic todo status editable
This commit is contained in:
@@ -1932,6 +1932,50 @@ describe('AssistantDatabase', () => {
|
||||
database.close()
|
||||
})
|
||||
|
||||
it('updates a derived todo and its source checklist together', async () => {
|
||||
const database = await createDatabase()
|
||||
const note = database.createMagicNote({ title: '发布笔记' })
|
||||
database.createMagicNoteEntry({
|
||||
noteId: note.id,
|
||||
content: {
|
||||
version: 1,
|
||||
ops: [
|
||||
{ insert: '核对发布材料' },
|
||||
{ insert: '\n', attributes: { list: 'unchecked' } }
|
||||
]
|
||||
},
|
||||
plainText: '核对发布材料'
|
||||
})
|
||||
const todo = database.listMagicTodos()[0]!
|
||||
|
||||
const updated = database.updateMagicTodo({
|
||||
todoId: todo.id,
|
||||
completed: true,
|
||||
expectedRevision: todo.revision
|
||||
})
|
||||
|
||||
expect(updated).toMatchObject({
|
||||
id: todo.id,
|
||||
completed: true,
|
||||
revision: todo.revision + 1
|
||||
})
|
||||
expect(
|
||||
database.getMagicNote(note.id).entries[0]!.content.ops
|
||||
).toEqual([
|
||||
{ insert: '核对发布材料' },
|
||||
{ insert: '\n', attributes: { list: 'checked' } }
|
||||
])
|
||||
expect(() =>
|
||||
database.updateMagicTodo({
|
||||
todoId: todo.id,
|
||||
completed: false,
|
||||
expectedRevision: todo.revision
|
||||
})
|
||||
).toThrow('待办已被更新,请刷新后重试')
|
||||
|
||||
database.close()
|
||||
})
|
||||
|
||||
it('protects magic note records from stale revisions', async () => {
|
||||
const database = await createDatabase()
|
||||
const note = database.createMagicNote({ title: '并发笔记' })
|
||||
|
||||
@@ -51,13 +51,16 @@ import {
|
||||
type MagicNoteRichContent,
|
||||
type MagicNoteSearchResult,
|
||||
type MagicNoteSummary,
|
||||
type MagicTodoItem
|
||||
type MagicTodoItem,
|
||||
type MagicTodoUpdateInput
|
||||
} from '../../shared/magic-notes-contracts'
|
||||
import type { ComputerControlAuditEvent } from '../computer-control/audit'
|
||||
import {
|
||||
magicNoteChecklistItems,
|
||||
magicNoteImageBytes,
|
||||
magicNotePreview
|
||||
magicNotePlainText,
|
||||
magicNotePreview,
|
||||
setMagicNoteChecklistCompletion
|
||||
} from '../magic-notes/rich-content'
|
||||
import { computeNextHeartbeatRun } from './heartbeat-recurrence'
|
||||
|
||||
@@ -2164,6 +2167,86 @@ export class AssistantDatabase {
|
||||
return toMagicTodo(row)
|
||||
}
|
||||
|
||||
updateMagicTodo(input: MagicTodoUpdateInput): MagicTodoItem {
|
||||
const database = this.requireDatabase()
|
||||
const now = new Date().toISOString()
|
||||
database.exec('BEGIN IMMEDIATE')
|
||||
try {
|
||||
const existing = database
|
||||
.prepare(
|
||||
`SELECT t.note_id, t.entry_id, t.source_index, t.completed,
|
||||
t.revision AS todo_revision,
|
||||
e.content_json, e.revision AS entry_revision
|
||||
FROM magic_todos t
|
||||
INNER JOIN magic_note_entries e ON e.id = t.entry_id
|
||||
WHERE t.id = ? AND t.source = 'note'`
|
||||
)
|
||||
.get(input.todoId) as
|
||||
| {
|
||||
note_id: string
|
||||
entry_id: string
|
||||
source_index: number
|
||||
completed: number
|
||||
todo_revision: number
|
||||
content_json: string
|
||||
entry_revision: number
|
||||
}
|
||||
| undefined
|
||||
if (!existing) {
|
||||
throw new Error('待办不存在')
|
||||
}
|
||||
if (existing.todo_revision !== input.expectedRevision) {
|
||||
throw new Error('待办已被更新,请刷新后重试')
|
||||
}
|
||||
if (Boolean(existing.completed) === input.completed) {
|
||||
database.exec('COMMIT')
|
||||
return this.getMagicTodo(input.todoId)
|
||||
}
|
||||
|
||||
const content = setMagicNoteChecklistCompletion(
|
||||
JSON.parse(existing.content_json) as MagicNoteRichContent,
|
||||
existing.source_index,
|
||||
input.completed
|
||||
)
|
||||
const result = database
|
||||
.prepare(
|
||||
`UPDATE magic_note_entries
|
||||
SET content_json = ?, plain_text = ?, comments_json = '[]',
|
||||
analyzed_at = NULL, revision = revision + 1, updated_at = ?
|
||||
WHERE id = ? AND revision = ?`
|
||||
)
|
||||
.run(
|
||||
JSON.stringify(content),
|
||||
magicNotePlainText(content),
|
||||
now,
|
||||
existing.entry_id,
|
||||
existing.entry_revision
|
||||
)
|
||||
if (result.changes !== 1) {
|
||||
throw new Error('记录已被更新,请刷新后重试')
|
||||
}
|
||||
database
|
||||
.prepare(
|
||||
`UPDATE magic_notes
|
||||
SET revision = revision + 1, updated_at = ?
|
||||
WHERE id = ?`
|
||||
)
|
||||
.run(now, existing.note_id)
|
||||
this.syncMagicNoteTodos(
|
||||
database,
|
||||
existing.note_id,
|
||||
existing.entry_id,
|
||||
content,
|
||||
now
|
||||
)
|
||||
database.exec('COMMIT')
|
||||
} catch (error) {
|
||||
database.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
return this.getMagicTodo(input.todoId)
|
||||
}
|
||||
|
||||
saveMagicTodoAnalysis(input: {
|
||||
todoId: string
|
||||
expectedRevision: number
|
||||
|
||||
+12
-1
@@ -86,7 +86,8 @@ import {
|
||||
magicNoteEntryDeleteSchema,
|
||||
magicNoteEntryUpdateSchema,
|
||||
magicNoteUpdateSchema,
|
||||
magicTodoIdSchema
|
||||
magicTodoIdSchema,
|
||||
magicTodoUpdateSchema
|
||||
} from '../shared/magic-notes-contracts'
|
||||
import {
|
||||
assistantIdSchema,
|
||||
@@ -3495,6 +3496,16 @@ export function registerIpcHandlers(
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
ipcChannels.magicTodosUpdate,
|
||||
(event, input: unknown) => {
|
||||
assertTrustedSender(event, window)
|
||||
return assistantDatabase.updateMagicTodo(
|
||||
magicTodoUpdateSchema.parse(input)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
ipcChannels.magicTodosAnalyze,
|
||||
async (event, input: unknown) => {
|
||||
|
||||
Reference in New Issue
Block a user