feat: configure canvas pages sent for note analysis

Canvas analysis previously sent every page. Add a Magic Notes setting for
1-8 canvas pages, defaulting to 1 for new and historical settings. Select
the first N pages in current canvas order before capture and use the same
text/image range for saved entries, drafts and canvas-source todos.

Keep full 50-page saving/export, expose the same setting through config
tools, and use the shared settings select with an inline help icon.
Include pagination, persistence, production IPC and isolated Electron
regressions, including text-only input and scaled page boundaries.

Validation: focused feature tests and isolated Electron regressions passed;
the final compact UI passed 14 tests. Typecheck and lint were reported
passing by the coordinating agent. No full-suite pass is claimed because
the completed full test command exposed no summary. No live model calls
were made for this commit preparation.

Release note: 魔法笔记新增“发送画布页数”设置,可选 1~8 页,默认 1 页,按当前画布顺序发送前 N 页的文字和图片;标题旁问号可查看说明,保存和导出完整画布不受影响。
This commit is contained in:
mesalogo
2026-09-20 00:07:07 +08:00
parent ecf09f4e95
commit bf1d34e897
39 changed files with 475 additions and 102 deletions
@@ -84,7 +84,7 @@ describe('magic note analyzer', () => {
const { runtime, requests } = recordingRuntime()
const comments = await analyzeMagicTodo(
runtime, canvasTodo, options, undefined, undefined,
{ supportsImageInput, content: canvasEntry.content }
{ supportsImageInput, content: canvasEntry.content, canvasPageCount: 2 }
)
expect(requests).toHaveLength(1)
expect(requests[0]?.prompt).toContain(canvasTodo.title)
@@ -127,7 +127,7 @@ describe('magic note analyzer', () => {
const usage: RuntimeModelUsageEvent[] = []
const comments = await analyzeMagicNoteEntry(
runtime, canvasEntry, options, undefined,
(event) => usage.push(event), { supportsImageInput: true }
(event) => usage.push(event), { supportsImageInput: true, canvasPageCount: 2 }
)
expect(requests).toHaveLength(1)
expect(requests[0]?.images).toEqual([
@@ -149,34 +149,59 @@ describe('magic note analyzer', () => {
pages: canvasEntry.content.pages.map((page) => ({ ...page, objects: [] }))
}
const comments = await analyzeMagicNoteDraft(runtime, '', options, undefined, undefined, { supportsImageInput: true, content })
expect(requests[0]?.images).toHaveLength(2)
expect(requests[0]?.images).toHaveLength(1)
expect(comments[0]?.inputMode).toBe('canvas-images')
})
it('falls back to all extracted text for a text model, without silently slicing later pages', async () => {
it('limits both continuous flow and objects to the first page for a text model', async () => {
const { runtime, requests } = recordingRuntime()
const content = {
...canvasEntry.content,
pages: canvasEntry.content.pages.map((page) => ({
...page, objects: [{ text: '最后一页的结论' }]
pages: canvasEntry.content.pages.map((page, index) => ({
...page, objects: [{ text: index === 0 ? '首页结论' : '最后一页的结论' }]
})),
flow: { ops: [{ insert: '文'.repeat(30_001) }] }
flow: { version: 1 as const, ops: [{ insert: '首页正文\n末页正文\n' }] }
}
const comments = await analyzeMagicNoteEntry(runtime, { ...canvasEntry, content }, options, undefined, undefined, { supportsImageInput: false })
const comments = await analyzeMagicNoteEntry(runtime, { ...canvasEntry, content }, {
...options, canvasPageText: [{ pageId: 'first', text: '首页正文\n' }, { pageId: 'second', text: '末页正文\n' }]
}, undefined, undefined, { supportsImageInput: false })
expect(requests[0]?.images).toBeUndefined()
expect(requests[0]?.prompt).toContain('最后一页的结论')
expect(requests[0]?.prompt).toContain('首页正文')
expect(requests[0]?.prompt).toContain('首页结论')
expect(requests[0]?.prompt).not.toContain('最后一页的结论')
expect(requests[0]?.prompt).not.toContain('末页正文')
expect(requests[0]?.prompt).toContain('未查看页面图像')
expect(comments[0]?.inputMode).toBe('text-fallback')
})
it('rejects incomplete flow page text without sending the full cross-page body', async () => {
const { runtime, requests } = recordingRuntime()
const content = { ...canvasEntry.content, flow: { version: 1 as const, ops: [{ insert: 'First and later pages\n' }] } }
for (const canvasPageText of [undefined, [], [{ pageId: 'second', text: 'Later' }],
[{ pageId: 'first', text: 'A' }, { pageId: 'first', text: 'B' }]]) {
await expect(analyzeMagicNoteDraft(runtime, 'Full text', { ...options, canvasPageText }, undefined, undefined,
{ supportsImageInput: false, content })).rejects.toThrow('分页文字缺失')
}
expect(requests).toHaveLength(0)
})
it('does not use later-page text when the selected first page is visual-only', async () => {
const { runtime, requests } = recordingRuntime()
const content = { ...canvasEntry.content, pages: canvasEntry.content.pages.map((page, index) => ({ ...page,
objects: index === 0 ? [] : [{ text: 'Later-page text' }] })) }
await expect(analyzeMagicNoteDraft(runtime, 'Later-page text', options, undefined, undefined,
{ supportsImageInput: false, content })).rejects.toThrow('所选画布页中没有')
expect(requests).toHaveLength(0)
})
it('rejects visual-only text fallback before issuing a request', async () => {
const { runtime, requests } = recordingRuntime()
const content = {
...canvasEntry.content,
pages: canvasEntry.content.pages.map((page) => ({ ...page, objects: [] })),
flow: { ops: [{ insert: { image: 'placeholder' } }] }
flow: { version: 1 as const, ops: [{ insert: { image: 'placeholder' } }] }
}
await expect(analyzeMagicNoteEntry(runtime, { ...canvasEntry, content, plainText: ' ' }, options, undefined, undefined, { supportsImageInput: false })).rejects.toThrow('请切换支持图像输入的默认模型')
await expect(analyzeMagicNoteEntry(runtime, { ...canvasEntry, content, plainText: ' ' }, options, undefined, undefined, { supportsImageInput: false, canvasPageCount: 2 })).rejects.toThrow('请切换支持图像输入的默认模型')
expect(requests).toHaveLength(0)
})
@@ -190,7 +215,7 @@ describe('magic note analyzer', () => {
const { runtime, requests } = recordingRuntime(error)
await expect(analyzeMagicNoteEntry(runtime, canvasEntry, options, undefined, undefined, { supportsImageInput: true })).rejects.toThrow(error)
expect(requests).toHaveLength(1)
expect(requests[0]?.images).toHaveLength(2)
expect(requests[0]?.images).toHaveLength(1)
})
it('keeps ordinary notes text-only even when the model supports images', async () => {
@@ -214,7 +239,7 @@ describe('magic note analyzer', () => {
}))
}
await analyzeMagicNoteDraft(runtime, '', options, undefined, undefined, {
supportsImageInput: false, content
supportsImageInput: false, content, canvasPageCount: 2
})
expect(requests[0]?.prompt).toContain('第 2 页:\\nPDF 原生文字 2')
expect(requests[0]?.images).toBeUndefined()