From 3a363075c2090f43cf41219d2fcddac269b04c51 Mon Sep 17 00:00:00 2001 From: Modole <104966315+Modole@users.noreply.github.com> Date: Sat, 15 Aug 2026 05:09:59 +0800 Subject: [PATCH] fix: include DSH runtime dependencies in installers --- .github/workflows/installers.yml | 2 + package-lock.json | 42 +++++------ package.json | 21 +++++- scripts/verify-packaged-runtime.mjs | 111 ++++++++++++++++++++++++++++ tests/desktop-shell.test.mjs | 39 +++++++++- 5 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 scripts/verify-packaged-runtime.mjs diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml index 0c4f4dd..4ccddb6 100644 --- a/.github/workflows/installers.yml +++ b/.github/workflows/installers.yml @@ -19,6 +19,7 @@ jobs: - run: npm ci - run: npm run audit:public - run: npm run desktop:installer -- --mac dmg --arm64 + - run: node scripts/verify-packaged-runtime.mjs "out/installers/mac-arm64/劳博士.app/Contents/Resources/app" - run: test "$(find out/installers -maxdepth 1 -name 'laobos-studio-*-macos-arm64.dmg' | wc -l | tr -d ' ')" = "1" - uses: actions/upload-artifact@v7 with: @@ -40,6 +41,7 @@ jobs: - run: npm ci - run: npm run audit:public - run: npm run desktop:installer -- --win nsis --x64 + - run: node scripts/verify-packaged-runtime.mjs "out/installers/win-unpacked/resources/app" - shell: pwsh run: | $installers = @(Get-ChildItem "out/installers/laobos-studio-*-windows-x64-setup.exe") diff --git a/package-lock.json b/package-lock.json index b634bcc..8465752 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,36 @@ { "name": "laobos-agent-studio", - "version": "0.2.1", + "version": "0.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "laobos-agent-studio", - "version": "0.2.1", + "version": "0.2.2", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { "@browserops/bridge": "^0.0.11", + "@deepseek-ai/cordis-plugin-group": "1.0.1", "@deepseek-ai/dsh": "0.1.0-rc.6", + "@deepseek-ai/dsh-anonymous-user-id": "0.1.0-rc.6", + "@deepseek-ai/dsh-atomic-write": "0.1.0-rc.6", + "@deepseek-ai/dsh-bash-local": "0.1.0-rc.6", + "@deepseek-ai/dsh-code-runtime": "0.1.0-rc.6", + "@deepseek-ai/dsh-compaction": "0.1.0-rc.6", + "@deepseek-ai/dsh-fs": "0.1.0-rc.6", + "@deepseek-ai/dsh-invariants": "0.1.0-rc.6", + "@deepseek-ai/dsh-output-retention": "0.1.0-rc.6", + "@deepseek-ai/dsh-sandbox": "0.1.0-rc.6", + "@deepseek-ai/dsh-scope": "0.1.0-rc.6", + "@deepseek-ai/dsh-session-telemetry": "0.1.0-rc.6", + "@deepseek-ai/dsh-session-title-llm": "0.1.0-rc.6", + "@deepseek-ai/dsh-shell": "0.1.0-rc.6", + "@deepseek-ai/dsh-spill": "0.1.0-rc.6", + "@deepseek-ai/dsh-subagent-in-process-driver": "0.1.0-rc.6", + "@deepseek-ai/dsh-subprocess": "0.1.0-rc.6", + "@deepseek-ai/dsh-timeout": "0.1.0-rc.6", + "@deepseek-ai/dsh-workflow": "0.1.0-rc.6", "@laobos/dsh-system-tools": "file:packages/laobos-system-tools", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", @@ -999,7 +1018,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/cordis-plugin-group/-/cordis-plugin-group-1.0.1.tgz", "integrity": "sha512-E1NThkFB3jn3TCqa6Oc++1zQHqLejF3W2wDwv2BlL3UEmgtBXL1K1j1koc+j676RuiOXtJkUJlPcOntRGKLWBQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/cordis-plugin-loader": "^1.0.2" @@ -1269,7 +1287,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-anonymous-user-id/-/dsh-anonymous-user-id-0.1.0-rc.6.tgz", "integrity": "sha512-+K/+tQzuDlmvKhoudB7qVMJMNF/p1+GTRQkkPA3JHIQM5yHj7AUYY8oMLROTvUivs8XWtkNyEP1/c6nPwrHazQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-brand": "^0.1.0-rc.6", @@ -1349,7 +1366,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-atomic-write/-/dsh-atomic-write-0.1.0-rc.6.tgz", "integrity": "sha512-Ot/2aygDzWuN4ypUkDJA6MpSlGbcvzzKnWAms0lg+2WUtPZr7O0cGb83K3TEO2NoAu0LJfVxlbo8Eya3tDaFtw==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6" @@ -1476,7 +1492,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-bash-local/-/dsh-bash-local-0.1.0-rc.6.tgz", "integrity": "sha512-YAc5W9uR7sN1Rrobp6fQlCaOdHqCWatTktV68UsoDuOJs5eM3nWEUUUVN8jdZCIDZhktT5hqbhe3+DzOX3Xg1w==", "license": "MIT", - "peer": true, "dependencies": { "@deepseek-ai/schemastery": "^3.18.1" }, @@ -1951,7 +1966,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-code-runtime/-/dsh-code-runtime-0.1.0-rc.6.tgz", "integrity": "sha512-aw8D4IOeMo11A3uxQeE4LFoW3bvaQnVkGFqqtS+lsINDARrOCJHXLUgecoKpys+Lc5erZZ4UQDnymJmL4OCcKA==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6" @@ -2034,7 +2048,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-compaction/-/dsh-compaction-0.1.0-rc.6.tgz", "integrity": "sha512-Uu8qgrHom13gdwwxAnqmNuWM8MqafkRlhjQMMJYmaMVVxjEUewSJ6lwtMbUEBR5ViLsfv4pTm5TnhxNCjedBUw==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-brand": "^0.1.0-rc.6", @@ -2142,7 +2155,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-fs/-/dsh-fs-0.1.0-rc.6.tgz", "integrity": "sha512-OTkwb4QsZgmjtA/8ZEPh1FapmrBr3N989/G4Wmo1JkAvKbMxkYty6LxjckOawTTz7GJTfUoZCrw9uopDfIMMNw==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-brand": "^0.1.0-rc.6", @@ -2401,7 +2413,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-invariants/-/dsh-invariants-0.1.0-rc.6.tgz", "integrity": "sha512-WfEfOi99a4cpOugRAHTBSTnesLieu3ist1q9PXDXFBHX++K1rAl9+sB7YrdnbB8LH0UOY532gS9xJUYU6w0SLw==", "license": "MIT", - "peer": true, "dependencies": { "@deepseek-ai/schemastery": "^3.18.1" }, @@ -2577,7 +2588,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-output-retention/-/dsh-output-retention-0.1.0-rc.6.tgz", "integrity": "sha512-3TQJxGqjotWrvNKI+Da8nIDjzpKZEGpImeMBp7EklfO0CDbiXA5B0Ms39zBniWkWr1lvbm2iJiYERY5uDeP6og==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6" @@ -2696,7 +2706,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-sandbox/-/dsh-sandbox-0.1.0-rc.6.tgz", "integrity": "sha512-SLZjuivQQKHTx4H7xlsjraEGGMIEA8BYOwvlm/9uZBrBrlttMU+d8Ne6QYpDcOwIEubL8mvrWRg8j3rtY3lVyA==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6", @@ -2773,7 +2782,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-scope/-/dsh-scope-0.1.0-rc.6.tgz", "integrity": "sha512-UlDLV4syLoJinNg9imhXrSAHrdaTa5Ff8gg46rzjFJGPUOhAk3DZff0hryT5OhrBi0A5Tj92qVpg2pRVvxnUzQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6" @@ -2950,7 +2958,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-session-telemetry/-/dsh-session-telemetry-0.1.0-rc.6.tgz", "integrity": "sha512-x3t7lu+SLHvxhmOlocNyOwcj9t4MxUkzh7QyOh/YSMvWgojSOhNdnKcjWq4pE9PxFXBWMDpN3bLNvtk5jFA/+w==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-agent": "^0.1.0-rc.6", @@ -3022,7 +3029,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-session-title-llm/-/dsh-session-title-llm-0.1.0-rc.6.tgz", "integrity": "sha512-BiqsXDSbyZbQ1XtLhKpILKdnDgZaQKWopZpSQW+Cy6i0wozMJ/xeO+oCWEFn0rQtsgBY6b+rKVKUW4wc7u9Ydg==", "license": "MIT", - "peer": true, "dependencies": { "@deepseek-ai/schemastery": "^3.18.1" }, @@ -3070,7 +3076,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-shell/-/dsh-shell-0.1.0-rc.6.tgz", "integrity": "sha512-l/pcwbdpM+Zgp2AzH0kH0z7bPU7L3k4uG1t5Du1V5ilk70tyMSxz2upNfAbjkDHZZZO18LTs33aq+zRWHX1CUg==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6", @@ -3173,7 +3178,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-spill/-/dsh-spill-0.1.0-rc.6.tgz", "integrity": "sha512-reV4utwd3E5LgiNrsaqh6i6zxHboVyrrL1ilALRukDbMxSyup1vuSRh+4CPxSUh9YcoToFJ6jz2Zmlsw755g1w==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-brand": "^0.1.0-rc.6", @@ -3328,7 +3332,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-subagent-in-process-driver/-/dsh-subagent-in-process-driver-0.1.0-rc.6.tgz", "integrity": "sha512-KK8Ep4PeSyrM3YQdcZhetXsiBd+bliKu6NWWKyTh60LobKtVNREfPBlqRfzc0mU0ZIKD4VEC7RfuJaqvz4UlPw==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-agent": "^0.1.0-rc.6", @@ -3360,7 +3363,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-subprocess/-/dsh-subprocess-0.1.0-rc.6.tgz", "integrity": "sha512-nZaZRjSnE1he+GAd14vURAH7n3Fw5eGx3nzMbkB96Y6Qx+oQB/7PTuXxKVxlCh8TSfMJTf851ahwWb1eALjKlw==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6" @@ -3448,7 +3450,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-timeout/-/dsh-timeout-0.1.0-rc.6.tgz", "integrity": "sha512-CUean0fAnfsJVszFEip7PsU/S26W+JfDFfsza2dCtlw8n6xlkbHA9Gjxdk2aTwqDGCgXEPkRW7mYkdJ0n6FR7w==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6" @@ -4783,7 +4784,6 @@ "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-workflow/-/dsh-workflow-0.1.0-rc.6.tgz", "integrity": "sha512-zpTqaEp5/aXp3Zj5OfpZpTZxncak8af0FSSCOj5e6Eo6+o6cW2Y48fPk5Rl9Dzcbm/x4Y4PZfJSyqazeA27YeA==", "license": "MIT", - "peer": true, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-agent": "^0.1.0-rc.6", diff --git a/package.json b/package.json index c6c268b..f90ebad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "laobos-agent-studio", - "version": "0.2.1", + "version": "0.2.2", "description": "劳博士:基于 DeepSeek Harness 的本地 Agent 桌面客户端", "author": "Modole contributors", "license": "SEE LICENSE IN LICENSE", @@ -43,7 +43,26 @@ }, "dependencies": { "@browserops/bridge": "^0.0.11", + "@deepseek-ai/cordis-plugin-group": "1.0.1", "@deepseek-ai/dsh": "0.1.0-rc.6", + "@deepseek-ai/dsh-anonymous-user-id": "0.1.0-rc.6", + "@deepseek-ai/dsh-atomic-write": "0.1.0-rc.6", + "@deepseek-ai/dsh-bash-local": "0.1.0-rc.6", + "@deepseek-ai/dsh-code-runtime": "0.1.0-rc.6", + "@deepseek-ai/dsh-compaction": "0.1.0-rc.6", + "@deepseek-ai/dsh-fs": "0.1.0-rc.6", + "@deepseek-ai/dsh-invariants": "0.1.0-rc.6", + "@deepseek-ai/dsh-output-retention": "0.1.0-rc.6", + "@deepseek-ai/dsh-sandbox": "0.1.0-rc.6", + "@deepseek-ai/dsh-scope": "0.1.0-rc.6", + "@deepseek-ai/dsh-session-telemetry": "0.1.0-rc.6", + "@deepseek-ai/dsh-session-title-llm": "0.1.0-rc.6", + "@deepseek-ai/dsh-shell": "0.1.0-rc.6", + "@deepseek-ai/dsh-spill": "0.1.0-rc.6", + "@deepseek-ai/dsh-subagent-in-process-driver": "0.1.0-rc.6", + "@deepseek-ai/dsh-subprocess": "0.1.0-rc.6", + "@deepseek-ai/dsh-timeout": "0.1.0-rc.6", + "@deepseek-ai/dsh-workflow": "0.1.0-rc.6", "@laobos/dsh-system-tools": "file:packages/laobos-system-tools", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", diff --git a/scripts/verify-packaged-runtime.mjs b/scripts/verify-packaged-runtime.mjs new file mode 100644 index 0000000..7e0b102 --- /dev/null +++ b/scripts/verify-packaged-runtime.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +import { access, readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const platformProvidedPackages = new Set(["electron"]); + +async function exists(target) { + try { + await access(target); + return true; + } catch { + return false; + } +} + +async function packageDirectories(nodeModulesDirectory, result, visited) { + if (visited.has(nodeModulesDirectory)) return; + visited.add(nodeModulesDirectory); + + const entries = await readdir(nodeModulesDirectory, { withFileTypes: true }).catch((error) => { + if (error?.code === "ENOENT") return []; + throw error; + }); + + for (const entry of entries) { + if (entry.name.startsWith(".")) continue; + const entryPath = path.join(nodeModulesDirectory, entry.name); + if (entry.name.startsWith("@")) { + const scopedEntries = await readdir(entryPath, { withFileTypes: true }); + for (const scopedEntry of scopedEntries) { + if (!scopedEntry.isDirectory() && !scopedEntry.isSymbolicLink()) continue; + const packageDirectory = path.join(entryPath, scopedEntry.name); + result.add(packageDirectory); + await packageDirectories(path.join(packageDirectory, "node_modules"), result, visited); + } + continue; + } + + if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; + result.add(entryPath); + await packageDirectories(path.join(entryPath, "node_modules"), result, visited); + } +} + +function requiredDependencies(manifest) { + const optional = new Set(Object.keys(manifest.optionalDependencies || {})); + const required = new Set( + Object.keys(manifest.dependencies || {}).filter((name) => !optional.has(name)), + ); + + for (const name of Object.keys(manifest.peerDependencies || {})) { + if (manifest.peerDependenciesMeta?.[name]?.optional !== true) required.add(name); + } + return [...required].sort(); +} + +async function resolvesInsideApp(appDirectory, packageDirectory, dependency) { + if (platformProvidedPackages.has(dependency)) return true; + let current = packageDirectory; + while (current === appDirectory || current.startsWith(`${appDirectory}${path.sep}`)) { + if (await exists(path.join(current, "node_modules", dependency, "package.json"))) return true; + if (current === appDirectory) break; + current = path.dirname(current); + } + return false; +} + +export async function verifyPackagedRuntime(appDirectoryInput) { + const appDirectory = path.resolve(appDirectoryInput); + const rootManifestPath = path.join(appDirectory, "package.json"); + if (!(await exists(rootManifestPath))) { + throw new Error(`安装包应用目录无效,缺少 package.json:${appDirectory}`); + } + + const packageRoots = new Set([appDirectory]); + await packageDirectories(path.join(appDirectory, "node_modules"), packageRoots, new Set()); + const missing = []; + + for (const packageDirectory of [...packageRoots].sort()) { + const manifestPath = path.join(packageDirectory, "package.json"); + if (!(await exists(manifestPath))) continue; + const manifest = JSON.parse(await readFile(manifestPath, "utf8")); + const owner = manifest.name || path.relative(appDirectory, packageDirectory) || ""; + for (const dependency of requiredDependencies(manifest)) { + if (!(await resolvesInsideApp(appDirectory, packageDirectory, dependency))) { + missing.push({ dependency, owner }); + } + } + } + + if (missing.length > 0) { + const rows = missing + .map(({ dependency, owner }) => `- ${owner} -> ${dependency}`) + .join("\n"); + throw new Error(`安装包缺少 ${missing.length} 项必需运行时依赖:\n${rows}`); + } + + return { packageCount: packageRoots.size }; +} + +const isCommandLineEntry = process.argv[1] + && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +if (isCommandLineEntry) { + const appDirectory = process.argv[2]; + if (!appDirectory) throw new Error("用法:node scripts/verify-packaged-runtime.mjs "); + const result = await verifyPackagedRuntime(appDirectory); + console.log(`安装包运行时依赖完整:已检查 ${result.packageCount} 个包。`); +} diff --git a/tests/desktop-shell.test.mjs b/tests/desktop-shell.test.mjs index 1bc6af9..57c3f51 100644 --- a/tests/desktop-shell.test.mjs +++ b/tests/desktop-shell.test.mjs @@ -1,6 +1,9 @@ import assert from "node:assert/strict"; -import { readFile } from "node:fs/promises"; +import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; import test from "node:test"; +import { verifyPackagedRuntime } from "../scripts/verify-packaged-runtime.mjs"; test("desktop shell boots DSH directly with a sandboxed renderer", async () => { const mainSource = await readFile( @@ -52,5 +55,39 @@ test("desktop release configuration builds native macOS and Windows installers", assert.match(workflow, /runs-on: macos-15/); assert.match(workflow, /runs-on: windows-latest/); assert.equal(workflow.match(/npm run audit:public/g)?.length, 2); + assert.equal(workflow.match(/verify-packaged-runtime\.mjs/g)?.length, 2); assert.equal(workflow.match(/actions\/upload-artifact@v7/g)?.length, 2); }); + +test("packaged runtime verification rejects a missing required peer dependency", async () => { + const appDirectory = await mkdtemp(path.join(tmpdir(), "laobos-packaged-runtime-test-")); + const hostDirectory = path.join(appDirectory, "node_modules", "fixture-host"); + const peerDirectory = path.join(appDirectory, "node_modules", "fixture-peer"); + + try { + await mkdir(hostDirectory, { recursive: true }); + await writeFile(path.join(appDirectory, "package.json"), JSON.stringify({ + name: "fixture-app", + dependencies: { "fixture-host": "1.0.0" }, + })); + await writeFile(path.join(hostDirectory, "package.json"), JSON.stringify({ + name: "fixture-host", + version: "1.0.0", + peerDependencies: { "fixture-peer": "^1.0.0" }, + })); + + await assert.rejects( + verifyPackagedRuntime(appDirectory), + /fixture-host -> fixture-peer/, + ); + + await mkdir(peerDirectory, { recursive: true }); + await writeFile(path.join(peerDirectory, "package.json"), JSON.stringify({ + name: "fixture-peer", + version: "1.0.0", + })); + assert.deepEqual(await verifyPackagedRuntime(appDirectory), { packageCount: 3 }); + } finally { + await rm(appDirectory, { recursive: true, force: true }); + } +});