feat: DSH 悬浮窗 v0.1.0 — 原生置顶小窗 + 会话镜像 + 抓屏隐身
- shell: WinForms + WebView2 原生壳(无边框/置顶/固定位置/锁定尺寸/全屏/自适应缩放/截图排除) - shell/ui.html: 自适应界面(工具栏、会话镜像区、底部聊天框、轮/步/tok/s/MTOK/缓存命中/上下文状态条、外观设置) - lib/host.js: DSH 插件 Host 半(endpoint 写入、SSE 会话与指标快照、float_window Agent 工具) - tools: WebView2 SDK 拉取、BOM 修复、Host 冒烟测试 - README: 架构、实测结论(WDA_EXCLUDEFROMCAPTURE、玻璃模式、DPI、WebView2 加载)
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# fix-bom.ps1 - ensure every .ps1 in this repo is UTF-8 WITH BOM.
|
||||
# Windows PowerShell 5.1 reads BOM-less .ps1 as ANSI/GBK, which corrupts non-ASCII
|
||||
# comments and can silently break parsing (observed: variables becoming empty).
|
||||
# ASCII-only on purpose: this file must survive being saved without a BOM.
|
||||
param([string]$Root)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (-not $Root) { $Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) }
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
$fixed = 0
|
||||
Get-ChildItem $Root -Recurse -File -Filter *.ps1 | Where-Object { $_.FullName -notmatch '\\\.cache\\' } | ForEach-Object {
|
||||
$bytes = [System.IO.File]::ReadAllBytes($_.FullName)
|
||||
$hasBom = $bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF
|
||||
if (-not $hasBom) {
|
||||
$text = [System.IO.File]::ReadAllText($_.FullName, [System.Text.Encoding]::UTF8)
|
||||
[System.IO.File]::WriteAllText($_.FullName, $text, $utf8Bom)
|
||||
Write-Host "[bom] + $($_.Name)"
|
||||
$script:fixed++
|
||||
}
|
||||
}
|
||||
Write-Host "[bom] done, fixed=$fixed"
|
||||
@@ -0,0 +1,87 @@
|
||||
<#
|
||||
setup-webview2.ps1 — 拉取 WebView2 SDK(托管 DLL + Loader),供 shell/float-window.ps1 内嵌 Chromium 使用。
|
||||
|
||||
为什么要这么绕:本机 PowerShell 的 schannel 凭据在当前沙箱里不可用
|
||||
(SEC_E_NO_CREDENTIALS),Invoke-WebRequest/curl 走不通;Node 自带 OpenSSL,网络正常。
|
||||
所以优先用 node 下载,并保留 Invoke-WebRequest 作为通用回退。
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Dest,
|
||||
[string]$Cache
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# 在无控制台的宿主(被 harness / 计划任务拉起)里 Write-Progress 会抛 Win32 "Access is denied"
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path | Split-Path -Parent
|
||||
if (-not $Dest) { $Dest = Join-Path $root 'shell\lib' }
|
||||
if (-not $Cache) { $Cache = Join-Path $root '.cache' }
|
||||
New-Item -ItemType Directory -Force -Path $Dest, $Cache | Out-Null
|
||||
|
||||
if (Test-Path (Join-Path $Dest 'Microsoft.Web.WebView2.WinForms.dll')) {
|
||||
Write-Host "[setup] WebView2 SDK 已存在:$Dest"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$nupkg = Join-Path $Cache 'webview2.nupkg'
|
||||
$needDownload = $true
|
||||
if (Test-Path $nupkg) { $needDownload = $false }
|
||||
|
||||
if ($needDownload) {
|
||||
$nodeScript = @'
|
||||
const fs = require('node:fs')
|
||||
const idx = 'https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/index.json'
|
||||
async function main() {
|
||||
const j = await (await fetch(idx, { signal: AbortSignal.timeout(30000) })).json()
|
||||
const stable = j.versions.filter((v) => !v.includes('-'))
|
||||
const v = stable[stable.length - 1]
|
||||
const url = `https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/${v}/microsoft.web.webview2.${v}.nupkg`
|
||||
const buf = Buffer.from(await (await fetch(url, { signal: AbortSignal.timeout(120000) })).arrayBuffer())
|
||||
fs.writeFileSync(process.argv[2], buf)
|
||||
console.log(v)
|
||||
}
|
||||
main().catch((e) => { console.error('download failed: ' + e.message); process.exit(1) })
|
||||
'@
|
||||
$nodeFile = Join-Path $Cache 'download-webview2.cjs'
|
||||
Set-Content -Path $nodeFile -Value $nodeScript -Encoding UTF8
|
||||
|
||||
$version = $null
|
||||
if (Get-Command node -ErrorAction SilentlyContinue) {
|
||||
$version = (& node $nodeFile $nupkg | Select-Object -Last 1)
|
||||
}
|
||||
if (-not (Test-Path $nupkg)) {
|
||||
# 回退:本机 PowerShell 的 TLS 可用时走这里
|
||||
$v = (Invoke-RestMethod 'https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/index.json').versions |
|
||||
Where-Object { $_ -notmatch '-' } | Select-Object -Last 1
|
||||
Invoke-WebRequest "https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/$v/microsoft.web.webview2.$v.nupkg" -OutFile $nupkg
|
||||
$version = $v
|
||||
}
|
||||
Write-Host "[setup] 下载 WebView2 SDK $version"
|
||||
}
|
||||
|
||||
$extract = Join-Path $Cache 'extract'
|
||||
Remove-Item -Recurse -Force $extract -ErrorAction SilentlyContinue
|
||||
# Expand-Archive (PS 5.1) 只认 .zip 扩展名,nupkg 本质就是 zip,复制一份再解
|
||||
$zip = Join-Path $Cache 'webview2.zip'
|
||||
Copy-Item $nupkg $zip -Force
|
||||
Expand-Archive -Path $zip -DestinationPath $extract -Force
|
||||
|
||||
# 包内布局随版本变(1.0.4191.47 是 lib\net462\),按文件名找 + 目录偏好挑选
|
||||
$wanted = @(
|
||||
@{ Leaf = 'Microsoft.Web.WebView2.Core.dll'; Prefer = '\\lib\\net4' },
|
||||
@{ Leaf = 'Microsoft.Web.WebView2.WinForms.dll'; Prefer = '\\lib\\net4' },
|
||||
@{ Leaf = 'WebView2Loader.dll'; Prefer = 'runtimes\\win-x64\\native' }
|
||||
)
|
||||
foreach ($w in $wanted) {
|
||||
$hits = @(Get-ChildItem $extract -Recurse -File -Filter $w.Leaf -ErrorAction SilentlyContinue)
|
||||
if (-not $hits) { throw "包内找不到 $($w.Leaf)" }
|
||||
$pick = @($hits | Where-Object { $_.FullName -match $w.Prefer } | Select-Object -First 1)
|
||||
if (-not $pick) { $pick = @($hits | Select-Object -First 1) }
|
||||
Copy-Item $pick[0].FullName (Join-Path $Dest $w.Leaf) -Force
|
||||
Write-Host "[setup] + $($w.Leaf) <- $($pick[0].FullName.Replace($extract, ''))"
|
||||
}
|
||||
|
||||
$missing = @($wanted | Where-Object { -not (Test-Path (Join-Path $Dest $_.Leaf)) })
|
||||
if ($missing) { throw "缺少文件:$(($missing | ForEach-Object { $_.Leaf }) -join ', ')" }
|
||||
Write-Host "[setup] 完成:$Dest"
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 冒烟测试:用假的 ctx 调 apply(),确认插件不会抛异常,路由能应答。
|
||||
* 用法:node tools/smoke-host.mjs
|
||||
*/
|
||||
import { apply, name, inject } from '../lib/host.js'
|
||||
|
||||
const routes = []
|
||||
const events = []
|
||||
const effects = []
|
||||
|
||||
const ctx = {
|
||||
logger: { info: (...a) => console.log('[mock]', ...a) },
|
||||
webServer: {
|
||||
port: 3099,
|
||||
host: '127.0.0.1',
|
||||
register: (r) => { routes.push(r); return () => { } },
|
||||
},
|
||||
get: () => undefined,
|
||||
set: () => { },
|
||||
on: (ev, fn) => { events.push([ev, fn]); return () => { } },
|
||||
effect: (fn) => { const d = fn(); effects.push(d); return () => { } },
|
||||
}
|
||||
|
||||
console.log('name =', name, '| inject =', JSON.stringify(inject))
|
||||
apply(ctx)
|
||||
console.log('routes =', routes.map((r) => `${r.kind}:${r.path}`).join(', '))
|
||||
console.log('events =', events.map((e) => e[0]).join(', '))
|
||||
console.log('effects =', effects.length)
|
||||
|
||||
function fakeRes() {
|
||||
return {
|
||||
status: 0, headers: null, body: '',
|
||||
writeHead(s, h) { this.status = s; this.headers = h },
|
||||
write(chunk) { this.body += chunk; return true },
|
||||
end(chunk) { if (chunk) this.body += chunk },
|
||||
}
|
||||
}
|
||||
|
||||
const route = routes[0]
|
||||
for (const p of ['/dsh-float-window/health', '/dsh-float-window/state', '/dsh-float-window/nope']) {
|
||||
const res = fakeRes()
|
||||
await route.handler({ method: 'GET', url: p, on: () => { } }, res)
|
||||
console.log(p, '->', res.status, String(res.body).slice(0, 120))
|
||||
}
|
||||
|
||||
// SSE 分支:只验证不抛异常且写出了 data:
|
||||
const res = fakeRes()
|
||||
await route.handler({ method: 'GET', url: '/dsh-float-window/events', on: () => { } }, res)
|
||||
console.log('events ->', res.status, res.headers['Content-Type'], JSON.stringify(res.body.slice(0, 80)))
|
||||
|
||||
console.log('SMOKE OK')
|
||||
Reference in New Issue
Block a user