diff --git a/build/run-packaged-deepseek-harness-smoke.cjs b/build/run-packaged-deepseek-harness-smoke.cjs index 4918bd7d..663d60f1 100644 --- a/build/run-packaged-deepseek-harness-smoke.cjs +++ b/build/run-packaged-deepseek-harness-smoke.cjs @@ -148,7 +148,7 @@ async function main() { [ 'appId: live.digiman.goodbuddy.harness-smoke', 'productName: GoodBuddyHarnessSmoke', - 'electronVersion: "43.2.0"', + `electronVersion: "${require('electron/package.json').version}"`, 'asar: true', 'npmRebuild: false', 'files:', diff --git a/docs/development/loongarch-preview-build.md b/docs/development/loongarch-preview-build.md index b30438dc..2fc2efa4 100644 --- a/docs/development/loongarch-preview-build.md +++ b/docs/development/loongarch-preview-build.md @@ -10,7 +10,7 @@ Debian `loong64` 桌面,不代表 GoodBuddy 已将龙芯纳入正式发布支 均为 LoongArch ELF。 预览版使用社区维护的 Electron 42.3.0 LoongArch 构建。GoodBuddy 正式版本 -当前使用 Electron 43.2.0,因此该预览包只用于移植和真机验证,不能作为正式 +当前使用 Electron 44.5.1,因此该预览包只用于移植和真机验证,不能作为正式 发布基线。Electron 官方目前也不提供 Linux LoongArch 预编译包。 已完成的实际构建结果: diff --git a/package-lock.json b/package-lock.json index ebf4cef2..abc29a83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,7 +84,7 @@ "@xterm/addon-search": "0.16.0", "@xterm/addon-webgl": "0.19.0", "@xterm/xterm": "6.0.0", - "electron": "^43.2.0", + "electron": "^44.5.1", "electron-builder": "^26.15.3", "electron-vite": "^5.0.0", "esbuild": "0.25.12", @@ -14755,9 +14755,9 @@ } }, "node_modules/electron": { - "version": "43.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-43.2.0.tgz", - "integrity": "sha512-80zvrgG7ZRXD+tD0IyLvrnN9n+veSxadMRsMaC9wKKP3iUbtC7rGM8+dVuCmOb0Rrwwv8ESW4awnUZh9Hbp1fA==", + "version": "44.5.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-44.5.1.tgz", + "integrity": "sha512-lx7AYoFIiyvEvgvpyWO9kOC60xtNPpiPZAth6Fmg9NutzpCAMLwpUlaBY2EXfZyIgUdzEmLMj9KvK0Vrokl1yQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5f75897e..08c590dd 100644 --- a/package.json +++ b/package.json @@ -385,7 +385,7 @@ "@xterm/addon-search": "0.16.0", "@xterm/addon-webgl": "0.19.0", "@xterm/xterm": "6.0.0", - "electron": "^43.2.0", + "electron": "^44.5.1", "electron-builder": "^26.15.3", "electron-vite": "^5.0.0", "esbuild": "0.25.12", diff --git a/src/main/context-manager.test.ts b/src/main/context-manager.test.ts index 2b71c5af..2b15d47a 100644 --- a/src/main/context-manager.test.ts +++ b/src/main/context-manager.test.ts @@ -4,13 +4,16 @@ import { basename, join } from 'node:path' import { strToU8, zipSync } from 'fflate' import { afterEach, describe, expect, it, vi } from 'vitest' -const { createFromBuffer, getSources, showOpenDialog } = vi.hoisted(() => ({ +const { createFromBuffer, getSources, showOpenDialog, readText, readClipboard } = vi.hoisted(() => ({ + readText: vi.fn(), + readClipboard: vi.fn(), createFromBuffer: vi.fn(), getSources: vi.fn(), showOpenDialog: vi.fn() })) vi.mock('electron', () => ({ + clipboard: { readText, read: readClipboard }, desktopCapturer: { getSources }, @@ -31,6 +34,8 @@ import { defaultDocumentParsingSettings } from './document-parsing-settings-stor const temporaryDirectories: string[] = [] afterEach(async () => { + readText.mockReset() + readClipboard.mockReset() getSources.mockReset() showOpenDialog.mockReset() createFromBuffer.mockReset() @@ -42,6 +47,40 @@ afterEach(async () => { }) describe('ContextManager', () => { + it('awaits clipboard text and prefers it over images', async () => { + readText.mockResolvedValue(' clipboard text ') + const manager = new ContextManager() + const attachment = await manager.readClipboard() + expect(attachment.kind).toBe('text') + expect(attachment.preview).toContain('clipboard text') + expect(readClipboard).not.toHaveBeenCalled() + }) + + it('decodes an asynchronous clipboard PNG when text is empty', async () => { + readText.mockResolvedValue('') + const data = Buffer.from([0x89, 0x50, 0x4e, 0x47]) + const getType = vi.fn().mockResolvedValue(new Blob([data])) + readClipboard.mockResolvedValue([{ types: ['image/png'], getType }]) + const image = { + isEmpty: () => false, + getSize: () => ({ width: 640, height: 480 }), + resize: vi.fn(), + toJPEG: () => Buffer.from([0xff, 0xd8, 0xff, 0xd9]) + } + image.resize.mockReturnValue(image) + createFromBuffer.mockReturnValue(image) + const attachment = await new ContextManager().readClipboard() + expect(getType).toHaveBeenCalledWith('image/png') + expect(createFromBuffer).toHaveBeenCalledWith(data) + expect(attachment.kind).toBe('image') + }) + + it('rejects a clipboard without text or a supported image', async () => { + readText.mockResolvedValue('') + readClipboard.mockResolvedValue([{ types: ['text/html'] }]) + await expect(new ContextManager().readClipboard()).rejects.toThrow('剪贴板中没有可用的文本或图片') + }) + it('preserves logical attachment identity and frozen message bytes across a draft reparse', async () => { const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-reparse-')) temporaryDirectories.push(directory) diff --git a/src/main/context-manager.ts b/src/main/context-manager.ts index f469b850..04b0e1c0 100644 --- a/src/main/context-manager.ts +++ b/src/main/context-manager.ts @@ -568,14 +568,18 @@ export class ContextManager { ) } - readClipboard(): ContextAttachment { - const text = clipboard.readText().trim() + async readClipboard(): Promise { + const text = (await clipboard.readText()).trim() if (text) { return this.storeText('剪贴板文本.txt', text) } - const image = clipboard.readImage() - if (!image.isEmpty()) { - return this.storeImage('剪贴板图片.png', image) + for (const item of await clipboard.read()) { + if (!item.types.includes('image/png')) continue + const blob = await item.getType('image/png') + const image = nativeImage.createFromBuffer(Buffer.from(await blob.arrayBuffer())) + if (!image.isEmpty()) { + return this.storeImage('剪贴板图片.png', image) + } } throw new Error('剪贴板中没有可用的文本或图片') } diff --git a/src/main/ipc.test.ts b/src/main/ipc.test.ts index 520d8909..97f47a95 100644 --- a/src/main/ipc.test.ts +++ b/src/main/ipc.test.ts @@ -264,9 +264,9 @@ describe('terminal IPC boundary', () => { snapshot.sessionId ) - expect( + await expect( electronMocks.handlers.get(ipcChannels.clipboardReadText)?.(event) - ).toBe('Get-Location\r') + ).resolves.toBe('Get-Location\r') expect(electronMocks.readClipboardText).toHaveBeenCalledOnce() electronMocks.handlers.get(ipcChannels.clipboardWriteText)?.( @@ -353,8 +353,8 @@ const electronMocks = vi.hoisted(() => { openPath: vi.fn(async () => ''), showItemInFolder: vi.fn(), openExternal: vi.fn(async () => undefined), - readClipboardText: vi.fn(() => 'Get-Location\r'), - writeClipboardText: vi.fn() + readClipboardText: vi.fn(async () => 'Get-Location\r'), + writeClipboardText: vi.fn(async () => {}) } }) diff --git a/src/main/window-ipc.ts b/src/main/window-ipc.ts index d9044671..9dc39ae5 100644 --- a/src/main/window-ipc.ts +++ b/src/main/window-ipc.ts @@ -10,12 +10,12 @@ export function registerClipboardIpcHandlers( ): void { registerHandler(ipcChannels.clipboardReadText, (event) => { assertTrustedSender(event, window) - return clipboardTextSchema.parse(clipboard.readText()) + return clipboard.readText().then((text) => clipboardTextSchema.parse(text)) }) registerHandler(ipcChannels.clipboardWriteText, (event, input) => { assertTrustedSender(event, window) - clipboard.writeText(clipboardTextSchema.parse(input)) + return clipboard.writeText(clipboardTextSchema.parse(input)) }) }