diff --git a/apps/tray/start-tray.ps1 b/apps/tray/start-tray.ps1 index 81ac5c1..1e36720 100644 --- a/apps/tray/start-tray.ps1 +++ b/apps/tray/start-tray.ps1 @@ -335,6 +335,18 @@ foreach ($requiredRuntimeFile in @($HostScript, $coreDist, $adapterDist)) { } } +if ($script:targetHost -eq "dsh") { + $pluginInstaller = Join-Path $RepoRoot "scripts\install-dsh-plugin.ps1" + $pluginRoot = Join-Path $RepoRoot "integrations\deepseek-harness" + if (Test-Path -LiteralPath $pluginInstaller -PathType Leaf) { + try { + & $pluginInstaller -PluginRoot $pluginRoot + } catch { + Write-BcTrayLog ("dsh plugin install failed: {0}" -f $_.Exception.Message) + } + } +} + # Cryptographically strong token for the local control plane. $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() try { diff --git a/docs/deepseek-harness.md b/docs/deepseek-harness.md index d8b551d..4cefe52 100644 --- a/docs/deepseek-harness.md +++ b/docs/deepseek-harness.md @@ -16,6 +16,8 @@ beautiCode 通过 DeepSeek Harness 的 Cordis 插件接口接入(`@beauticode/ ## 安装插件 +Windows 安装包会在安装结束时(以及选择 DeepSeek Harness 时)自动写入你的 DSH profile。没有 DSH 也不影响安装;第一次运行 `dsh web` 会走 home 层补丁。也可手动: + ```sh # 源码目录 dsh plugin --profile web add file:/integrations/deepseek-harness diff --git a/docs/windows-installer.md b/docs/windows-installer.md index 4921d59..68f240d 100644 --- a/docs/windows-installer.md +++ b/docs/windows-installer.md @@ -10,8 +10,9 @@ The package includes the compiled beautiCode runtime and a pinned Node.js x64 runtime. The target machine does not need Node.js, npm, TypeScript, or a source -checkout. DeepSeek Harness is **not** bundled — install the plugin from -`{app}\integrations\deepseek-harness` into your DSH profile and run `dsh web` +checkout. DeepSeek Harness is **not** bundled. The installer ships the plugin files and +wires them into the user's DSH profile (`dsh plugin` is not required; if no +profile exists yet, a home-level `cordis.patch.yml` is written). Run `dsh web` yourself. Codex Desktop is optional and remains a separate prerequisite if you choose that host; the tray still launches Codex as before. diff --git a/installer/windows/beauticode.iss b/installer/windows/beauticode.iss index 07f2a47..2ac7f01 100644 --- a/installer/windows/beauticode.iss +++ b/installer/windows/beauticode.iss @@ -58,4 +58,8 @@ Name: "{autodesktop}\beautiCode"; Filename: "{sys}\WindowsPowerShell\v1.0\powers Name: "{userstartup}\beautiCode"; Filename: "{sys}\WindowsPowerShell\v1.0\powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ""{app}\scripts\start-beauticode-engine.ps1"" -NoLaunchCodex"; WorkingDir: "{app}"; IconFilename: "{app}\beauticode.ico"; Tasks: autostart [Run] +Filename: "{sys}\WindowsPowerShell\v1.0\powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\install-dsh-plugin.ps1"" -PluginRoot ""{app}\integrations\deepseek-harness"""; WorkingDir: "{app}"; StatusMsg: "正在把 beautiCode 插件写入 DeepSeek Harness…"; Flags: postinstall runhidden Filename: "{sys}\WindowsPowerShell\v1.0\powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ""{app}\scripts\start-beauticode.ps1"""; WorkingDir: "{app}"; Description: "启动 beautiCode"; Flags: postinstall nowait skipifsilent + +[UninstallRun] +Filename: "{sys}\WindowsPowerShell\v1.0\powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\install-dsh-plugin.ps1"" -PluginRoot ""{app}\integrations\deepseek-harness"" -Remove"; WorkingDir: "{app}"; Flags: runhidden diff --git a/packages/adapter-dsh/test/launcher-scripts.test.js b/packages/adapter-dsh/test/launcher-scripts.test.js index 85cc5e6..5767f3d 100644 --- a/packages/adapter-dsh/test/launcher-scripts.test.js +++ b/packages/adapter-dsh/test/launcher-scripts.test.js @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; @@ -12,6 +13,7 @@ const scripts = [ "scripts/start-beauticode.ps1", "scripts/start-beauticode-engine.ps1", "scripts/codex-launch.ps1", + "scripts/install-dsh-plugin.ps1", "apps/tray/start-tray.ps1", ]; @@ -100,3 +102,92 @@ test("picker DryRun routes DSH to the tray and Codex to its launcher", () => { assert.match(result.stdout, new RegExp(needle.replace(/[.]/g, "\\."))); } }); + +test("install-dsh-plugin wires a missing DSH home and can uninstall", () => { + const script = path.join(repoRoot, "scripts/install-dsh-plugin.ps1"); + const pluginRoot = path.join(repoRoot, "integrations/deepseek-harness"); + const home = fs.mkdtempSync(path.join(os.tmpdir(), "bc-dsh-home-")); + try { + const add = spawnSync( + "powershell.exe", + [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + script, + "-PluginRoot", + pluginRoot, + "-DshHome", + home, + ], + { encoding: "utf8", windowsHide: true }, + ); + assert.equal(add.status, 0, add.stderr || add.stdout); + const homePatch = fs.readFileSync(path.join(home, "cordis.patch.yml"), "utf8"); + assert.match(homePatch, /id: beauticode-bridge/); + assert.match(homePatch, /file:\/\//); + + const web = path.join(home, "profiles", "web"); + fs.mkdirSync(web, { recursive: true }); + fs.writeFileSync( + path.join(web, "package.json"), + JSON.stringify({ + name: "dsh-profile-web", + private: true, + dependencies: {}, + dsh: { profile: { bundles: ["@deepseek-ai/dsh-base"] } }, + }), + "utf8", + ); + fs.writeFileSync(path.join(web, "cordis.patch.yml"), "[]\n", "utf8"); + const migrate = spawnSync( + "powershell.exe", + [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + script, + "-PluginRoot", + pluginRoot, + "-DshHome", + home, + ], + { encoding: "utf8", windowsHide: true }, + ); + assert.equal(migrate.status, 0, migrate.stderr || migrate.stdout); + const webPatch = fs.readFileSync(path.join(web, "cordis.patch.yml"), "utf8"); + assert.match(webPatch, /@beauticode\/dsh-plugin/); + assert.equal(fs.existsSync(path.join(home, "cordis.patch.yml")), false); + assert.ok( + fs.existsSync( + path.join(web, "node_modules", "@beauticode", "dsh-plugin", "index.mjs"), + ), + ); + + const remove = spawnSync( + "powershell.exe", + [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + script, + "-PluginRoot", + pluginRoot, + "-DshHome", + home, + "-Remove", + ], + { encoding: "utf8", windowsHide: true }, + ); + assert.equal(remove.status, 0, remove.stderr || remove.stdout); + assert.equal( + fs.existsSync(path.join(web, "node_modules", "@beauticode", "dsh-plugin")), + false, + ); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } +}); diff --git a/scripts/build-windows-installer.ps1 b/scripts/build-windows-installer.ps1 index 33131cf..2bafc07 100644 --- a/scripts/build-windows-installer.ps1 +++ b/scripts/build-windows-installer.ps1 @@ -225,6 +225,7 @@ foreach ($relativeFile in @( "scripts\codex-launch.ps1", "scripts\start-beauticode.ps1", "scripts\start-beauticode-engine.ps1", + "scripts\install-dsh-plugin.ps1", "packages\core\package.json", "packages\adapter-codex\package.json", "packages\adapter-dsh\package.json", diff --git a/scripts/install-dsh-plugin.ps1 b/scripts/install-dsh-plugin.ps1 new file mode 100644 index 0000000..3e87fdb --- /dev/null +++ b/scripts/install-dsh-plugin.ps1 @@ -0,0 +1,234 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Wire the beautiCode Cordis plugin into the user's own DSH profile. + +.DESCRIPTION + Does not install or start DeepSeek Harness. If a web profile already exists, + link the plugin package and insert it in that profile's patch. If DSH has + not been initialized yet, write a home-level cordis.patch.yml so the first + `dsh web` still loads the plugin. +#> +[CmdletBinding()] +param( + [string]$PluginRoot = "", + [string]$DshHome = "", + [switch]$Remove +) + +$ErrorActionPreference = "Stop" + +$scriptDir = $PSScriptRoot +if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path } +$repoRoot = (Resolve-Path (Join-Path $scriptDir "..")).Path + +if (-not $PluginRoot) { + $PluginRoot = Join-Path $repoRoot "integrations\deepseek-harness" +} +$PluginRoot = [IO.Path]::GetFullPath($PluginRoot) +$indexFile = Join-Path $PluginRoot "index.mjs" +$packageFile = Join-Path $PluginRoot "package.json" + +if (-not $DshHome) { + if ($env:DSH_HOME) { $DshHome = $env:DSH_HOME } + else { $DshHome = Join-Path $env:USERPROFILE ".dsh" } +} +$DshHome = [IO.Path]::GetFullPath($DshHome) +$webProfile = Join-Path $DshHome "profiles\web" +$webPatch = Join-Path $webProfile "cordis.patch.yml" +$webPackage = Join-Path $webProfile "package.json" +$homePatch = Join-Path $DshHome "cordis.patch.yml" +$pluginName = "@beauticode/dsh-plugin" +$bridgeId = "beauticode-bridge" + +function Write-BcLog([string]$Message) { + $line = "[{0:u}] {1}" -f (Get-Date).ToUniversalTime(), $Message + Write-Host $line + try { + $logRoot = if ($env:LOCALAPPDATA) { + Join-Path $env:LOCALAPPDATA "beautiCode\logs" + } else { + Join-Path ([IO.Path]::GetTempPath()) "beautiCode\logs" + } + if (-not (Test-Path -LiteralPath $logRoot)) { + New-Item -ItemType Directory -Path $logRoot -Force | Out-Null + } + Add-Content -LiteralPath (Join-Path $logRoot "dsh-plugin-install.log") -Value $line -Encoding UTF8 + } catch { } +} + +function ConvertTo-FileUri([string]$Path) { + $full = [IO.Path]::GetFullPath($Path).Replace("\", "/") + if ($full -match '^[A-Za-z]:') { + return "file:///" + $full + } + return "file://" + $full +} + +function Get-FileUriInsert([string]$Uri) { + return @( + "# beauticode-bridge (installer)" + "- insert:" + " - id: $bridgeId" + " name: '$Uri'" + " inject: [webServer]" + ) -join "`n" +} + +function Get-PackageInsert { + return @( + "# beauticode-bridge (installer)" + "- insert:" + " - id: $bridgeId" + " name: '$pluginName'" + " inject: [webServer]" + ) -join "`n" +} + +function Test-PatchHasBridge([string]$Text) { + return [bool]($Text -match "(?m)^\s*-\s*id:\s*$bridgeId\s*$") +} + +function Remove-BridgeFromPatch([string]$Path) { + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + $raw = [IO.File]::ReadAllText($Path) + if (-not (Test-PatchHasBridge $raw)) { return $false } + $cleaned = [regex]::Replace( + $raw, + "(?ms)(?:^|\r?\n)# beauticode-bridge \(installer\)\r?\n- insert:\r?\n(?:[ \t]+.*\r?\n)*", + "`n" + ) + $cleaned = [regex]::Replace( + $cleaned, + "(?ms)(?:^|\r?\n)- insert:\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+-\s*id:\s*$bridgeId\r?\n(?:[ \t]+.*\r?\n)*", + "`n" + ) + $trimmed = $cleaned.Trim() + if ($trimmed -eq "" -or $trimmed -eq "[]") { + $dir = Split-Path -Parent $Path + if ([IO.Path]::GetFileName($Path) -eq "cordis.patch.yml" -and + $dir -eq $DshHome) { + Remove-Item -LiteralPath $Path -Force + return $true + } + [IO.File]::WriteAllText($Path, "# Your patch layer for this dsh profile.`r`n[]`r`n") + return $true + } + [IO.File]::WriteAllText($Path, $trimmed.TrimEnd() + "`r`n") + return $true +} + +function Write-BridgePatch([string]$Path, [string]$Body) { + $dir = Split-Path -Parent $Path + if (-not (Test-Path -LiteralPath $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + [IO.File]::WriteAllText($Path, $Body + "`r`n") + return + } + $raw = [IO.File]::ReadAllText($Path) + if (Test-PatchHasBridge $raw) { + $replaced = [regex]::Replace( + $raw, + "(?ms)(?:# beauticode-bridge \(installer\)\r?\n)?- insert:\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+-\s*id:\s*$bridgeId\r?\n(?:[ \t]+.*\r?\n)*", + ($Body + "`r`n") + ) + if ($replaced -eq $raw) { + [IO.File]::WriteAllText($Path, $Body + "`r`n") + } else { + [IO.File]::WriteAllText($Path, $replaced.TrimEnd() + "`r`n") + } + return + } + $stripped = $raw.Trim() + if ($stripped -eq "" -or $stripped -eq "[]") { + [IO.File]::WriteAllText($Path, $Body + "`r`n") + return + } + [IO.File]::WriteAllText($Path, $stripped.TrimEnd() + "`r`n`r`n" + $Body + "`r`n") +} + +function Ensure-PluginJunction { + $linkParent = Join-Path $webProfile "node_modules\@beauticode" + $link = Join-Path $linkParent "dsh-plugin" + if (-not (Test-Path -LiteralPath $linkParent)) { + New-Item -ItemType Directory -Path $linkParent -Force | Out-Null + } + if (Test-Path -LiteralPath $link) { + $item = Get-Item -LiteralPath $link -Force + $target = $null + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { + $target = $item.Target + if ($target -is [array]) { $target = $target[0] } + } + if ($target -and ([IO.Path]::GetFullPath($target) -eq $PluginRoot)) { + return + } + Remove-Item -LiteralPath $link -Force -Recurse + } + New-Item -ItemType Junction -Path $link -Target $PluginRoot | Out-Null +} + +function Ensure-WebPackageDep { + if (-not (Test-Path -LiteralPath $webPackage -PathType Leaf)) { return } + $raw = [IO.File]::ReadAllText($webPackage) + $json = $raw | ConvertFrom-Json + if (-not $json.dependencies) { + $json | Add-Member -NotePropertyName dependencies -NotePropertyValue ([pscustomobject]@{}) -Force + } + $linkSpec = "link:" + ($PluginRoot -replace "\\", "/") + $deps = $json.dependencies + $current = $null + if ($deps.PSObject.Properties.Name -contains $pluginName) { + $current = [string]$deps.$pluginName + } + if ($current -eq $linkSpec) { return } + $deps | Add-Member -NotePropertyName $pluginName -NotePropertyValue $linkSpec -Force + $json | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $webPackage -Encoding UTF8 +} + +if (-not (Test-Path -LiteralPath $indexFile -PathType Leaf)) { + throw ("缺少 DSH 插件入口:{0}" -f $indexFile) +} +if (-not (Test-Path -LiteralPath $packageFile -PathType Leaf)) { + throw ("缺少 DSH 插件清单:{0}" -f $packageFile) +} + +if ($Remove) { + $removed = $false + if (Remove-BridgeFromPatch $webPatch) { $removed = $true } + if (Remove-BridgeFromPatch $homePatch) { $removed = $true } + $link = Join-Path $webProfile "node_modules\@beauticode\dsh-plugin" + if (Test-Path -LiteralPath $link) { + Remove-Item -LiteralPath $link -Force -Recurse + $removed = $true + } + if ($removed) { Write-BcLog "Removed beautiCode DSH plugin wiring." } + else { Write-BcLog "No beautiCode DSH plugin wiring to remove." } + exit 0 +} + +$fileUri = ConvertTo-FileUri $indexFile +$webExists = Test-Path -LiteralPath $webPackage -PathType Leaf + +if ($webExists) { + Ensure-PluginJunction + Ensure-WebPackageDep + Write-BridgePatch $webPatch (Get-PackageInsert) + if (Test-Path -LiteralPath $homePatch -PathType Leaf) { + $homeRaw = [IO.File]::ReadAllText($homePatch) + if (Test-PatchHasBridge $homeRaw) { + [void](Remove-BridgeFromPatch $homePatch) + } + } + Write-BcLog ("Linked $pluginName into $webProfile") +} else { + if (-not (Test-Path -LiteralPath $DshHome)) { + New-Item -ItemType Directory -Path $DshHome -Force | Out-Null + } + Write-BridgePatch $homePatch (Get-FileUriInsert $fileUri) + Write-BcLog ("DSH web profile not found; wrote home patch $homePatch") +} + +exit 0 diff --git a/scripts/start-beauticode.ps1 b/scripts/start-beauticode.ps1 index d156682..f73d06b 100644 --- a/scripts/start-beauticode.ps1 +++ b/scripts/start-beauticode.ps1 @@ -468,6 +468,18 @@ if ($DryRun) { return } +if ($route.Host -eq "dsh") { + $pluginInstaller = Join-Path $repoRoot "scripts\install-dsh-plugin.ps1" + $pluginRoot = Join-Path $repoRoot "integrations\deepseek-harness" + if (Test-Path -LiteralPath $pluginInstaller -PathType Leaf) { + try { + & $pluginInstaller -PluginRoot $pluginRoot + } catch { + Write-Host ("未能自动写入 DSH 插件:{0}" -f $_.Exception.Message) + } + } +} + $runningHost = Get-BcRunningHost if ($runningHost -eq $route.Host) { [void](Send-BcTrayEvent -Name "Local\beautiCode.Engine.ShowPanel.v1")