From 4b4379fc1d6db60c8c40cd36327d426bc3f7dd4e Mon Sep 17 00:00:00 2001 From: Eason <100257303+Yj-30422@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:21:37 +0800 Subject: [PATCH] fix: support legacy plugin settings slots --- package.json | 2 +- ...lient-ui-settings-plugins+0.1.0-rc.7.patch | 16 ++ ...ek-ai+dsh-client-ui-slots+0.1.0-rc.7.patch | 20 +++ scripts/install-plugin-compatibility.mjs | 39 +++++ test/plugin-slot-compatibility.test.ts | 137 ++++++++++++++++++ 5 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 patches/@deepseek-ai+dsh-client-ui-settings-plugins+0.1.0-rc.7.patch create mode 100644 patches/@deepseek-ai+dsh-client-ui-slots+0.1.0-rc.7.patch create mode 100644 scripts/install-plugin-compatibility.mjs create mode 100644 test/plugin-slot-compatibility.test.ts diff --git a/package.json b/package.json index 07c2c546..340385d9 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "agent" ], "scripts": { - "postinstall": "patch-package && node scripts/install-brand-assets.mjs && install-electron --no", + "postinstall": "patch-package && node scripts/install-plugin-compatibility.mjs && node scripts/install-brand-assets.mjs && install-electron --no", "dev": "electron-vite dev", "build": "electron-vite build", "icons:generate": "node scripts/generate-app-icons.mjs", diff --git a/patches/@deepseek-ai+dsh-client-ui-settings-plugins+0.1.0-rc.7.patch b/patches/@deepseek-ai+dsh-client-ui-settings-plugins+0.1.0-rc.7.patch new file mode 100644 index 00000000..34fe9251 --- /dev/null +++ b/patches/@deepseek-ai+dsh-client-ui-settings-plugins+0.1.0-rc.7.patch @@ -0,0 +1,16 @@ +diff --git a/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/lib/client.js +index aa98760..6967420 100644 +--- a/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/lib/client.js ++++ b/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/lib/client.js +@@ -982,7 +982,10 @@ window.__ModuleLoader__.load({ + } + publish() { + const served = new Set(this.served); +- const namespaces = this.entries().flatMap((entry) => entry.options.key !== void 0 && served.has(entry.options.key) ? [entry.options.key] : []); ++ // rc.6 list registrations gain key=id in the shell compatibility shim. ++ // Keep those self-contained cards visible; modern key-only cards still ++ // require a matching Host settings namespace. ++ const namespaces = this.entries().flatMap((entry) => entry.options.key !== void 0 && (entry.options.id === entry.options.key || served.has(entry.options.key)) ? [entry.options.key] : []); + const previous = this.store.getSnapshot(); + if (previous.loaded === this.loaded && previous.namespaces.length === namespaces.length && previous.namespaces.every((ns, index) => ns === namespaces[index])) return; + this.store.set({ diff --git a/patches/@deepseek-ai+dsh-client-ui-slots+0.1.0-rc.7.patch b/patches/@deepseek-ai+dsh-client-ui-slots+0.1.0-rc.7.patch new file mode 100644 index 00000000..04f2211e --- /dev/null +++ b/patches/@deepseek-ai+dsh-client-ui-slots+0.1.0-rc.7.patch @@ -0,0 +1,20 @@ +diff --git a/node_modules/@deepseek-ai/dsh-client-ui-slots/lib/index.js b/node_modules/@deepseek-ai/dsh-client-ui-slots/lib/index.js +index c653bea..f8e6358 100644 +--- a/node_modules/@deepseek-ai/dsh-client-ui-slots/lib/index.js ++++ b/node_modules/@deepseek-ai/dsh-client-ui-slots/lib/index.js +@@ -65,6 +65,15 @@ var SlotCore = class { + const rec = this.records.get(options.name); + if (!rec?.spec) throw new Error(`slot "${options.name}" is not declared (a parent entry's children table must declare it)`); + const spec = rec.spec; ++ // rc.6 exposed the plugin settings surface as a list slot, so existing ++ // third-party bundles register an id. rc.7 made this one slot keyed; ++ // preserve the old identity while leaving explicit keys authoritative. ++ if (options.name === "settings.plugin.item" && spec.kind === "keyed" && options.key === void 0 && options.id !== void 0) { ++ options = { ++ ...options, ++ key: options.id ++ }; ++ } + const priority = options.priority ?? 0; + const occupantHint = (occupant) => `at priority ${priority}${occupant.registrant !== void 0 ? ` (registered by ${occupant.registrant})` : ""} — register at a different priority to shadow it (lowest renders)`; + switch (spec.kind) { diff --git a/scripts/install-plugin-compatibility.mjs b/scripts/install-plugin-compatibility.mjs new file mode 100644 index 00000000..3365a533 --- /dev/null +++ b/scripts/install-plugin-compatibility.mjs @@ -0,0 +1,39 @@ +import { readFile, writeFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const frontendDirectory = path.join( + projectRoot, + 'node_modules', + '@deepseek-ai', + 'dsh-web-frontend', + 'dist' +) +const indexPath = path.join(frontendDirectory, 'index.html') + +const index = await readFile(indexPath, 'utf8') +const assetUrl = index.match(/src="\/(assets\/index-[^"]+\.js)"/)?.[1] +if (assetUrl === undefined) { + throw new Error('Could not install plugin compatibility: DSH frontend asset was not found') +} + +const assetPath = path.join(frontendDirectory, assetUrl) +const original = 'const u=s.spec,c=r.priority??0' +const compatible = + 'const u=s.spec;r.name==="settings.plugin.item"&&u.kind==="keyed"&&r.key===void 0&&r.id!==void 0&&(r={...r,key:r.id});const c=r.priority??0' +const asset = await readFile(assetPath, 'utf8') + +if (!asset.includes(compatible)) { + const occurrences = asset.split(original).length - 1 + if (occurrences !== 1) { + throw new Error( + `Could not install plugin compatibility in ${path.relative(projectRoot, assetPath)}: expected one SlotCore registration site, found ${occurrences}` + ) + } + await writeFile(assetPath, asset.replace(original, compatible)) +} + +console.log( + `Installed legacy plugin compatibility: ${path.relative(projectRoot, assetPath)}` +) diff --git a/test/plugin-slot-compatibility.test.ts b/test/plugin-slot-compatibility.test.ts new file mode 100644 index 00000000..06f87caa --- /dev/null +++ b/test/plugin-slot-compatibility.test.ts @@ -0,0 +1,137 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots' +import { describe, expect, it } from 'vitest' + +const projectRoot = path.resolve(import.meta.dirname, '..') + +type RuntimeRegister = ( + options: Record, + component: () => null +) => () => void + +function keyedSlotCore(name = 'settings.plugin.item'): { + core: SlotCore + register: RuntimeRegister +} { + const core = new SlotCore() + const register = core.register.bind(core) as unknown as RuntimeRegister + register( + { + name: 'root', + children: { [name]: { kind: 'keyed', scope: 'root' } } + }, + () => null + ) + return { core, register } +} + +describe('legacy plugin settings slot compatibility', () => { + it('uses an rc.6 list id as the rc.7 key and preserves disposal', () => { + const { core, register } = keyedSlotCore() + + const dispose = register( + { name: 'settings.plugin.item', id: 'github', order: 30 }, + () => null + ) + + expect(core.entriesOfSlot('settings.plugin.item')).toHaveLength(1) + expect(core.entriesOfSlot('settings.plugin.item')[0]?.options).toMatchObject({ + id: 'github', + key: 'github', + order: 30 + }) + + dispose() + expect(core.entriesOfSlot('settings.plugin.item')).toHaveLength(0) + }) + + it('keeps explicit rc.7 keys authoritative and rejects collisions', () => { + const { core, register } = keyedSlotCore() + + register( + { name: 'settings.plugin.item', id: 'legacy-id', key: 'explicit-key' }, + () => null + ) + + expect(core.entriesOfSlot('settings.plugin.item')[0]?.options.key).toBe( + 'explicit-key' + ) + expect(() => + register({ name: 'settings.plugin.item', key: 'explicit-key' }, () => null) + ).toThrow('already has an entry for key "explicit-key"') + }) + + it('does not relax keyed registration for unrelated slots', () => { + const { register } = keyedSlotCore('unrelated.keyed') + + expect(() => + register({ name: 'unrelated.keyed', id: 'legacy-id' }, () => null) + ).toThrow('keyed slot "unrelated.keyed" requires options.key') + }) + + it('ships the matching legacy dispatch patch for the settings controller', async () => { + const settingsPatch = await readFile( + path.join( + projectRoot, + 'patches', + '@deepseek-ai+dsh-client-ui-settings-plugins+0.1.0-rc.7.patch' + ), + 'utf8' + ) + + expect(settingsPatch).toContain( + 'entry.options.id === entry.options.key || served.has(entry.options.key)' + ) + }) + + it('patches the SlotCore bundle actually served by the desktop app', async () => { + const indexHtml = await readFile( + path.join( + projectRoot, + 'node_modules', + '@deepseek-ai', + 'dsh-web-frontend', + 'dist', + 'index.html' + ), + 'utf8' + ) + const asset = indexHtml.match(/src="\/(assets\/index-[^"]+\.js)"/)?.[1] + expect(asset).toBeDefined() + + const compatibilityInstaller = await readFile( + path.join( + projectRoot, + 'scripts', + 'install-plugin-compatibility.mjs' + ), + 'utf8' + ) + const packageJson = JSON.parse( + await readFile(path.join(projectRoot, 'package.json'), 'utf8') + ) as { scripts: { postinstall: string } } + + expect(compatibilityInstaller).toContain( + 'r.name==="settings.plugin.item"&&u.kind==="keyed"&&r.key===void 0&&r.id!==void 0&&(r={...r,key:r.id})' + ) + expect(packageJson.scripts.postinstall).toContain( + 'node scripts/install-plugin-compatibility.mjs' + ) + + const servedAsset = await readFile( + path.join( + projectRoot, + 'node_modules', + '@deepseek-ai', + 'dsh-web-frontend', + 'dist', + asset ?? '' + ), + 'utf8' + ) + expect(servedAsset).toContain( + 'r.name==="settings.plugin.item"&&u.kind==="keyed"&&r.key===void 0&&r.id!==void 0&&(r={...r,key:r.id})' + ) + }) +})