- 34 push_to_chat 滚动汇总卡: 同 chat 在途事务卡原地追加, ✅/❌ 终态另起新卡, 更新失败回退重绑; L3 过程不再逐条新开卡片刷屏 - 20 伪受理断层: 发票前过 can_accept_request 守卫, 不可启动回实况话术; inflight 移交 watcher 持有, 运行期计数不随 handler 返回归零 - 22 兜底回查限定受理时间戳边界(纯函数+单测), 缺时间戳视为边界外, 禁投旧回答冒充 - 23 watcher 消费本会话 AiError 视同终局投失败卡, 不再空挂 70min - 26 话术改实况: 未受理(暂不入队)可稍后重发, 删假排队承诺 - 27 快任务兜底改受理卡发出后 8s 宽限窗判定, 替换入场恒假死分支 - 21 /status 命令桌面入口: ai_chat_send 拦截, 零 LLM 零 spawn 返回进度摘要 - 38 出站 md 净化: 标题转粗体/表格转列表/围栏保留, feishu_channel 与 df-im-gateway 同规则互引 - 受理卡符号对齐规范(任务开始用🚀, ✅仅完成) - 验收: cargo check 零 error + feishu 13/df-im-gateway 19/chat 17 passed + vue-tsc 0
168 lines
7.8 KiB
PowerShell
168 lines
7.8 KiB
PowerShell
# DevFlow 自宿主换装脚本 v3
|
|
# 流程: 等旧进程退出并探测句柄释放, 静默安装, 按构建产物哈希校验且失败重试一次, 交互用户可见重启, 结束自清理
|
|
$ErrorActionPreference = 'Stop'
|
|
$installer = 'E:\wk-lab\devflow\target\release\bundle\nsis\devflow_0.1.0_x64-setup.exe'
|
|
$target = 'C:\Users\23780\AppData\Local\devflow\devflow.exe'
|
|
$builtExe = 'E:\wk-lab\devflow\target\release\devflow.exe'
|
|
$log = 'E:\wk-lab\devflow\swap-install.log'
|
|
$lockFile = 'E:\wk-lab\devflow\swap-install.lock'
|
|
|
|
# 构建产物哈希是唯一强基准; 产物缺失时退化为与安装前快照比对的弱校验
|
|
$hashWant = $null
|
|
if (Test-Path $builtExe) { $hashWant = (Get-FileHash $builtExe -Algorithm SHA256).Hash }
|
|
|
|
# 0. SYSTEM 上下文转投用户会话
|
|
$who = whoami
|
|
if ($who -like '*SYSTEM*') {
|
|
$loggedUser = (Get-CimInstance Win32_ComputerSystem).UserName
|
|
if ($loggedUser) {
|
|
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$PSCommandPath`""
|
|
$principal = New-ScheduledTaskPrincipal -UserId $loggedUser -LogonType Interactive
|
|
Register-ScheduledTask -TaskName 'DevFlowSwapUser' -Action $action -Principal $principal -Force | Out-Null
|
|
Start-ScheduledTask -TaskName 'DevFlowSwapUser'
|
|
# 锁由重入脚本创建删除, 是其存活的可靠信号; 按 task State 等有 Ready 竞态且超时会腰斩换装
|
|
$deadline = (Get-Date).AddSeconds(30)
|
|
while (-not (Test-Path $lockFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 1 }
|
|
$deadline = (Get-Date).AddSeconds(300)
|
|
while ((Test-Path $lockFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 2 }
|
|
Unregister-ScheduledTask -TaskName 'DevFlowSwapUser' -Confirm:$false -ErrorAction SilentlyContinue
|
|
} else {
|
|
"[FAIL] SYSTEM context and no logged-on user found" | Out-File $log -Encoding utf8
|
|
}
|
|
Unregister-ScheduledTask -TaskName 'DevFlowSwapInstall' -Confirm:$false -ErrorAction SilentlyContinue
|
|
exit 0
|
|
}
|
|
|
|
if (Test-Path $lockFile) {
|
|
$lockAge = (Get-Date) - (Get-Item $lockFile).LastWriteTime
|
|
if ($lockAge.TotalMinutes -lt 5) { exit 0 }
|
|
}
|
|
New-Item $lockFile -Force | Out-Null
|
|
|
|
# 独占打开目标 exe 成功即旧句柄已释放; Windows 文件锁延迟释放会让 NSIS 静默伪成功
|
|
function Wait-HandleReleased {
|
|
if (-not (Test-Path $target)) { return $true }
|
|
$deadline = (Get-Date).AddSeconds(30)
|
|
while ((Get-Date) -lt $deadline) {
|
|
try {
|
|
$fs = [IO.File]::Open($target, 'Open', 'ReadWrite', 'None')
|
|
$fs.Close()
|
|
"[$(Get-Date -Format 'HH:mm:ss')] handle released" | Add-Content $log
|
|
return $true
|
|
} catch {
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
}
|
|
"[$(Get-Date -Format 'HH:mm:ss')] handle wait timeout, continue" | Add-Content $log
|
|
return $false
|
|
}
|
|
|
|
# 静默安装并校验新 exe, 返回换装是否成功
|
|
function Invoke-SwapInstall {
|
|
param([string]$Attempt)
|
|
$p = Start-Process $installer -ArgumentList '/S' -Wait -PassThru
|
|
"[$(Get-Date -Format 'HH:mm:ss')] installer exit=$($p.ExitCode) (attempt $Attempt)" | Add-Content $log
|
|
if ($p.ExitCode -ne 0) { return $false }
|
|
if (-not (Test-Path $target)) {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] FAIL: target missing after install" | Add-Content $log
|
|
return $false
|
|
}
|
|
$afterHash = (Get-FileHash $target -Algorithm SHA256).Hash
|
|
$afterSize = (Get-Item $target).Length
|
|
"[$(Get-Date -Format 'HH:mm:ss')] installed size=$afterSize hash=$($afterHash.Substring(0, 12))" | Add-Content $log
|
|
if ($hashWant) {
|
|
if ($afterHash -ne $hashWant) { return $false }
|
|
} else {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] weak check: built exe absent, compare with pre-install hash" | Add-Content $log
|
|
if ($afterHash -eq $before.hash) { return $false }
|
|
}
|
|
return $true
|
|
}
|
|
|
|
try {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] swap start (user=$(whoami))" | Out-File $log -Encoding utf8
|
|
if ($hashWant) {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] verify baseline: built exe hash=$($hashWant.Substring(0, 12))" | Add-Content $log
|
|
} else {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] verify baseline: weak check (built exe absent)" | Add-Content $log
|
|
}
|
|
|
|
# 1. 等旧进程退出(最多 30s), 超时强杀
|
|
$deadline = (Get-Date).AddSeconds(30)
|
|
while ((Get-Date) -lt $deadline) {
|
|
if (-not (Get-Process devflow -ErrorAction SilentlyContinue)) { break }
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
Get-Process devflow -ErrorAction SilentlyContinue | Stop-Process -Force
|
|
Start-Sleep -Seconds 2
|
|
"[$(Get-Date -Format 'HH:mm:ss')] old process stopped" | Add-Content $log
|
|
|
|
# 2. 探测句柄释放
|
|
Wait-HandleReleased | Out-Null
|
|
|
|
# 3. 安装前快照, 供弱校验与前后比对
|
|
$before = @{ hash = 'absent'; size = 0 }
|
|
if (Test-Path $target) {
|
|
$before.hash = (Get-FileHash $target -Algorithm SHA256).Hash
|
|
$before.size = (Get-Item $target).Length
|
|
}
|
|
$beforeHashShort = $before.hash
|
|
if ($beforeHashShort.Length -gt 12) { $beforeHashShort = $beforeHashShort.Substring(0, 12) }
|
|
"[$(Get-Date -Format 'HH:mm:ss')] before size=$($before.size) hash=$beforeHashShort" | Add-Content $log
|
|
|
|
# 4. 静默安装 + 校验, 校验失败重试一次
|
|
if (Invoke-SwapInstall 'first') {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] verify pass (first attempt)" | Add-Content $log
|
|
} else {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] verify fail, retry once" | Add-Content $log
|
|
Wait-HandleReleased | Out-Null
|
|
if (Invoke-SwapInstall 'retry') {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] verify pass (retry)" | Add-Content $log
|
|
} else {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] FAIL: verify failed twice" | Add-Content $log
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# 5. 重启并验证; GUI 必须起在交互用户会话才可见
|
|
Start-Process $target
|
|
Start-Sleep -Seconds 5
|
|
$proc = Get-Process devflow -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($proc) {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] OK: restarted pid=$($proc.Id)" | Add-Content $log
|
|
exit 0
|
|
}
|
|
|
|
# SYSTEM 会话直启的 GUI 不可见, 转投交互登录用户名下的一次性计划任务
|
|
$explorer = Get-CimInstance Win32_Process -Filter "Name='explorer.exe'" | Select-Object -First 1
|
|
if ($explorer) {
|
|
$o = Invoke-CimMethod -InputObject $explorer -MethodName GetOwner
|
|
$user = "$($o.Domain)\$($o.User)"
|
|
try {
|
|
$action = New-ScheduledTaskAction -Execute $target
|
|
$principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive
|
|
Register-ScheduledTask -TaskName 'DevFlowStartOnce' -Action $action -Principal $principal -Force -ErrorAction Stop | Out-Null
|
|
Start-ScheduledTask -TaskName 'DevFlowStartOnce'
|
|
Start-Sleep -Seconds 5
|
|
} catch {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] start task error: $($_.Exception.Message)" | Add-Content $log
|
|
} finally {
|
|
Unregister-ScheduledTask -TaskName 'DevFlowStartOnce' -Confirm:$false -ErrorAction SilentlyContinue
|
|
}
|
|
$proc = Get-Process devflow -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($proc) {
|
|
"[$(Get-Date -Format 'HH:mm:ss')] OK: restarted as $user pid=$($proc.Id)" | Add-Content $log
|
|
exit 0
|
|
}
|
|
}
|
|
|
|
# 仍无进程, 落标记等人工拉起
|
|
Set-Content -Path 'E:\wk-lab\devflow\swap-need-start.flag' -Value (Get-Date -Format o)
|
|
"[$(Get-Date -Format 'HH:mm:ss')] FAIL: need manual start" | Add-Content $log
|
|
exit 1
|
|
}
|
|
finally {
|
|
Remove-Item $lockFile -ErrorAction SilentlyContinue
|
|
Unregister-ScheduledTask -TaskName 'DevFlowSwapInstall' -Confirm:$false -ErrorAction SilentlyContinue
|
|
}
|