diff --git a/README.md b/README.md index 535b82b..9b9624f 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,9 @@ chmod +x install.sh && ./install.sh # OpenCode 模式:一键生成 opencode.json 并启动 OpenCode + 看板 bash edict.sh opencode +# Windows PowerShell +powershell -ExecutionPolicy Bypass -File .\edict.ps1 opencode + # OpenClaw 模式:一键启动(推荐) chmod +x start.sh && ./start.sh @@ -472,6 +475,7 @@ edict/ │ └── groups/ # Agent 分组(sansheng / liubu) ├── scripts/ │ ├── run_loop.sh # 数据刷新循环(每 15 秒) +│ ├── run_loop_opencode.ps1 # OpenCode 数据刷新循环(Windows PowerShell) │ ├── kanban_update.py # 看板 CLI(含旨意数据清洗 + 标题校验 + 状态机) │ ├── skill_manager.py # Skill 管理工具(远程/本地 Skills 添加、更新、移除) │ ├── agentrec_advisor.py # Agent 模型推荐(功过簿 + 成本优化) @@ -497,6 +501,7 @@ edict/ ├── start.sh # 一键启动(Dashboard + 数据刷新) ├── edict.service # systemd 服务配置(生产部署) ├── edict.sh # 服务管理脚本(start/stop/restart/status) +├── edict.ps1 # Windows 服务管理脚本(opencode/stop/status/logs) ├── CONTRIBUTING.md # 贡献指南 └── LICENSE # MIT License ``` diff --git a/WINDOWS_INSTALL_CN.md b/WINDOWS_INSTALL_CN.md index af4c0ce..4617e69 100644 --- a/WINDOWS_INSTALL_CN.md +++ b/WINDOWS_INSTALL_CN.md @@ -51,6 +51,14 @@ cd C:\Users\\.openclaw\workspace\skills\edict powershell -ExecutionPolicy Bypass -File .\install.ps1 ``` +如果你使用 OpenCode 模式,可以不依赖 Git Bash,直接启动: + +```powershell +powershell -ExecutionPolicy Bypass -File .\edict.ps1 opencode +``` + +这个命令会自动生成 `opencode.json`、同步 `.opencode\prompts\*`,并启动 OpenCode server、看板服务器和后台刷新循环。 + --- ## 4. 安装后检查两件事 @@ -72,7 +80,19 @@ openclaw config set tools.sessions.visibility all --- ## 5. 启动后台刷新循环 -在 Git Bash / MINGW64 里运行: +如果使用 OpenCode 模式,推荐直接运行: + +```powershell +powershell -ExecutionPolicy Bypass -File .\edict.ps1 opencode +``` + +如果使用 OpenClaw 模式,在 PowerShell 里运行: + +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\run_loop.ps1 +``` + +也可以在 Git Bash / MINGW64 里运行旧版脚本: ```bash cd ~/.openclaw/workspace/skills/edict/scripts @@ -84,7 +104,7 @@ bash run_loop.sh --- ## 6. 启动 dashboard -在 PowerShell 里运行: +OpenCode 模式下 `edict.ps1 opencode` 会自动启动 dashboard。OpenClaw 手动模式下,在 PowerShell 里运行: ```powershell cd C:\Users\\.openclaw\workspace\skills\edict @@ -106,7 +126,7 @@ http://127.0.0.1:7891 - 面板可以打开 - `官员总览` 能显示三省六部官员信息 - `模型配置` 能显示 agent 列表 -- 右上角 Gateway 状态正常 +- `省部调度` 里显示 OpenCode 或 OpenClaw Gateway 状态正常 - 倒计时会持续刷新页面数据 --- @@ -199,7 +219,7 @@ openclaw config set tools.sessions.visibility all --- -## 5. 为什么还要跑 `run_loop.sh` +## 5. 为什么还要跑刷新循环 dashboard 右上角虽然有一个 5 秒倒计时,但它只是: @@ -209,8 +229,8 @@ dashboard 右上角虽然有一个 5 秒倒计时,但它只是: 真正负责后台数据刷新的是: -```bash -bash run_loop.sh +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\run_loop.ps1 ``` 它会持续执行同步脚本,更新: @@ -222,9 +242,9 @@ bash run_loop.sh 所以: - dashboard 倒计时 = **读数据** -- `run_loop.sh` = **产数据 / 刷数据** +- `scripts\run_loop.ps1` = **产数据 / 刷数据** -Windows 下也建议正常运行 `run_loop.sh`。 +Windows 下不建议再强依赖 `bash run_loop.sh`;优先使用 `scripts\run_loop.ps1` 或 `edict.ps1 opencode`。 --- @@ -290,8 +310,8 @@ agents.json ## 第四步 启动后台刷新循环: -```bash -bash run_loop.sh +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\run_loop.ps1 ``` ## 第五步 @@ -301,10 +321,17 @@ bash run_loop.sh python dashboard\server.py ``` +如果使用 OpenCode,则第四步和第五步可以合并为: + +```powershell +powershell -ExecutionPolicy Bypass -File .\edict.ps1 opencode +``` + --- # 五、一句话总结 ## Windows 用户最稳的做法就是: -先清旧链接,再运行安装脚本;安装后检查 agent 配置和 `tools.sessions.visibility = all`;如有需要可参考 `agents.json` 脱敏模板并替换 ``;最后启动 `run_loop.sh` 和 `dashboard/server.py`。 +OpenCode 模式:直接运行 `powershell -ExecutionPolicy Bypass -File .\edict.ps1 opencode`。 +OpenClaw 模式:先清旧链接,再运行安装脚本;安装后检查 agent 配置和 `tools.sessions.visibility = all`;如有需要可参考 `agents.json` 脱敏模板并替换 ``;最后启动 `scripts\run_loop.ps1` 和 `dashboard\server.py`。 diff --git a/docs/getting-started.md b/docs/getting-started.md index 3df7e53..ba91861 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -9,10 +9,10 @@ 三省六部现在支持 OpenCode 和 [OpenClaw](https://openclaw.ai) 两种运行时。新项目本地体验推荐 OpenCode;已有 OpenClaw 工作区可继续沿用。 ```bash -# OpenCode(推荐) +# OpenCode(macOS / Linux) curl -fsSL https://opencode.ai/install | bash -# OpenCode(Node.js 方式) +# OpenCode(Windows PowerShell 推荐先装 Node.js 18+,再用 npm) npm install -g opencode-ai # OpenClaw(macOS) @@ -67,6 +67,9 @@ openclaw channels add --type feishu --agent taizi # OpenCode 模式:一键启动 OpenCode server、看板和数据循环 bash edict.sh opencode +# Windows PowerShell +powershell -ExecutionPolicy Bypass -File .\edict.ps1 opencode + # OpenClaw 模式:终端 1,数据刷新循环(每 15 秒同步) bash scripts/run_loop.sh diff --git a/edict.ps1 b/edict.ps1 new file mode 100644 index 0000000..0d01223 --- /dev/null +++ b/edict.ps1 @@ -0,0 +1,133 @@ +<# +.SYNOPSIS + 三省六部 Windows 服务管理脚本 +.DESCRIPTION + PowerShell 入口,对应 edict.sh。 + 用法: powershell -ExecutionPolicy Bypass -File .\edict.ps1 {opencode|stop|status|logs} +#> +#Requires -Version 5.1 +param( + [ValidateSet("opencode", "stop", "status", "logs")] + [string]$Command, + [ValidateSet("server", "loop", "opencode", "all")] + [string]$Target = "all" +) + +$ErrorActionPreference = "Continue" + +$RepoDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$PidDir = Join-Path $RepoDir ".pids" +$LogDir = Join-Path $RepoDir "logs" +$ServerPidFile = Join-Path $PidDir "server.pid" +$LoopPidFile = Join-Path $PidDir "loop.pid" +$OpenCodePidFile = Join-Path $PidDir "opencode.pid" +$DashboardHost = if ($env:EDICT_DASHBOARD_HOST) { $env:EDICT_DASHBOARD_HOST } else { "127.0.0.1" } +$DashboardPort = if ($env:EDICT_DASHBOARD_PORT) { [int]$env:EDICT_DASHBOARD_PORT } else { 7891 } + +function Is-Running([string]$PidFile) { + if (-not (Test-Path $PidFile)) { return $false } + $pidText = Get-Content $PidFile -ErrorAction SilentlyContinue + if (-not ($pidText -match '^\d+$')) { + Remove-Item $PidFile -Force -ErrorAction SilentlyContinue + return $false + } + $p = Get-Process -Id ([int]$pidText) -ErrorAction SilentlyContinue + if ($p) { return $true } + Remove-Item $PidFile -Force -ErrorAction SilentlyContinue + return $false +} + +function Get-PidText([string]$PidFile) { + if (Test-Path $PidFile) { return (Get-Content $PidFile -ErrorAction SilentlyContinue) } + return "" +} + +function Stop-PidFile([string]$PidFile) { + if (-not (Test-Path $PidFile)) { return $false } + $pidText = Get-Content $PidFile -ErrorAction SilentlyContinue + if ($pidText -and ($pidText -match '^\d+$')) { + try { Stop-Process -Id ([int]$pidText) -Force -ErrorAction SilentlyContinue } catch {} + } + Remove-Item $PidFile -Force -ErrorAction SilentlyContinue + return $true +} + +function Show-Status { + Write-Host "三省六部 · Windows 服务状态" -ForegroundColor Blue + Write-Host "" + foreach ($item in @( + @{ Name = "看板服务器"; File = $ServerPidFile }, + @{ Name = "数据刷新循环"; File = $LoopPidFile }, + @{ Name = "OpenCode server"; File = $OpenCodePidFile } + )) { + if (Is-Running $item.File) { + Write-Host " ● $($item.Name) PID=$(Get-PidText $item.File) 运行中" -ForegroundColor Green + } else { + Write-Host " ○ $($item.Name) 未运行" -ForegroundColor Red + } + } + Write-Host "" + try { + $health = Invoke-RestMethod -Uri "http://${DashboardHost}:${DashboardPort}/healthz" -TimeoutSec 3 + if ($health.status -eq "ok") { + Write-Host " 健康检查: 正常" -ForegroundColor Green + } else { + Write-Host " 健康检查: degraded" -ForegroundColor Yellow + } + Write-Host " 看板地址: http://${DashboardHost}:${DashboardPort}" -ForegroundColor Blue + } catch { + Write-Host " 健康检查: 无法连接" -ForegroundColor Red + } +} + +function Stop-All { + Write-Host "正在关闭服务..." -ForegroundColor Yellow + $stopped = 0 + foreach ($item in @( + @{ Name = "看板服务器"; File = $ServerPidFile }, + @{ Name = "数据刷新循环"; File = $LoopPidFile }, + @{ Name = "OpenCode server"; File = $OpenCodePidFile } + )) { + if (Stop-PidFile $item.File) { + Write-Host " 已停止 $($item.Name)" -ForegroundColor Green + $stopped += 1 + } + } + Remove-Item (Join-Path $env:TEMP "sansheng_liubu_opencode_refresh.pid") -Force -ErrorAction SilentlyContinue + if ($stopped -eq 0) { + Write-Host " 没有发现由本脚本管理的服务" -ForegroundColor Yellow + } +} + +function Show-Logs([string]$Which) { + New-Item -ItemType Directory -Path $LogDir -Force | Out-Null + $files = switch ($Which) { + "server" { @("server.log", "server.err.log") } + "loop" { @("loop.log", "loop.err.log") } + "opencode" { @("opencode.log", "opencode.err.log") } + default { @("server.log", "server.err.log", "loop.log", "loop.err.log", "opencode.log", "opencode.err.log") } + } + $paths = $files | ForEach-Object { Join-Path $LogDir $_ } | Where-Object { Test-Path $_ } + if (-not $paths) { + Write-Host "暂无日志文件: $LogDir" -ForegroundColor Yellow + return + } + Get-Content -Path $paths -Wait -Tail 80 +} + +switch ($Command) { + "opencode" { + powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $RepoDir "scripts\start_opencode.ps1") + } + "stop" { Stop-All } + "status" { Show-Status } + "logs" { Show-Logs $Target } + default { + Write-Host "用法: powershell -ExecutionPolicy Bypass -File .\edict.ps1 {opencode|stop|status|logs}" -ForegroundColor Yellow + Write-Host " opencode 启动 OpenCode 模式" + Write-Host " stop 停止脚本管理的服务" + Write-Host " status 查看状态" + Write-Host " logs 查看日志,可选 server|loop|opencode|all" + exit 1 + } +} diff --git a/install.ps1 b/install.ps1 index ce24f79..0b3cbbc 100644 --- a/install.ps1 +++ b/install.ps1 @@ -303,9 +303,12 @@ Write-Host "下一步:" Write-Host " 1. 配置 API Key(如尚未配置):" Write-Host " openclaw agents add taizi # 按提示输入 Anthropic API Key" Write-Host " .\install.ps1 # 重新运行以同步到所有 Agent" -Write-Host " 2. 启动数据刷新循环: bash scripts/run_loop.sh" +Write-Host " 2. 启动数据刷新循环: powershell -ExecutionPolicy Bypass -File .\scripts\run_loop.ps1" Write-Host " 3. 启动看板服务器: python dashboard/server.py" Write-Host " 4. 打开看板: http://127.0.0.1:7891" Write-Host "" +Write-Host " OpenCode 模式可直接运行:" +Write-Host " powershell -ExecutionPolicy Bypass -File .\edict.ps1 opencode" +Write-Host "" Warn "首次安装必须配置 API Key,否则 Agent 会报错" Info "文档: docs/getting-started.md" diff --git a/scripts/run_loop_opencode.ps1 b/scripts/run_loop_opencode.ps1 new file mode 100644 index 0000000..f5e9a0a --- /dev/null +++ b/scripts/run_loop_opencode.ps1 @@ -0,0 +1,128 @@ +<# +.SYNOPSIS + 三省六部 OpenCode 数据刷新循环 (Windows PowerShell 版本) +.DESCRIPTION + run_loop_opencode.sh 的 Windows 等效脚本。 + 用法: .\scripts\run_loop_opencode.ps1 [-Interval 15] [-ScanInterval 120] +#> +#Requires -Version 5.1 +param( + [int]$Interval = 15, + [int]$ScanInterval = 120 +) + +$ErrorActionPreference = "Continue" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoDir = Split-Path -Parent $ScriptDir +$env:EDICT_HOME = if ($env:EDICT_HOME) { $env:EDICT_HOME } else { $RepoDir } +$env:EDICT_RUNTIME = "opencode" +$env:EDICT_AGENT_RUNTIME = "opencode" + +$Log = Join-Path $env:TEMP "sansheng_liubu_opencode_refresh.log" +$PidFile = Join-Path $env:TEMP "sansheng_liubu_opencode_refresh.pid" +$MaxLogSize = 10MB +$ScriptTimeout = 30 +$DashboardPort = if ($env:EDICT_DASHBOARD_PORT) { $env:EDICT_DASHBOARD_PORT } else { "7891" } + +function Resolve-Python { + if ($env:EDICT_PYTHON) { + $cmd = Get-Command $env:EDICT_PYTHON -ErrorAction SilentlyContinue + if ($cmd) { return @{ File = $cmd.Source; Prefix = @() } } + } + foreach ($candidate in @("python", "python3")) { + $cmd = Get-Command $candidate -ErrorAction SilentlyContinue + if ($cmd) { return @{ File = $cmd.Source; Prefix = @() } } + } + $py = Get-Command py -ErrorAction SilentlyContinue + if ($py) { return @{ File = $py.Source; Prefix = @("-3") } } + throw "未找到 python / python3 / py。请先安装 Python 3.10+。" +} + +$Python = Resolve-Python + +if (Test-Path $PidFile) { + $OldPid = Get-Content $PidFile -ErrorAction SilentlyContinue + if ($OldPid -and (Get-Process -Id $OldPid -ErrorAction SilentlyContinue)) { + Write-Host "已有 OpenCode 刷新循环运行中 (PID=$OldPid),退出" + exit 1 + } + Remove-Item $PidFile -Force -ErrorAction SilentlyContinue +} +$PID | Out-File $PidFile -Encoding ASCII + +$cleanup = { + "$(Get-Date -Format HH:mm:ss) [opencode-loop] Shutting down..." | Out-File $Log -Append -Encoding UTF8 + Remove-Item $PidFile -Force -ErrorAction SilentlyContinue +} +Register-EngineEvent PowerShell.Exiting -Action $cleanup | Out-Null + +function Rotate-Log { + if (Test-Path $Log) { + $size = (Get-Item $Log).Length + if ($size -gt $MaxLogSize) { + Move-Item $Log "$Log.1" -Force + "$(Get-Date -Format HH:mm:ss) [opencode-loop] Log rotated" | Out-File $Log -Encoding UTF8 + } + } +} + +function Safe-Run([string]$Script) { + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $Python.File + $args = @($Python.Prefix) + @($Script) + $psi.Arguments = ($args | ForEach-Object { '"' + ($_ -replace '"', '\"') + '"' }) -join " " + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.UseShellExecute = $false + $psi.WorkingDirectory = $RepoDir + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $psi + $process.Start() | Out-Null + + if (-not $process.WaitForExit($ScriptTimeout * 1000)) { + try { + $process.Kill() + "$(Get-Date -Format HH:mm:ss) [opencode-loop] Script timeout (${ScriptTimeout}s): $Script" | Out-File $Log -Append -Encoding UTF8 + } catch {} + } + + $stdout = $process.StandardOutput.ReadToEnd() + $stderr = $process.StandardError.ReadToEnd() + if ($stdout) { $stdout | Out-File $Log -Append -Encoding UTF8 } + if ($stderr) { $stderr | Out-File $Log -Append -Encoding UTF8 } +} + +function Invoke-SchedulerScan { + try { + Invoke-RestMethod -Uri "http://127.0.0.1:$DashboardPort/api/scheduler-scan" ` + -Method POST ` + -ContentType "application/json" ` + -Body '{"thresholdSec":180}' ` + -TimeoutSec 5 | Out-Null + } catch { + $_ | Out-File $Log -Append -Encoding UTF8 + } +} + +Write-Host "三省六部 OpenCode 数据刷新循环启动 (PID=$PID)" +Write-Host " Script dir: $ScriptDir" +Write-Host " Interval: ${Interval}s Scan: ${ScanInterval}s Timeout: ${ScriptTimeout}s" +Write-Host " Log: $Log" +Write-Host " Ctrl+C to stop" + +$ScanCounter = 0 +while ($true) { + Rotate-Log + Safe-Run (Join-Path $ScriptDir "sync_opencode_agents.py") + Safe-Run (Join-Path $ScriptDir "refresh_live_data.py") + + $ScanCounter += $Interval + if ($ScanCounter -ge $ScanInterval) { + $ScanCounter = 0 + Invoke-SchedulerScan + } + + Start-Sleep -Seconds $Interval +} diff --git a/scripts/start_opencode.ps1 b/scripts/start_opencode.ps1 new file mode 100644 index 0000000..272a032 --- /dev/null +++ b/scripts/start_opencode.ps1 @@ -0,0 +1,261 @@ +<# +.SYNOPSIS + 三省六部 OpenCode 一键启动 (Windows PowerShell 版本) +.DESCRIPTION + 生成 OpenCode agent 配置,启动 OpenCode server、dashboard/server.py 和数据刷新循环。 + 用法: powershell -ExecutionPolicy Bypass -File .\scripts\start_opencode.ps1 +#> +#Requires -Version 5.1 + +$ErrorActionPreference = "Stop" + +$RepoDir = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$DashboardHost = if ($env:EDICT_DASHBOARD_HOST) { $env:EDICT_DASHBOARD_HOST } else { "127.0.0.1" } +$DashboardPort = if ($env:EDICT_DASHBOARD_PORT) { [int]$env:EDICT_DASHBOARD_PORT } else { 7891 } +$OpenCodeHost = if ($env:OPENCODE_HOST) { $env:OPENCODE_HOST } else { "127.0.0.1" } +$OpenCodePort = if ($env:OPENCODE_PORT) { [int]$env:OPENCODE_PORT } else { 4096 } +$OpenCodeServerUrl = if ($env:OPENCODE_SERVER_URL) { $env:OPENCODE_SERVER_URL.TrimEnd("/") } else { "http://${OpenCodeHost}:${OpenCodePort}" } + +$PidDir = Join-Path $RepoDir ".pids" +$LogDir = Join-Path $RepoDir "logs" +$ServerPidFile = Join-Path $PidDir "server.pid" +$LoopPidFile = Join-Path $PidDir "loop.pid" +$OpenCodePidFile = Join-Path $PidDir "opencode.pid" +$ServerLog = Join-Path $LogDir "server.log" +$ServerErrLog = Join-Path $LogDir "server.err.log" +$LoopLog = Join-Path $LogDir "loop.log" +$LoopErrLog = Join-Path $LogDir "loop.err.log" +$OpenCodeLog = Join-Path $LogDir "opencode.log" +$OpenCodeErrLog = Join-Path $LogDir "opencode.err.log" + +function Log($msg) { Write-Host "✅ $msg" -ForegroundColor Green } +function Info($msg) { Write-Host "▶ $msg" -ForegroundColor Green } +function Warn($msg) { Write-Host "⚠️ $msg" -ForegroundColor Yellow } +function Fail($msg) { Write-Host "❌ $msg" -ForegroundColor Red; exit 1 } + +function Resolve-Python { + if ($env:EDICT_PYTHON) { + $cmd = Get-Command $env:EDICT_PYTHON -ErrorAction SilentlyContinue + if ($cmd) { return @{ File = $cmd.Source; Prefix = @() } } + } + foreach ($candidate in @("python", "python3")) { + $cmd = Get-Command $candidate -ErrorAction SilentlyContinue + if ($cmd) { + $version = & $cmd.Source -c "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}')" 2>$null + if ($LASTEXITCODE -eq 0) { + $parts = $version.Split(".") + if ([int]$parts[0] -gt 3 -or ([int]$parts[0] -eq 3 -and [int]$parts[1] -ge 10)) { + return @{ File = $cmd.Source; Prefix = @() } + } + } + } + } + $py = Get-Command py -ErrorAction SilentlyContinue + if ($py) { + $version = & $py.Source -3 -c "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}')" 2>$null + if ($LASTEXITCODE -eq 0) { return @{ File = $py.Source; Prefix = @("-3") } } + } + throw "未找到 Python 3.10+。" +} + +function Resolve-OpenCode { + if ($env:OPENCODE_BIN) { + $cmd = Get-Command $env:OPENCODE_BIN -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + if (Test-Path $env:OPENCODE_BIN) { return $env:OPENCODE_BIN } + } + $cmd = Get-Command opencode -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + foreach ($p in @( + (Join-Path $env:USERPROFILE ".opencode\bin\opencode.exe"), + (Join-Path $env:USERPROFILE ".opencode\bin\opencode.cmd"), + (Join-Path $env:USERPROFILE ".opencode\bin\opencode") + )) { + if (Test-Path $p) { return $p } + } + throw "未找到 OpenCode CLI。请先安装 opencode,或设置 OPENCODE_BIN。" +} + +function Join-Args([string[]]$Args) { + return ($Args | ForEach-Object { '"' + ($_ -replace '"', '\"') + '"' }) -join " " +} + +function Invoke-Python([string[]]$Args) { + & $script:Python.File @($script:Python.Prefix + $Args) +} + +function Test-HttpOk([string]$Url) { + try { + $r = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 3 + return ($r.StatusCode -ge 200 -and $r.StatusCode -lt 500) + } catch { + return $false + } +} + +function Test-OpenCodeAgents { + try { + $dir = [System.Uri]::EscapeDataString($RepoDir) + $agents = Invoke-RestMethod -Uri "$OpenCodeServerUrl/agent?directory=$dir" -TimeoutSec 5 + $names = @($agents | ForEach-Object { $_.name }) + return ($names -contains "taizi" -and $names -contains "zhongshu" -and $names -contains "shangshu") + } catch { + return $false + } +} + +function Get-PortPids([int]$Port) { + try { + return @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique) + } catch { + return @() + } +} + +function Stop-PidFile([string]$PidFile) { + if (-not (Test-Path $PidFile)) { return } + $pidText = Get-Content $PidFile -ErrorAction SilentlyContinue + if ($pidText -and ($pidText -match '^\d+$')) { + $p = Get-Process -Id ([int]$pidText) -ErrorAction SilentlyContinue + if ($p) { + try { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } catch {} + } + } + Remove-Item $PidFile -Force -ErrorAction SilentlyContinue +} + +function Stop-OpenCodeOnPort { + foreach ($pid in Get-PortPids $OpenCodePort) { + $cmd = (Get-CimInstance Win32_Process -Filter "ProcessId=$pid" -ErrorAction SilentlyContinue).CommandLine + if ($cmd -and $cmd.ToLowerInvariant().Contains("opencode")) { + Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue + } + } +} + +function Stop-RepoDashboardOnPort { + foreach ($pid in Get-PortPids $DashboardPort) { + $cmd = (Get-CimInstance Win32_Process -Filter "ProcessId=$pid" -ErrorAction SilentlyContinue).CommandLine + if ($cmd -and $cmd.Contains("dashboard") -and $cmd.Contains("server.py")) { + Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue + } + } +} + +function Ensure-DataFiles { + New-Item -ItemType Directory -Path $PidDir, $LogDir, (Join-Path $RepoDir "data") -Force | Out-Null + foreach ($f in @("live_status.json","agent_config.json","model_change_log.json","sync_status.json","officials_stats.json")) { + $fp = Join-Path $RepoDir "data\$f" + if (-not (Test-Path $fp)) { "{}" | Out-File $fp -Encoding UTF8 } + } + foreach ($f in @("pending_model_changes.json","tasks_source.json","tasks.json","officials.json")) { + $fp = Join-Path $RepoDir "data\$f" + if (-not (Test-Path $fp)) { "[]" | Out-File $fp -Encoding UTF8 } + } +} + +function Start-OpenCodeServer { + $args = @("serve", "--hostname", $OpenCodeHost, "--port", [string]$OpenCodePort) + $p = Start-Process -FilePath $OpenCodeBin -ArgumentList (Join-Args $args) ` + -WorkingDirectory $RepoDir ` + -RedirectStandardOutput $OpenCodeLog ` + -RedirectStandardError $OpenCodeErrLog ` + -WindowStyle Hidden ` + -PassThru + $p.Id | Out-File $OpenCodePidFile -Encoding ASCII +} + +function Start-DashboardServer { + $args = @($Python.Prefix) + @((Join-Path $RepoDir "dashboard\server.py"), "--host", $DashboardHost, "--port", [string]$DashboardPort) + $p = Start-Process -FilePath $Python.File -ArgumentList (Join-Args $args) ` + -WorkingDirectory $RepoDir ` + -RedirectStandardOutput $ServerLog ` + -RedirectStandardError $ServerErrLog ` + -WindowStyle Hidden ` + -PassThru + $p.Id | Out-File $ServerPidFile -Encoding ASCII +} + +function Start-RefreshLoop { + $script = Join-Path $RepoDir "scripts\run_loop_opencode.ps1" + $args = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $script) + $p = Start-Process -FilePath "powershell" -ArgumentList (Join-Args $args) ` + -WorkingDirectory $RepoDir ` + -RedirectStandardOutput $LoopLog ` + -RedirectStandardError $LoopErrLog ` + -WindowStyle Hidden ` + -PassThru + $p.Id | Out-File $LoopPidFile -Encoding ASCII +} + +Write-Host "" +Write-Host "╔══════════════════════════════════════════╗" -ForegroundColor Blue +Write-Host "║ 三省六部 · OpenCode Windows 启动中 ║" -ForegroundColor Blue +Write-Host "╚══════════════════════════════════════════╝" -ForegroundColor Blue +Write-Host "" + +try { $script:Python = Resolve-Python } catch { Fail $_ } +try { $script:OpenCodeBin = Resolve-OpenCode } catch { Fail $_ } + +$env:EDICT_RUNTIME = "opencode" +$env:EDICT_AGENT_RUNTIME = "opencode" +$env:EDICT_PYTHON = $Python.File +$env:OPENCODE_BIN = $OpenCodeBin +$env:OPENCODE_SERVER_URL = $OpenCodeServerUrl + +Ensure-DataFiles + +Write-Host "Python: $($Python.File)" -ForegroundColor Green +Write-Host "OpenCode: $OpenCodeBin" -ForegroundColor Green + +Info "同步 OpenCode agent 配置..." +Invoke-Python @((Join-Path $RepoDir "scripts\sync_opencode_agents.py")) + +if ((Test-HttpOk "$OpenCodeServerUrl/doc") -and (Test-OpenCodeAgents)) { + Log "复用已运行的 OpenCode server: $OpenCodeServerUrl" +} else { + if (Test-HttpOk "$OpenCodeServerUrl/doc") { + Warn "当前 $OpenCodeServerUrl 不是本项目的 OpenCode server,正在切换..." + Stop-OpenCodeOnPort + Stop-PidFile $OpenCodePidFile + Start-Sleep -Milliseconds 500 + } + Info "启动 OpenCode server..." + Stop-PidFile $OpenCodePidFile + Start-OpenCodeServer + for ($i = 0; $i -lt 20; $i++) { + if (Test-HttpOk "$OpenCodeServerUrl/doc") { break } + Start-Sleep -Milliseconds 500 + } + if (-not ((Test-HttpOk "$OpenCodeServerUrl/doc") -and (Test-OpenCodeAgents))) { + Fail "OpenCode server 启动失败,请查看日志: $OpenCodeLog / $OpenCodeErrLog" + } +} + +Info "切换看板到 OpenCode 运行时..." +Stop-PidFile $ServerPidFile +Stop-PidFile $LoopPidFile +Stop-RepoDashboardOnPort +Remove-Item (Join-Path $env:TEMP "sansheng_liubu_opencode_refresh.pid") -Force -ErrorAction SilentlyContinue +Start-Sleep -Milliseconds 500 + +Start-DashboardServer +Start-RefreshLoop + +for ($i = 0; $i -lt 20; $i++) { + if (Test-HttpOk "http://${DashboardHost}:${DashboardPort}/healthz") { break } + Start-Sleep -Milliseconds 500 +} +if (-not (Test-HttpOk "http://${DashboardHost}:${DashboardPort}/healthz")) { + Fail "看板启动失败,请查看日志: $ServerLog / $ServerErrLog" +} + +Write-Host "" +Log "OpenCode 模式已启动" +Write-Host " 看板: http://${DashboardHost}:${DashboardPort}" -ForegroundColor Blue +Write-Host " OpenCode: $OpenCodeServerUrl" -ForegroundColor Blue +Write-Host " 日志: $LogDir" -ForegroundColor Blue + +try { + Start-Process "http://${DashboardHost}:${DashboardPort}" | Out-Null +} catch {}