From 7b743e51130a8fb8a19e777f49c0899f5d7c97ba Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Mon, 5 Oct 2026 02:02:12 +0800 Subject: [PATCH 1/2] fix: retain plugin archives and start independent fresh sessions --- apps/desktop/src/local-runtime.ts | 6 ++-- apps/desktop/src/plugin-archive-cache.ts | 36 +++++++++++++++++++ apps/desktop/tests/local-runtime.spec.ts | 2 +- .../tests/plugin-archive-cache.spec.ts | 32 +++++++++++++++++ apps/web/pnpm-lock.yaml | 3 ++ .../probe-expert-conversation-entry.mjs | 9 +++++ packages/bundle/package.json | 7 ++-- packages/bundle/src/client/harness/client.ts | 2 ++ .../src/client/harness/fresh-session.ts | 28 +++++++++++++++ 9 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/src/plugin-archive-cache.ts create mode 100644 apps/desktop/tests/plugin-archive-cache.spec.ts create mode 100644 packages/bundle/src/client/harness/fresh-session.ts diff --git a/apps/desktop/src/local-runtime.ts b/apps/desktop/src/local-runtime.ts index d00b618d31..ded61771c9 100644 --- a/apps/desktop/src/local-runtime.ts +++ b/apps/desktop/src/local-runtime.ts @@ -5,6 +5,7 @@ import { dirname, join } from 'node:path' import { pathToFileURL } from 'node:url' import { load, dump } from 'js-yaml' import { syncBundledCompatibility } from './runtime-compatibility.ts' +import { cachedPackageManager } from './plugin-archive-cache.ts' import { enterpriseNamespace } from './connection-mode.ts' import type { EnterpriseMember, Authority } from './enterprise-auth.ts' @@ -142,8 +143,8 @@ export function enterpriseEnvironment(root: string, executable: string, parent: export function officialLauncher(source: string, home: string, executable: string, packageManager: string = join(dirname(executable), '..', '..', 'pnpm', 'bin', 'pnpm.cjs')): string { const cli = join(source, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js') - const pnpm = packageManager - if (!existsSync(cli) || !existsSync(pnpm)) throw new Error('安装包缺少官方 DSH 或锁定的插件安装工具') + if (!existsSync(cli) || !existsSync(packageManager)) throw new Error('安装包缺少官方 DSH 或锁定的插件安装工具') + const pnpm = cachedPackageManager(home, packageManager) const launcher = join(home, '.workdsh-launch.mjs') writeFileSync(launcher, `import { runCli } from ${JSON.stringify(pathToFileURL(cli).href)};\nawait runCli({ manageDesktopProfile: true, packageManager: { command: ${JSON.stringify(executable)}, args: [${JSON.stringify(pnpm)}] } });\n`, { mode: 0o600 }) return launcher @@ -157,6 +158,7 @@ export function officialHostLauncher(source: string, home: string, executable: s const anchor = join(source, 'profile-installation.json'), profile = join(home, 'profiles', 'workdsh') if (!existsSync(anchor)) throw new Error('安装包缺少完整官方与 WorkDSH 安装清单') const launcher = join(home, '.workdsh-host.mjs') + packageManager = cachedPackageManager(home, packageManager) writeFileSync(launcher, `import { runProfile } from ${JSON.stringify(pathToFileURL(boot).href)};\nimport { loadProfileDirectory, loadLayeredEnv, reportSkippedBundles } from ${JSON.stringify(pathToFileURL(appBoot).href)};\nconst installAnchor = ${JSON.stringify(anchor)};\nconst profile = loadProfileDirectory('dsh', ${JSON.stringify(profile)}, installAnchor);\nreportSkippedBundles('dsh', profile);\nawait runProfile({ environment: loadLayeredEnv('dsh'), profile: 'workdsh', resolvedProfile: { profile, installAnchor }, patchFiles: ${JSON.stringify(patches)}, args: process.argv.slice(2), packageManager: { command: ${JSON.stringify(executable)}, args: [${JSON.stringify(packageManager)}] } });\n`, { mode: 0o600 }) return launcher } diff --git a/apps/desktop/src/plugin-archive-cache.ts b/apps/desktop/src/plugin-archive-cache.ts new file mode 100644 index 0000000000..8405ea4d4c --- /dev/null +++ b/apps/desktop/src/plugin-archive-cache.ts @@ -0,0 +1,36 @@ +import { join } from 'node:path' +import { writeFileSync } from 'node:fs' + +/** Delegate to the bundled pnpm after retaining local archives in this account's Home. */ +export function cachedPackageManager(home: string, pnpm: string): string { + const runner = join(home, '.workdsh-package-manager.mjs') + writeFileSync(runner, String.raw`import { createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, writeFileSync, statSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawn } from 'node:child_process'; +const cache = join(fileURLToPath(new URL('.', import.meta.url)), 'plugin-archives'); +const args = process.argv.slice(2); +if (args.includes('add')) { + for (let i = 0; i < args.length; i++) { + const spec = args[i]; + if (!/\.(tgz|tar\.gz)$/i.test(spec) || /^(https?:|npm:)/i.test(spec)) continue; + const source = spec.startsWith('file://') ? fileURLToPath(spec) : resolve(spec.startsWith('file:') ? spec.slice(5) : spec); + if (!statSync(source).isFile()) throw new Error('Plugin archive is not a file: ' + source); + const bytes = readFileSync(source); + const digest = createHash('sha256').update(bytes).digest('hex'); + mkdirSync(cache, { recursive: true, mode: 0o700 }); + const destination = join(cache, digest + '.tgz'); + try { writeFileSync(destination, bytes, { flag: 'wx', mode: 0o600 }); } + catch (error) { if (error.code !== 'EEXIST') throw error; } + if (createHash('sha256').update(readFileSync(destination)).digest('hex') !== digest) throw new Error('Plugin archive cache integrity failure'); + args[i] = destination; + } +} +const child = spawn(process.execPath, [${JSON.stringify(pnpm)}, ...args], { stdio: 'inherit', windowsHide: true }); +for (const signal of ['SIGTERM', 'SIGINT']) process.on(signal, () => child.kill(signal)); +child.on('error', error => { console.error(error.message); process.exitCode = 1; }); +child.on('exit', (code, signal) => { process.exitCode = code ?? (signal ? 1 : 0); }); +`, { mode: 0o600 }) + return runner +} diff --git a/apps/desktop/tests/local-runtime.spec.ts b/apps/desktop/tests/local-runtime.spec.ts index f852f3b648..1b4754140b 100644 --- a/apps/desktop/tests/local-runtime.spec.ts +++ b/apps/desktop/tests/local-runtime.spec.ts @@ -60,7 +60,7 @@ it('isolates account/backend directories, device records and credential/skill en it('delegates startup and plugin installation to official runCli using one packaged Node and pnpm', () => { const { source, home } = fixture(); materializeRuntimeProfile(source, home) const launcher = readFileSync(officialLauncher(source, home, '/runtime/node', join(source, 'node_modules/pnpm/bin/pnpm.cjs')), 'utf8') - expect(launcher).toContain('runCli({ manageDesktopProfile: true, packageManager:'); expect(launcher).toContain(JSON.stringify(join(source, 'node_modules/pnpm/bin/pnpm.cjs'))) + expect(launcher).toContain('runCli({ manageDesktopProfile: true, packageManager:'); expect(launcher).toContain('.workdsh-package-manager.mjs'); expect(readFileSync(join(home, '.workdsh-package-manager.mjs'), 'utf8')).toContain(JSON.stringify(join(source, 'node_modules/pnpm/bin/pnpm.cjs'))) expect(launcher).not.toContain('token'); expect(launcher).not.toContain('createHost') const host = readFileSync(officialHostLauncher(source, home, '/runtime/node', join(source, 'node_modules/pnpm/bin/pnpm.cjs'), ['/user/patch.yml']), 'utf8') expect(host).toContain('runProfile({ environment: loadLayeredEnv') diff --git a/apps/desktop/tests/plugin-archive-cache.spec.ts b/apps/desktop/tests/plugin-archive-cache.spec.ts new file mode 100644 index 0000000000..defa988fee --- /dev/null +++ b/apps/desktop/tests/plugin-archive-cache.spec.ts @@ -0,0 +1,32 @@ +import { afterEach, expect, test } from 'vitest' +import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { execFileSync } from 'node:child_process' +import { cachedPackageManager } from '../src/plugin-archive-cache.ts' + +const homes: string[] = [] +afterEach(() => { for (const home of homes.splice(0)) rmSync(home, { recursive: true, force: true }) }) +test('local archives are retained by content and pnpm records the managed path', () => { + const home = mkdtempSync(join(tmpdir(), 'workdsh-plugin-cache-')); homes.push(home) + const fake = join(home, 'pnpm.mjs') + writeFileSync(fake, 'console.log(JSON.stringify(process.argv.slice(2)))') + const source = join(home, 'download with spaces.tgz'); writeFileSync(source, 'archive content') + const runner = cachedPackageManager(home, fake) + const args = JSON.parse(execFileSync(process.execPath, [runner, 'add', source, '--save-exact'], { encoding: 'utf8' })) as string[] + expect(args[0]).toBe('add'); expect(args[2]).toBe('--save-exact') + expect(args[1]).toMatch(/plugin-archives[/\\][a-f0-9]{64}\.tgz$/) + rmSync(source) + expect(readFileSync(args[1]!, 'utf8')).toBe('archive content') + expect(JSON.parse(execFileSync(process.execPath, [runner, 'install', '--frozen-lockfile'], { encoding: 'utf8' }))).toEqual(['install', '--frozen-lockfile']) + writeFileSync(args[1]!, 'corrupted'); writeFileSync(source, 'archive content') + expect(() => execFileSync(process.execPath, [runner, 'add', source], { stdio: 'pipe' })).toThrow() +}) +test('registry installs pass through and personal/member caches are separate', () => { + const home = mkdtempSync(join(tmpdir(), 'workdsh-plugin-cache-')); homes.push(home) + const fake = join(home, 'pnpm.mjs'); writeFileSync(fake, 'console.log(JSON.stringify(process.argv.slice(2)))') + const member = join(home, 'member'); mkdirSync(member) + const personalRunner = cachedPackageManager(home, fake), memberRunner = cachedPackageManager(member, fake) + expect(memberRunner).not.toBe(personalRunner) + expect(JSON.parse(execFileSync(process.execPath, [memberRunner, 'add', '@company/plugin@1.0.0'], { encoding: 'utf8' }))).toEqual(['add', '@company/plugin@1.0.0']) +}) diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 78a88dc11f..5d0d7529c3 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -469,6 +469,9 @@ importers: '@deepseek-ai/dsh-client-ui-slots': specifier: 0.2.0-rc.2 version: 0.2.0-rc.2(@deepseek-ai/cordis@4.0.4) + '@deepseek-ai/dsh-client-ui-workspace': + specifier: 0.2.0-rc.2 + version: 0.2.0-rc.2(@deepseek-ai/cordis@4.0.4) '@deepseek-ai/dsh-llm': specifier: 0.2.0-rc.2 version: 0.2.0-rc.2(@deepseek-ai/cordis@4.0.4) diff --git a/apps/web/scripts/probe-expert-conversation-entry.mjs b/apps/web/scripts/probe-expert-conversation-entry.mjs index 8f696a42cb..b513a46682 100644 --- a/apps/web/scripts/probe-expert-conversation-entry.mjs +++ b/apps/web/scripts/probe-expert-conversation-entry.mjs @@ -227,6 +227,15 @@ try { await connectors.getByRole('button', { name: '管理连接器', exact: true }).click(); await expect(connectors).toHaveCount(0); pass('MCP management footer dismisses the picker and opens management'); + await featureApi('connectors', 'set-selection', { sessionId: comboCreation.sessionId, connectorIds: selectedMcp }); + await page.getByText('New Session', { exact: true }).first().click(); + await expect(page.getByRole('button', { name: '召唤专家', exact: true })).toHaveText(''); + await expect(page.getByRole('button', { name: '连接器', exact: true })).toBeVisible(); + await expect(editor).toHaveText(''); + await expect(editor.locator('[data-composer-chip="workdsh-library"]')).toHaveCount(0); + assert.equal((await featureApi('library', 'task-selection', { sessionId: comboCreation.sessionId })).length, 1); + assert.deepEqual(await featureApi('connectors', 'selection', { sessionId: comboCreation.sessionId }), selectedMcp); + pass('New Session starts without expert, MCP or Library draft while retaining the previous task references'); assert.deepEqual(browserErrors, []); await writeFile(join(artifacts, 'report.json'), JSON.stringify({ checks, sessionId: creation.sessionId, home }, null, 2)); } catch (error) { diff --git a/packages/bundle/package.json b/packages/bundle/package.json index 9361544bca..b6343915e9 100644 --- a/packages/bundle/package.json +++ b/packages/bundle/package.json @@ -41,7 +41,9 @@ "@deepseek-ai/dsh-client-ui-sidebar-right", "@deepseek-ai/dsh-client-ui-session", "@deepseek-ai/dsh-api-remotes", - "@deepseek-ai/dsh-api-session-controller" + "@deepseek-ai/dsh-api-session-controller", + "@deepseek-ai/dsh-api-workspace-controller", + "@deepseek-ai/dsh-client-ui-workspace" ] } }, @@ -76,7 +78,8 @@ "workdsh-plugin-workbench": "workspace:*", "workdsh-ui": "workspace:*", "@types/node": "22.19.0", - "@deepseek-ai/dsh-agent": "0.2.0-rc.2" + "@deepseek-ai/dsh-agent": "0.2.0-rc.2", + "@deepseek-ai/dsh-client-ui-workspace": "0.2.0-rc.2" }, "dependencies": { "@deepseek-ai/dsh-browser-use": "0.2.0-rc.2", diff --git a/packages/bundle/src/client/harness/client.ts b/packages/bundle/src/client/harness/client.ts index 091c0c0f9a..f13f828b21 100644 --- a/packages/bundle/src/client/harness/client.ts +++ b/packages/bundle/src/client/harness/client.ts @@ -1,4 +1,5 @@ import { ShellAppearance } from '../components/ShellAppearance.js'; +import { installFreshSessionNavigation } from './fresh-session.js'; import { communityMarketView, type MarketHost } from '../components/CommunityMarket.js'; import type { Context } from '@deepseek-ai/cordis'; import type {} from '@deepseek-ai/dsh-api-remotes/client'; @@ -32,6 +33,7 @@ const productViews: Readonly> = { }; export function apply(ctx: Context): void { + installFreshSessionNavigation(ctx); ctx.inject(['market'], scope => { if (scope.market.version !== 1) return; scope.market.setSettingsVisible(false); diff --git a/packages/bundle/src/client/harness/fresh-session.ts b/packages/bundle/src/client/harness/fresh-session.ts new file mode 100644 index 0000000000..398063b8d7 --- /dev/null +++ b/packages/bundle/src/client/harness/fresh-session.ts @@ -0,0 +1,28 @@ +import type { Context } from '@deepseek-ai/cordis'; +import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'; +import type {} from '@deepseek-ai/dsh-api-workspace-controller/client'; +import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'; + +/** A New Session is distinct from reopening an unsent draft or switching experts. */ +export function installFreshSessionNavigation(ctx: Context): void { + ctx.inject(['uiWorkspace', 'workspaces', 'sessions'], scope => scope.effect(() => { + const owner = scope.uiWorkspace; + const original = owner.startSession; + const sessions = scope.sessions as unknown as ISessions; + let disposed = false; + const start: typeof original = workspaceId => { + const current = Object.values(sessions.list.getSnapshot().byId).find(row => (row.retainedBy.mainView ?? 0) > 0); + const workspaces = scope.workspaces.list.getSnapshot().items; + const workspace = workspaceId ? workspaces.find(row => row.workspaceId === workspaceId) + : workspaces.find(row => current && row.sessionIds.includes(current.id)) ?? workspaces[0]; + if (!workspace) { original.call(owner, workspaceId); return; } + const navigation = scope.layout.beginNavigation(); + // No sessionId: the official controller must allocate a new, unbound Session. + void sessions.create({ workspaceId: workspace.workspaceId }).then(id => { + if (!disposed && !navigation.aborted) owner.openSession(id); + }).catch(error => { if (!disposed && !navigation.aborted) console.error('无法新建会话', error); }); + }; + owner.startSession = start; + return () => { disposed = true; if (owner.startSession === start) owner.startSession = original; }; + }, 'workdsh.fresh-session-navigation')); +} From 149d00bc1aec1878266b7c0371aa400cbb9f103d Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Mon, 5 Oct 2026 02:09:11 +0800 Subject: [PATCH 2/2] release(desktop): prepare 2.0.6-alpha.5 --- .github/workflows/ci.yml | 2 ++ apps/desktop/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a490d7d7ea..107861bbb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -348,6 +348,8 @@ jobs: cat > release-notes.md <<'EOF' WorkDSH Desktop 将 WorkBuddy 风格的项目、资料、专家、技能和连接器工作台与 DeepSeek Harness 插件能力结合。 + - 本地插件压缩包按内容校验后保存到应用管理目录,安装后移动或删除原始下载文件不再阻挡后续安装。 + - 新会话创建独立空白会话,不沿用旧草稿的专家、MCP 或资料选择;明确切换专家仍保留当前草稿与引用。 - 修复 macOS 召唤或切换专家时的 Dock 闪动,后台浏览器从原生启动阶段保持无 Dock 图标,主窗口正常显示。 - 对话框可直接召唤专家并显示当前专家名称;切换专家保留资料库的可点击引用和 MCP 选择。 - 资料库支持 CSV/XLSX 导入;专家、资料库和 MCP 的选择浮层保持一致。 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 8f5615ee59..4ae9d3bb74 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "dsh-plugin-desktop", - "version": "2.0.6-alpha.4", + "version": "2.0.6-alpha.5", "description": "WorkDSH Electron carrier for a pinned DeepSeek Harness runtime Profile", "license": "MIT", "publishConfig": {