feat(installer): revamp install flow with i18n

This commit is contained in:
zhayujie
2026-05-31 20:11:23 +08:00
parent 7bf4ef3d05
commit 9e6a2cc2c0
6 changed files with 1066 additions and 444 deletions

View File

@@ -118,9 +118,11 @@ def run_install_browser(
0 on success, 1 on fatal failure (pip or chromium install failed). 0 on success, 1 on fatal failure (pip or chromium install failed).
""" """
from cli.utils import get_cli_language from cli.utils import get_cli_language
from common import i18n
# Import `common` only after get_cli_language() runs ensure_sys_path(),
# so it works when `cow` is invoked from outside the project directory.
get_cli_language() # resolve cow_lang so i18n.t reflects config get_cli_language() # resolve cow_lang so i18n.t reflects config
from common import i18n
_t = i18n.t _t = i18n.t
stream = stream or _default_stream stream = stream or _default_stream

View File

@@ -276,9 +276,12 @@ def status():
"""Show CowAgent running status.""" """Show CowAgent running status."""
from cli import __version__ from cli import __version__
from cli.utils import load_config_json, get_cli_language from cli.utils import load_config_json, get_cli_language
from common import i18n
# get_cli_language() calls ensure_sys_path(), which adds the project root
# to sys.path. Import `common` only AFTER that, otherwise it fails with
# ModuleNotFoundError when `cow` runs from outside the project dir.
get_cli_language() # resolve cow_lang so i18n.t reflects config get_cli_language() # resolve cow_lang so i18n.t reflects config
from common import i18n
_t = i18n.t _t = i18n.t
pid = _read_pid() pid = _read_pid()

View File

@@ -518,9 +518,11 @@ def _install_targz_bytes(content: bytes, name: str, skills_dir: str, result: Ins
def _print_install_success(name: str, source: str): def _print_install_success(name: str, source: str):
"""Print a unified install success message with description and source.""" """Print a unified install success message with description and source."""
from cli.utils import get_cli_language from cli.utils import get_cli_language
from common import i18n
# Import `common` only after get_cli_language() runs ensure_sys_path(),
# so it works when `cow` is invoked from outside the project directory.
get_cli_language() # resolve cow_lang so i18n.t reflects config get_cli_language() # resolve cow_lang so i18n.t reflects config
from common import i18n
_t = i18n.t _t = i18n.t
skills_dir = get_skills_dir() skills_dir = get_skills_dir()

View File

@@ -1,6 +1,6 @@
{ {
"cow_lang": "auto", "cow_lang": "auto",
"channel_type": "weixin", "channel_type": "web",
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash",
"deepseek_api_key": "", "deepseek_api_key": "",
"deepseek_api_base": "https://api.deepseek.com/v1", "deepseek_api_base": "https://api.deepseek.com/v1",

851
run.sh

File diff suppressed because it is too large Load Diff

View File

@@ -18,10 +18,19 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# ── ensure UTF-8 console encoding on Windows ───────────────────── # ── ensure UTF-8 everywhere on Windows ───────────────────────────
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # Without this, Chinese text renders as mojibake (e.g. "éæ©") on Windows
# PowerShell 5.1, whose console defaults to the system ANSI code page (GBK on
# Chinese systems). Set the active code page AND the console encodings so both
# our output and any child process (git/python) speak UTF-8.
try { chcp 65001 | Out-Null } catch {}
try {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
} catch {}
# $OutputEncoding controls how strings are piped to external programs.
$OutputEncoding = [System.Text.Encoding]::UTF8
$env:PYTHONIOENCODING = "utf-8" $env:PYTHONIOENCODING = "utf-8"
chcp 65001 | Out-Null
# ── colours ────────────────────────────────────────────────────── # ── colours ──────────────────────────────────────────────────────
function Write-Cow { param([string]$M) Write-Host $M -ForegroundColor Green } function Write-Cow { param([string]$M) Write-Host $M -ForegroundColor Green }
@@ -29,6 +38,31 @@ function Write-Warn { param([string]$M) Write-Host $M -ForegroundColor Yellow }
function Write-Err { param([string]$M) Write-Host $M -ForegroundColor Red } function Write-Err { param([string]$M) Write-Host $M -ForegroundColor Red }
function Write-Info { param([string]$M) Write-Host $M -ForegroundColor Cyan } function Write-Info { param([string]$M) Write-Host $M -ForegroundColor Cyan }
# ── i18n: install-flow language ──────────────────────────────────
# $UiLang controls the language of install prompts/menus ("zh" or "en").
# Chosen by the user at the first step; defaults to environment detection
# for management commands (start/stop/...).
$script:UiLang = ""
# Detect default UI language from the OS culture (best-effort). Checks the
# display/UI culture first (closest to the user's chosen Windows language),
# then the regional format culture as a fallback. Any zh-* signal -> "zh".
function Get-DefaultUiLang {
foreach ($getter in @({ (Get-UICulture).Name }, { (Get-Culture).Name })) {
try {
$name = & $getter
if ($name -match '^zh') { return "zh" }
} catch {}
}
return "en"
}
# Translation helper: T <zh> <en> -> string in the active UI language.
function T {
param([string]$Zh, [string]$En)
if ($script:UiLang -eq "en") { return $En } else { return $Zh }
}
# ── detect project directory ───────────────────────────────────── # ── detect project directory ─────────────────────────────────────
$ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path } $ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path }
$BaseDir = Split-Path $ScriptDir -Parent $BaseDir = Split-Path $ScriptDir -Parent
@@ -39,6 +73,125 @@ if (-not $IsProjectDir) {
$IsProjectDir = (Test-Path "$BaseDir\app.py") -and (Test-Path "$BaseDir\config-template.json") $IsProjectDir = (Test-Path "$BaseDir\app.py") -and (Test-Path "$BaseDir\config-template.json")
} }
# Initialize $UiLang for management commands: prefer cow_lang from an existing
# config.json, otherwise fall back to environment detection.
function Initialize-UiLang {
if ($script:UiLang) { return }
$cfgLang = ""
if (Test-Path "$BaseDir\config.json") {
try {
$cfg = Get-Content "$BaseDir\config.json" -Raw | ConvertFrom-Json
if ($cfg.cow_lang) { $cfgLang = "$($cfg.cow_lang)" }
} catch {}
}
switch ($cfgLang) {
"zh" { $script:UiLang = "zh" }
"en" { $script:UiLang = "en" }
default { $script:UiLang = Get-DefaultUiLang }
}
}
# ── arrow-key selectable menu with number fallback ───────────────
# Usage: $idx = Select-Menu -Title "..." -Options @("a","b") [-Default 1]
# Returns the selected 1-based index.
function Select-Menu {
param(
[string]$Title,
[string[]]$Options,
[int]$Default = 1
)
$count = $Options.Count
$cur = [Math]::Max(0, [Math]::Min($Default - 1, $count - 1))
# Fallback to numbered input when there is no interactive console
# (e.g. piped input, redirected host).
$interactive = $true
try {
if ([Console]::IsInputRedirected) { $interactive = $false }
} catch { $interactive = $false }
if (-not $interactive) {
Write-Info $Title
for ($i = 0; $i -lt $count; $i++) {
Write-Host (" {0}) {1}" -f ($i + 1), $Options[$i])
}
do {
$sel = Read-Host (T "请输入序号" "Enter number")
if (-not $sel) { $sel = "$($cur + 1)" }
} while ($sel -notmatch '^\d+$' -or [int]$sel -lt 1 -or [int]$sel -gt $count)
return [int]$sel
}
Write-Info $Title
Write-Host (T "↑/↓ 选择Enter 确认" "Use ↑/↓ to move, Enter to select") -ForegroundColor Cyan
[Console]::CursorVisible = $false
$firstDraw = $true
try {
while ($true) {
if (-not $firstDraw) {
# Move cursor up to the top of the option block to redraw it.
$top = [Console]::CursorTop - $count
if ($top -lt 0) { $top = 0 }
[Console]::SetCursorPosition(0, $top)
}
$firstDraw = $false
for ($i = 0; $i -lt $count; $i++) {
# Clear the line first to avoid leftover characters.
Write-Host (" " * ([Console]::WindowWidth - 1)) -NoNewline
[Console]::SetCursorPosition(0, [Console]::CursorTop)
if ($i -eq $cur) {
Write-Host (" > " + $Options[$i]) -ForegroundColor Green
} else {
Write-Host (" " + $Options[$i])
}
}
$key = [Console]::ReadKey($true)
switch ($key.Key) {
"UpArrow" { $cur = (($cur - 1 + $count) % $count) }
"DownArrow" { $cur = (($cur + 1) % $count) }
"Enter" { return ($cur + 1) }
default {
# Number shortcut (1-9) jumps to that option and confirms.
$ch = $key.KeyChar
if ($ch -match '^[1-9]$') {
$n = [int]"$ch"
if ($n -ge 1 -and $n -le $count) { return $n }
}
}
}
}
} finally {
[Console]::CursorVisible = $true
}
}
# ── language selection (first step of install) ───────────────────
function Select-Language {
# Order is fixed (English first, Chinese second). The default highlight
# follows detection, but conservatively: only a confident "zh" signal
# (a zh-* system culture) preselects Chinese; everything else defaults to
# English. The menu hint shows in the detected language for familiarity.
$detected = Get-DefaultUiLang
if ($detected -eq "zh") {
$default = 2
$script:UiLang = "zh"
} else {
$default = 1
$script:UiLang = "en"
}
$idx = Select-Menu -Title "Select Language / 选择语言" -Options @("English", "中文 (Chinese)") -Default $default
switch ($idx) {
1 { $script:UiLang = "en" }
2 { $script:UiLang = "zh" }
default { $script:UiLang = "en" }
}
$script:InstallLang = $script:UiLang
}
# ── Python detection ───────────────────────────────────────────── # ── Python detection ─────────────────────────────────────────────
function Find-Python { function Find-Python {
foreach ($cmd in @("python3", "python")) { foreach ($cmd in @("python3", "python")) {
@@ -59,45 +212,36 @@ function Find-Python {
$PythonCmd = Find-Python $PythonCmd = Find-Python
function Assert-Python { function Assert-Python {
if (-not $PythonCmd) { if (-not $PythonCmd) {
Write-Err "Python 3.9-3.13 not found. Please install from https://www.python.org/downloads/" Write-Err (T "未找到 Python 3.9-3.13,请从 https://www.python.org/downloads/ 安装" "Python 3.9-3.13 not found. Please install from https://www.python.org/downloads/")
Read-Host "Press Enter to exit" Read-Host (T "按回车退出" "Press Enter to exit")
exit 1 exit 1
} }
Write-Cow "Found Python: $PythonCmd" Write-Cow ((T "找到 Python" "Found Python") + ": $PythonCmd")
} }
# ── clone project ──────────────────────────────────────────────── # ── clone project ────────────────────────────────────────────────
function Install-Project { function Install-Project {
if (Test-Path "CowAgent") { if (Test-Path "CowAgent") {
Write-Warn "Directory 'CowAgent' already exists." # Auto-backup the existing directory without prompting.
$choice = Read-Host "Overwrite(o), backup(b), or quit(q)? [default: b]" $backup = "CowAgent_backup_$(Get-Date -Format 'yyyyMMddHHmmss')"
if (-not $choice) { $choice = "b" } Rename-Item "CowAgent" $backup
switch ($choice.ToLower()) { Write-Warn ((T "已存在 CowAgent 目录,已自动备份为" "Existing 'CowAgent' directory backed up to") + " '$backup'")
"o" { Remove-Item -Recurse -Force "CowAgent" }
"b" {
$backup = "CowAgent_backup_$(Get-Date -Format 'yyyyMMddHHmmss')"
Rename-Item "CowAgent" $backup
Write-Cow "Backed up to '$backup'"
}
"q" { Write-Err "Installation cancelled."; exit 1 }
default { Write-Err "Invalid choice."; exit 1 }
}
} }
$gitBin = Get-Command git -ErrorAction SilentlyContinue $gitBin = Get-Command git -ErrorAction SilentlyContinue
if (-not $gitBin) { if (-not $gitBin) {
Write-Err "Git not found. Please install from https://git-scm.com/download/win" Write-Err (T "未找到 Git请从 https://git-scm.com/download/win 安装" "Git not found. Please install from https://git-scm.com/download/win")
Read-Host "Press Enter to exit" Read-Host (T "按回车退出" "Press Enter to exit")
exit 1 exit 1
} }
Write-Cow "Cloning CowAgent project..." Write-Cow (T "正在克隆 CowAgent 项目..." "Cloning CowAgent project...")
$cloneOk = $false $cloneOk = $false
# Test GitHub connectivity before attempting clone # Test GitHub connectivity before attempting clone
try { try {
$null = Invoke-WebRequest -Uri "https://github.com" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop $null = Invoke-WebRequest -Uri "https://github.com" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop
Write-Cow "GitHub is reachable, cloning from GitHub..." Write-Cow (T "GitHub 可达,正在从 GitHub 克隆..." "GitHub is reachable, cloning from GitHub...")
$prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue" $prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue"
git clone --depth 10 --progress "https://github.com/zhayujie/CowAgent.git" 2>&1 | ForEach-Object { Write-Host $_ } git clone --depth 10 --progress "https://github.com/zhayujie/CowAgent.git" 2>&1 | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -eq 0) { $cloneOk = $true } if ($LASTEXITCODE -eq 0) { $cloneOk = $true }
@@ -108,7 +252,7 @@ function Install-Project {
} catch {} } catch {}
if (-not $cloneOk) { if (-not $cloneOk) {
Write-Warn "GitHub clone failed or timed out, switching to Gitee mirror..." Write-Warn (T "GitHub 克隆失败或超时,切换到 Gitee 镜像..." "GitHub clone failed or timed out, switching to Gitee mirror...")
$prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue" $prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue"
git clone --depth 10 --progress "https://gitee.com/zhayujie/CowAgent.git" 2>&1 | ForEach-Object { Write-Host $_ } git clone --depth 10 --progress "https://gitee.com/zhayujie/CowAgent.git" 2>&1 | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -eq 0) { $cloneOk = $true } if ($LASTEXITCODE -eq 0) { $cloneOk = $true }
@@ -119,35 +263,83 @@ function Install-Project {
} }
if (-not $cloneOk) { if (-not $cloneOk) {
Write-Err "Clone failed from both GitHub and Gitee. Please check your network connection." Write-Err (T "GitHub 和 Gitee 均克隆失败,请检查网络连接。" "Clone failed from both GitHub and Gitee. Please check your network connection.")
Write-Err "You can also manually clone: git clone https://gitee.com/zhayujie/CowAgent.git" Write-Err (T "你也可以手动克隆: git clone https://gitee.com/zhayujie/CowAgent.git" "You can also manually clone: git clone https://gitee.com/zhayujie/CowAgent.git")
Read-Host "Press Enter to exit" Read-Host (T "按回车退出" "Press Enter to exit")
exit 1 exit 1
} }
Set-Location "CowAgent" Set-Location "CowAgent"
$script:BaseDir = $PWD.Path $script:BaseDir = $PWD.Path
$script:IsProjectDir = $true $script:IsProjectDir = $true
Write-Cow "Project cloned: $BaseDir" Write-Cow ((T "项目已克隆" "Project cloned") + ": $BaseDir")
}
# Test whether a URL is reachable within a short timeout. Uses a HEAD request
# and hides progress so it never blocks the UI for long. Any failure (DNS, TLS,
# timeout) just returns $false so the caller falls back gracefully.
function Test-UrlReachable {
param([string]$Url, [int]$TimeoutSec = 4)
$oldPP = $ProgressPreference; $ProgressPreference = "SilentlyContinue"
try {
$null = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -TimeoutSec $TimeoutSec -ErrorAction Stop
return $true
} catch {
return $false
} finally {
$ProgressPreference = $oldPP
}
}
# Pick the pip index by install language, with the other as fallback:
# - zh users: Tsinghua mirror first, official PyPI fallback
# - others : official PyPI first, Tsinghua mirror fallback
# Returns an args array to splat into pip (empty = pip default / official PyPI).
function Get-PipMirrorArgs {
$tuna = "https://pypi.tuna.tsinghua.edu.cn/simple"
$pypi = "https://pypi.org/simple"
if ($script:UiLang -eq "zh") {
if (Test-UrlReachable "$tuna/") {
Write-Warn ((T "使用 pip 镜像" "Using pip mirror") + ": $tuna")
return @("-i", $tuna)
}
} else {
if ((-not (Test-UrlReachable "$pypi/")) -and (Test-UrlReachable "$tuna/")) {
Write-Warn ((T "使用 pip 镜像" "Using pip mirror") + ": $tuna")
return @("-i", $tuna)
}
}
return @()
} }
# ── install dependencies ───────────────────────────────────────── # ── install dependencies ─────────────────────────────────────────
function Install-Dependencies { function Install-Dependencies {
Write-Cow "Installing dependencies..." Write-Cow (T "正在安装依赖..." "Installing dependencies...")
# Probe the mirror first (with progress hidden so the slow IWR call doesn't
# leave the screen blank for too long).
$oldPP = $ProgressPreference; $ProgressPreference = "SilentlyContinue"
$pipMirror = Get-PipMirrorArgs
$ProgressPreference = $oldPP
# Keep pip output VISIBLE (do not pipe to Out-Null): on slow networks the
# download can take minutes, and a silent step looks like a hang.
$prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue" $prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue"
& $PythonCmd -m pip install --upgrade pip setuptools wheel 2>&1 | Out-Null
& $PythonCmd -m pip install -r "$BaseDir\requirements.txt" 2>&1 | ForEach-Object { Write-Host $_ } Write-Info (T "正在升级 pip 等基础工具..." "Upgrading pip and basic tools...")
& $PythonCmd -m pip install --upgrade pip setuptools wheel @pipMirror
Write-Info (T "正在安装项目依赖(可能需要几分钟)..." "Installing project dependencies (may take a few minutes)...")
& $PythonCmd -m pip install -r "$BaseDir\requirements.txt" @pipMirror
$pipExit = $LASTEXITCODE $pipExit = $LASTEXITCODE
$ErrorActionPreference = $prevEAP $ErrorActionPreference = $prevEAP
if ($pipExit -ne 0) { if ($pipExit -ne 0) {
Write-Warn "Some dependencies may have issues, but continuing..." Write-Warn (T "部分依赖可能有问题,但继续安装..." "Some dependencies may have issues, but continuing...")
} }
Write-Cow "Registering cow CLI..." Write-Cow (T "正在注册 cow CLI..." "Registering cow CLI...")
$prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue" $prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue"
& $PythonCmd -m pip install -e $BaseDir 2>&1 | Out-Null & $PythonCmd -m pip install -e $BaseDir @pipMirror 2>&1 | Out-Null
$ErrorActionPreference = $prevEAP $ErrorActionPreference = $prevEAP
# Ensure Python Scripts dir is in PATH for this session # Ensure Python Scripts dir is in PATH for this session
@@ -160,139 +352,206 @@ function Install-Dependencies {
$cowBin = Get-Command cow -ErrorAction SilentlyContinue $cowBin = Get-Command cow -ErrorAction SilentlyContinue
if ($cowBin) { if ($cowBin) {
Write-Cow "cow CLI registered: $($cowBin.Source)" Write-Cow ((T "cow CLI 注册成功" "cow CLI registered") + ": $($cowBin.Source)")
} else { } else {
Write-Warn "cow CLI not in PATH. You can use: $PythonCmd -m cli.cli" Write-Warn ((T "cow CLI 不在 PATH 中,你可以使用" "cow CLI not in PATH. You can use") + ": $PythonCmd -m cli.cli")
Write-Warn "To fix permanently, add Python Scripts directory to your system PATH." Write-Warn (T "如需永久修复,请将 Python Scripts 目录加入系统 PATH。" "To fix permanently, add Python Scripts directory to your system PATH.")
} }
} }
# ── model selection ────────────────────────────────────────────── # ── model selection ──────────────────────────────────────────────
# Order mirrors run.sh: DeepSeek, Claude, Gemini, OpenAI, MiniMax, Zhipu,
# Qwen, Doubao, Kimi, LinkAI, then Skip (11th option).
# Each entry: Provider / default model name / config key field / optional base.
$ModelChoices = @{ $ModelChoices = @{
"1" = @{ Provider = "DeepSeek"; Default = "deepseek-v4-flash"; Key = "DEEPSEEK_KEY" } 1 = @{ Provider = "DeepSeek"; Default = "deepseek-v4-flash"; Field = "deepseek_api_key" }
"2" = @{ Provider = "MiniMax"; Default = "MiniMax-M2.7"; Key = "MINIMAX_KEY" } 2 = @{ Provider = "Claude"; Default = "claude-opus-4-8"; Field = "claude_api_key"; BaseField = "claude_api_base" }
"3" = @{ Provider = "Zhipu AI"; Default = "glm-5.1"; Key = "ZHIPU_KEY" } 3 = @{ Provider = "Gemini"; Default = "gemini-3.1-pro-preview"; Field = "gemini_api_key"; BaseField = "gemini_api_base" }
"4" = @{ Provider = "Kimi (Moonshot)"; Default = "kimi-k2.6"; Key = "MOONSHOT_KEY" } 4 = @{ Provider = "OpenAI GPT"; Default = "gpt-5.4"; Field = "open_ai_api_key"; BaseField = "open_ai_api_base" }
"5" = @{ Provider = "Doubao (Volcengine Ark)"; Default = "doubao-seed-2-0-code-preview-260215"; Key = "ARK_KEY" } 5 = @{ Provider = "MiniMax"; Default = "MiniMax-M2.7"; Field = "minimax_api_key" }
"6" = @{ Provider = "Qwen (DashScope)"; Default = "qwen3.6-plus"; Key = "DASHSCOPE_KEY" } 6 = @{ Provider = "Zhipu AI"; Default = "glm-5.1"; Field = "zhipu_ai_api_key" }
"7" = @{ Provider = "Claude"; Default = "claude-opus-4-8"; Key = "CLAUDE_KEY"; Base = "https://api.anthropic.com/v1" } 7 = @{ Provider = "Qwen (DashScope)"; Default = "qwen3.6-plus"; Field = "dashscope_api_key" }
"8" = @{ Provider = "Gemini"; Default = "gemini-3.1-pro-preview"; Key = "GEMINI_KEY"; Base = "https://generativelanguage.googleapis.com" } 8 = @{ Provider = "Doubao (Volcengine Ark)"; Default = "doubao-seed-2-0-code-preview-260215"; Field = "ark_api_key" }
"9" = @{ Provider = "OpenAI GPT"; Default = "gpt-5.4"; Key = "OPENAI_KEY"; Base = "https://api.openai.com/v1" } 9 = @{ Provider = "Kimi (Moonshot)"; Default = "kimi-k2.6"; Field = "moonshot_api_key" }
"10" = @{ Provider = "LinkAI"; Default = "deepseek-v4-flash"; Key = "LINKAI_KEY" } 10 = @{ Provider = "LinkAI"; Default = "deepseek-v4-flash"; Field = "linkai_api_key"; Linkai = $true }
} }
function Select-Model { function Select-Model {
Write-Info "========================================="
Write-Info " Select AI Model"
Write-Info "========================================="
Write-Host "1) DeepSeek (deepseek-v4-flash, deepseek-v4-pro, etc.)"
Write-Host "2) MiniMax (MiniMax-M2.7, MiniMax-M2.5, etc.)"
Write-Host "3) Zhipu AI (glm-5.1, glm-5-turbo, glm-5, etc.)"
Write-Host "4) Kimi (kimi-k2.6, kimi-k2.5, kimi-k2, etc.)"
Write-Host "5) Doubao (doubao-seed-2-0-code-preview-260215, etc.)"
Write-Host "6) Qwen (qwen3.6-plus, qwen3.5-plus, qwen3-max, qwq-plus, etc.)"
Write-Host "7) Claude (claude-opus-4-8, claude-opus-4-7, claude-sonnet-4-6, etc.)"
Write-Host "8) Gemini (gemini-3.1-flash-lite-preview, gemini-3.1-pro-preview, etc.)"
Write-Host "9) OpenAI GPT (gpt-5.4, gpt-5.2, gpt-4.1, etc.)"
Write-Host "10) LinkAI (access multiple models via one API)"
Write-Host "" Write-Host ""
$title = T "选择 AI 模型" "Select AI Model"
$options = @(
"DeepSeek (deepseek-v4-flash, deepseek-v4-pro, etc.)",
"Claude (claude-opus-4-8, claude-opus-4-7, claude-sonnet-4-6, etc.)",
"Gemini (gemini-3.1-flash-lite-preview, gemini-3.1-pro-preview, etc.)",
"OpenAI GPT (gpt-5.4, gpt-5.2, gpt-4.1, etc.)",
"MiniMax (MiniMax-M2.7, MiniMax-M2.5, etc.)",
"Zhipu AI (glm-5.1, glm-5-turbo, glm-5, etc.)",
"Qwen (qwen3.6-plus, qwen3.5-plus, qwen3-max, qwq-plus, etc.)",
"Doubao (doubao-seed-2-0-code-preview-260215, etc.)",
"Kimi (kimi-k2.6, kimi-k2.5, kimi-k2, etc.)",
("LinkAI (" + (T "一个 Key 接入所有模型" "access all models via one API") + ")"),
(T "⏭ 跳过(稍后在 Web 控制台配置)" "⏭ Skip (configure later in the web console)")
)
$script:ModelChoice = Select-Menu -Title $title -Options $options -Default 1
}
do { # Configure model. Only ask for the API key; model name and base default to
$choice = Read-Host "Enter your choice [default: 1 - DeepSeek]" # sensible values and can be changed later in the web console.
if (-not $choice) { $choice = "1" } function Configure-Model {
} while ($choice -notmatch '^([1-9]|10)$') # Reset model-related state
$script:ModelName = ""
$script:ModelField = ""
$script:ApiKey = ""
$script:ApiBase = ""
$script:ApiBaseField = ""
$script:UseLinkai = $false
$m = $ModelChoices[$choice] if ($script:ModelChoice -eq 11) {
Write-Cow "Configuring $($m.Provider)..." # Skip: leave model unset, will be configured in the web console.
Write-Warn (T "已跳过模型配置,稍后可在 Web 控制台填写" "Model configuration skipped, you can set it later in the web console")
$script:ApiKey = Read-Host "Enter $($m.Provider) API Key" return
$model = Read-Host "Enter model name [default: $($m.Default)]"
if (-not $model) { $model = $m.Default }
$script:ModelName = $model
$script:KeyName = $m.Key
$script:UseLinkai = ($choice -eq "10")
if ($m.Base) {
$base = Read-Host "Enter API Base URL [default: $($m.Base)]"
if (-not $base) { $base = $m.Base }
$script:ApiBase = $base
} else {
$script:ApiBase = ""
} }
$script:ModelChoice = $choice
$m = $ModelChoices[$script:ModelChoice]
Write-Cow ((T "正在配置" "Configuring") + " $($m.Provider)...")
# Show where to obtain a LinkAI key.
if ($m.Linkai) {
Write-Info ((T "获取 LinkAI Key" "Get your LinkAI Key") + ": https://link-ai.tech/console/interface")
}
$hint = T "回车跳过,稍后在 Web 控制台填写" "press Enter to skip, set later in web console"
$script:ApiKey = Read-Host ((T "请输入" "Enter") + " $($m.Provider) API Key ($hint)")
$script:ModelName = $m.Default
$script:ModelField = $m.Field
if ($m.BaseField) { $script:ApiBaseField = $m.BaseField }
if ($m.Linkai) { $script:UseLinkai = $true }
} }
# ── channel selection ──────────────────────────────────────────── # ── channel selection ────────────────────────────────────────────
# Channel label by stable key (independent of menu order).
function Get-ChannelLabel {
param([string]$Key)
switch ($Key) {
"web" { return (T "Web 网页控制台(推荐,开箱即用)" "Web Console (recommended, ready to use)") }
"weixin" { return (T "微信 Weixin" "WeChat (Weixin)") }
"feishu" { return (T "飞书 Feishu" "Feishu / Lark") }
"dingtalk" { return (T "钉钉 DingTalk" "DingTalk") }
"wecom_bot" { return (T "企微智能机器人 WeCom Bot" "WeCom Bot") }
"qq" { return "QQ" }
"wechatcom_app" { return (T "企微自建应用 WeCom App" "WeCom App") }
"telegram" { return "Telegram" }
"slack" { return "Slack" }
"discord" { return "Discord" }
"skip" { return (T "⏭ 跳过(稍后在 Web 控制台配置)" "⏭ Skip (configure later in the web console)") }
}
}
# Select channel. The display order depends on the install language:
# - English: Web first, then the global IM channels (Telegram/Discord/Slack),
# then the China-focused channels.
# - Chinese: Web first, then China-focused channels, then global ones.
# A stable key list decouples the menu order from the config logic.
function Select-Channel { function Select-Channel {
Write-Host "" Write-Host ""
Write-Info "=========================================" $title = T "选择接入渠道" "Select Communication Channel"
Write-Info " Select Communication Channel" if ($script:UiLang -eq "en") {
Write-Info "=========================================" $script:ChannelKeys = @("web", "telegram", "discord", "slack", "weixin", "feishu", "dingtalk", "wecom_bot", "qq", "wechatcom_app", "skip")
Write-Host "1) Weixin" } else {
Write-Host "2) Feishu" $script:ChannelKeys = @("web", "weixin", "feishu", "dingtalk", "wecom_bot", "qq", "wechatcom_app", "telegram", "slack", "discord", "skip")
Write-Host "3) DingTalk" }
Write-Host "4) WeCom Bot" $options = @($script:ChannelKeys | ForEach-Object { Get-ChannelLabel $_ })
Write-Host "5) QQ" $idx = Select-Menu -Title $title -Options $options -Default 1
Write-Host "6) WeCom App" # Map the 1-based menu position back to the stable channel key.
Write-Host "7) Web" $script:ChannelChoice = $script:ChannelKeys[$idx - 1]
Write-Host "" }
do {
$choice = Read-Host "Enter your choice [default: 1 - Weixin]"
if (-not $choice) { $choice = "1" }
} while ($choice -notmatch '^[1-7]$')
# Configure channel, dispatched by stable channel key (not menu position).
function Configure-Channel {
$script:ChannelExtra = @{} $script:ChannelExtra = @{}
$script:AccessInfo = ""
switch ($choice) { switch ($script:ChannelChoice) {
"1" { $script:ChannelType = "weixin" } { $_ -eq "web" -or $_ -eq "skip" } {
"2" { # Web (also the default when skipped). Default port, no prompt.
$script:ChannelType = "feishu"
$script:ChannelExtra["feishu_app_id"] = Read-Host "Enter Feishu App ID"
$script:ChannelExtra["feishu_app_secret"] = Read-Host "Enter Feishu App Secret"
}
"3" {
$script:ChannelType = "dingtalk"
$script:ChannelExtra["dingtalk_client_id"] = Read-Host "Enter DingTalk Client ID"
$script:ChannelExtra["dingtalk_client_secret"] = Read-Host "Enter DingTalk Client Secret"
}
"4" {
$script:ChannelType = "wecom_bot"
$script:ChannelExtra["wecom_bot_id"] = Read-Host "Enter WeCom Bot ID"
$script:ChannelExtra["wecom_bot_secret"] = Read-Host "Enter WeCom Bot Secret"
}
"5" {
$script:ChannelType = "qq"
$script:ChannelExtra["qq_app_id"] = Read-Host "Enter QQ App ID"
$script:ChannelExtra["qq_app_secret"] = Read-Host "Enter QQ App Secret"
}
"6" {
$script:ChannelType = "wechatcom_app"
$script:ChannelExtra["wechatcom_corp_id"] = Read-Host "Enter WeChat Corp ID"
$script:ChannelExtra["wechatcomapp_token"] = Read-Host "Enter WeChat Com App Token"
$script:ChannelExtra["wechatcomapp_secret"] = Read-Host "Enter WeChat Com App Secret"
$script:ChannelExtra["wechatcomapp_agent_id"] = Read-Host "Enter WeChat Com App Agent ID"
$script:ChannelExtra["wechatcomapp_aes_key"] = Read-Host "Enter WeChat Com App AES Key"
$port = Read-Host "Enter port [default: 9898]"
if (-not $port) { $port = "9898" }
$script:ChannelExtra["wechatcomapp_port"] = [int]$port
}
"7" {
$script:ChannelType = "web" $script:ChannelType = "web"
$port = Read-Host "Enter web port [default: 9899]" $script:ChannelExtra["web_port"] = 9899
if (-not $port) { $port = "9899" } $script:AccessInfo = (T "Web 控制台地址" "Web console") + " : http://localhost:9899/chat"
$script:ChannelExtra["web_port"] = [int]$port }
"weixin" {
$script:ChannelType = "weixin"
$script:AccessInfo = T "微信渠道已配置,请在终端或 Web 控制台扫码登录" "Weixin channel configured. Scan QR code in terminal or web console to login."
}
"feishu" {
$script:ChannelType = "feishu"
Write-Cow (T "配置飞书WebSocket 模式)..." "Configure Feishu (WebSocket mode)...")
$script:ChannelExtra["feishu_app_id"] = Read-Host (T "请输入飞书 App ID" "Enter Feishu App ID")
$script:ChannelExtra["feishu_app_secret"] = Read-Host (T "请输入飞书 App Secret" "Enter Feishu App Secret")
$script:ChannelExtra["feishu_event_mode"] = "websocket"
$script:AccessInfo = T "飞书渠道已配置WebSocket 模式)" "Feishu channel configured (WebSocket mode)"
}
"dingtalk" {
$script:ChannelType = "dingtalk"
Write-Cow (T "配置钉钉..." "Configure DingTalk...")
$script:ChannelExtra["dingtalk_client_id"] = Read-Host (T "请输入钉钉 Client ID" "Enter DingTalk Client ID")
$script:ChannelExtra["dingtalk_client_secret"] = Read-Host (T "请输入钉钉 Client Secret" "Enter DingTalk Client Secret")
$script:AccessInfo = T "钉钉渠道已配置" "DingTalk channel configured"
}
"wecom_bot" {
$script:ChannelType = "wecom_bot"
Write-Cow (T "配置企微智能机器人..." "Configure WeCom Bot...")
$script:ChannelExtra["wecom_bot_id"] = Read-Host (T "请输入 WeCom Bot ID" "Enter WeCom Bot ID")
$script:ChannelExtra["wecom_bot_secret"] = Read-Host (T "请输入 WeCom Bot Secret" "Enter WeCom Bot Secret")
$script:AccessInfo = T "企微智能机器人渠道已配置" "WeCom Bot channel configured"
}
"qq" {
$script:ChannelType = "qq"
Write-Cow (T "配置 QQ 机器人..." "Configure QQ Bot...")
$script:ChannelExtra["qq_app_id"] = Read-Host (T "请输入 QQ App ID" "Enter QQ App ID")
$script:ChannelExtra["qq_app_secret"] = Read-Host (T "请输入 QQ App Secret" "Enter QQ App Secret")
$script:AccessInfo = T "QQ 机器人渠道已配置" "QQ Bot channel configured"
}
"wechatcom_app" {
$script:ChannelType = "wechatcom_app"
Write-Cow (T "配置企微自建应用..." "Configure WeCom App...")
$script:ChannelExtra["wechatcom_corp_id"] = Read-Host (T "请输入企业 Corp ID" "Enter WeChat Corp ID")
$script:ChannelExtra["wechatcomapp_token"] = Read-Host (T "请输入应用 Token" "Enter WeChat Com App Token")
$script:ChannelExtra["wechatcomapp_secret"] = Read-Host (T "请输入应用 Secret" "Enter WeChat Com App Secret")
$script:ChannelExtra["wechatcomapp_agent_id"] = Read-Host (T "请输入应用 Agent ID" "Enter WeChat Com App Agent ID")
$script:ChannelExtra["wechatcomapp_aes_key"] = Read-Host (T "请输入应用 AES Key" "Enter WeChat Com App AES Key")
$port = Read-Host ((T "请输入应用端口" "Enter port") + " [" + (T "默认" "default") + ": 9898]")
if (-not ($port -match '^\d+$')) { $port = "9898" }
$script:ChannelExtra["wechatcomapp_port"] = [int]$port
$script:AccessInfo = (T "企微自建应用渠道已配置,端口" "WeCom App channel configured on port") + " $port"
}
"telegram" {
$script:ChannelType = "telegram"
Write-Cow (T "配置 Telegram..." "Configure Telegram...")
$script:ChannelExtra["telegram_token"] = Read-Host (T "请输入 Telegram Bot Token" "Enter Telegram Bot Token")
$script:AccessInfo = T "Telegram 渠道已配置" "Telegram channel configured"
}
"slack" {
$script:ChannelType = "slack"
Write-Cow (T "配置 Slack..." "Configure Slack...")
$script:ChannelExtra["slack_bot_token"] = Read-Host ((T "请输入 Slack Bot Token" "Enter Slack Bot Token") + " (xoxb-...)")
$script:ChannelExtra["slack_app_token"] = Read-Host ((T "请输入 Slack App Token" "Enter Slack App Token") + " (xapp-...)")
$script:AccessInfo = T "Slack 渠道已配置" "Slack channel configured"
}
"discord" {
$script:ChannelType = "discord"
Write-Cow (T "配置 Discord..." "Configure Discord...")
$script:ChannelExtra["discord_token"] = Read-Host (T "请输入 Discord Bot Token" "Enter Discord Bot Token")
$script:AccessInfo = T "Discord 渠道已配置" "Discord channel configured"
} }
} }
} }
# ── generate config.json ───────────────────────────────────────── # ── generate config.json ─────────────────────────────────────────
function New-ConfigFile { function New-ConfigFile {
Write-Cow "Generating config.json..." Write-Cow (T "正在生成 config.json..." "Generating config.json...")
$config = [ordered]@{ $config = [ordered]@{
channel_type = $ChannelType channel_type = if ($script:ChannelType) { $script:ChannelType } else { "web" }
model = $ModelName model = if ($script:ModelName) { $script:ModelName } else { "" }
cow_lang = if ($script:InstallLang) { $script:InstallLang } else { "auto" }
open_ai_api_key = "" open_ai_api_key = ""
open_ai_api_base = "https://api.openai.com/v1" open_ai_api_base = "https://api.openai.com/v1"
claude_api_key = "" claude_api_key = ""
@@ -311,7 +570,7 @@ function New-ConfigFile {
voice_reply_voice = $false voice_reply_voice = $false
speech_recognition = $true speech_recognition = $true
group_speech_recognition = $false group_speech_recognition = $false
use_linkai = $UseLinkai use_linkai = [bool]$script:UseLinkai
linkai_api_key = "" linkai_api_key = ""
linkai_app_code = "" linkai_app_code = ""
agent = $true agent = $true
@@ -320,51 +579,33 @@ function New-ConfigFile {
agent_max_steps = 15 agent_max_steps = 15
} }
# Set the correct API key field # Set the API key into the right field (skipped models leave it empty).
$keyMap = @{ if ($script:ModelField -and $config.Contains($script:ModelField)) {
OPENAI_KEY = "open_ai_api_key" $config[$script:ModelField] = $script:ApiKey
CLAUDE_KEY = "claude_api_key"
GEMINI_KEY = "gemini_api_key"
ZHIPU_KEY = "zhipu_ai_api_key"
MOONSHOT_KEY = "moonshot_api_key"
ARK_KEY = "ark_api_key"
DASHSCOPE_KEY = "dashscope_api_key"
MINIMAX_KEY = "minimax_api_key"
DEEPSEEK_KEY = "deepseek_api_key"
LINKAI_KEY = "linkai_api_key"
} }
if ($keyMap.ContainsKey($KeyName)) { # Set API base if the model has a configurable base and the user changed it.
$config[$keyMap[$KeyName]] = $ApiKey if ($script:ApiBase -and $script:ApiBaseField -and $config.Contains($script:ApiBaseField)) {
} $config[$script:ApiBaseField] = $script:ApiBase
# Set API base if provided
$baseMap = @{
"7" = "claude_api_base"
"8" = "gemini_api_base"
"9" = "open_ai_api_base"
}
if ($ApiBase -and $baseMap.ContainsKey($ModelChoice)) {
$config[$baseMap[$ModelChoice]] = $ApiBase
} }
# Merge channel-specific fields # Merge channel-specific fields
foreach ($k in $ChannelExtra.Keys) { foreach ($k in $script:ChannelExtra.Keys) {
$config[$k] = $ChannelExtra[$k] $config[$k] = $script:ChannelExtra[$k]
} }
$jsonText = $config | ConvertTo-Json -Depth 5 $jsonText = $config | ConvertTo-Json -Depth 5
[System.IO.File]::WriteAllText("$BaseDir\config.json", $jsonText, (New-Object System.Text.UTF8Encoding $false)) [System.IO.File]::WriteAllText("$BaseDir\config.json", $jsonText, (New-Object System.Text.UTF8Encoding $false))
Write-Cow "Configuration file created." Write-Cow (T "配置文件创建成功。" "Configuration file created.")
} }
# ── start via cow CLI ───────────────────────────────────────────── # ── start via cow CLI ─────────────────────────────────────────────
function Start-CowAgent { function Start-CowAgent {
Write-Cow "Starting CowAgent..." Write-Cow (T "正在启动 CowAgent..." "Starting CowAgent...")
$cowBin = Get-Command cow -ErrorAction SilentlyContinue $cowBin = Get-Command cow -ErrorAction SilentlyContinue
if ($cowBin) { if ($cowBin) {
& cow start & cow start
} else { } else {
Write-Warn "cow CLI not found, starting directly..." Write-Warn (T "未找到 cow CLI直接启动..." "cow CLI not found, starting directly...")
& $PythonCmd "$BaseDir\app.py" & $PythonCmd "$BaseDir\app.py"
} }
} }
@@ -376,7 +617,7 @@ function Invoke-CowCommand {
if ($cowBin) { if ($cowBin) {
& cow $Cmd & cow $Cmd
} else { } else {
Write-Err "cow CLI not found. Run this script without arguments first to install." Write-Err (T "未找到 cow CLI请先不带参数运行本脚本进行安装。" "cow CLI not found. Run this script without arguments first to install.")
exit 1 exit 1
} }
} }
@@ -387,19 +628,19 @@ function Show-Usage {
Write-Info " CowAgent Management Script (Windows)" Write-Info " CowAgent Management Script (Windows)"
Write-Info "=========================================" Write-Info "========================================="
Write-Host "" Write-Host ""
Write-Host "Usage:" Write-Host (T "用法:" "Usage:")
Write-Host " .\run.ps1 # Install / Configure" Write-Host (" .\run.ps1 # " + (T "安装 / 配置" "Install / Configure"))
Write-Host " .\run.ps1 <command> # Management command" Write-Host (" .\run.ps1 <command> # " + (T "管理命令" "Management command"))
Write-Host "" Write-Host ""
Write-Host "Commands:" Write-Host (T "命令:" "Commands:")
Write-Host " start Start the service" Write-Host (" start " + (T "启动服务" "Start the service"))
Write-Host " stop Stop the service" Write-Host (" stop " + (T "停止服务" "Stop the service"))
Write-Host " restart Restart the service" Write-Host (" restart " + (T "重启服务" "Restart the service"))
Write-Host " status Check service status" Write-Host (" status " + (T "查看状态" "Check service status"))
Write-Host " logs View logs" Write-Host (" logs " + (T "查看日志" "View logs"))
Write-Host " config Reconfigure project" Write-Host (" config " + (T "重新配置项目" "Reconfigure project"))
Write-Host " update Update and restart" Write-Host (" update " + (T "更新并重启" "Update and restart"))
Write-Host " help Show this message" Write-Host (" help " + (T "显示本帮助" "Show this message"))
Write-Host "" Write-Host ""
} }
@@ -411,15 +652,19 @@ function Install-Mode {
Write-Info "=========================================" Write-Info "========================================="
Write-Host "" Write-Host ""
# Step 0: choose the install/UI language. Everything after this is localized.
Select-Language
Write-Host ""
if ($IsProjectDir) { if ($IsProjectDir) {
Write-Cow "Detected existing project directory." Write-Cow (T "检测到已有项目目录。" "Detected existing project directory.")
if (Test-Path "$BaseDir\config.json") { if (Test-Path "$BaseDir\config.json") {
Write-Cow "Project already configured." Write-Cow (T "项目已配置。" "Project already configured.")
Write-Host "" Write-Host ""
Show-Usage Show-Usage
return return
} }
Write-Warn "No config.json found. Let's configure your project!" Write-Warn (T "未找到 config.json开始配置项目" "No config.json found. Let's configure your project!")
Write-Host "" Write-Host ""
Assert-Python Assert-Python
} else { } else {
@@ -429,25 +674,20 @@ function Install-Mode {
Install-Dependencies Install-Dependencies
Select-Model Select-Model
Configure-Model
Select-Channel Select-Channel
Configure-Channel
New-ConfigFile New-ConfigFile
# Auto-start after configuration for a true out-of-the-box experience.
Write-Host "" Write-Host ""
$startNow = Read-Host "Start CowAgent now? [Y/n]" if ($script:AccessInfo) { Write-Cow $script:AccessInfo }
if ($startNow -ne "n" -and $startNow -ne "N") { Start-CowAgent
Start-CowAgent
} else {
Write-Cow "Installation complete!"
Write-Host ""
Write-Host "To start manually:"
Write-Host " cd $BaseDir"
Write-Host " cow start"
}
} }
# ── update ──────────────────────────────────────────────────────── # ── update ────────────────────────────────────────────────────────
function Update-Project { function Update-Project {
Write-Cow "Updating CowAgent..." Write-Cow (T "正在更新 CowAgent..." "Updating CowAgent...")
Set-Location $BaseDir Set-Location $BaseDir
# Stop if running # Stop if running
@@ -459,20 +699,20 @@ function Update-Project {
} }
if (Test-Path "$BaseDir\.git") { if (Test-Path "$BaseDir\.git") {
Write-Cow "Pulling latest code..." Write-Cow (T "正在拉取最新代码..." "Pulling latest code...")
$prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue" $prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue"
git pull 2>&1 | Out-Null git pull 2>&1 | Out-Null
$pullExit = $LASTEXITCODE $pullExit = $LASTEXITCODE
$ErrorActionPreference = $prevEAP $ErrorActionPreference = $prevEAP
if ($pullExit -ne 0) { if ($pullExit -ne 0) {
Write-Warn "GitHub failed, trying Gitee..." Write-Warn (T "GitHub 拉取失败,尝试 Gitee..." "GitHub failed, trying Gitee...")
$ErrorActionPreference = "Continue" $ErrorActionPreference = "Continue"
git remote set-url origin https://gitee.com/zhayujie/CowAgent.git 2>&1 | Out-Null git remote set-url origin https://gitee.com/zhayujie/CowAgent.git 2>&1 | Out-Null
git pull 2>&1 | Out-Null git pull 2>&1 | Out-Null
$ErrorActionPreference = $prevEAP $ErrorActionPreference = $prevEAP
} }
} else { } else {
Write-Warn "Not a git repository, skipping code update." Write-Warn (T "非 git 仓库,跳过代码更新。" "Not a git repository, skipping code update.")
} }
Assert-Python Assert-Python
@@ -480,11 +720,13 @@ function Update-Project {
# Start via python -m cli.cli instead of cow.exe, because the exe may # Start via python -m cli.cli instead of cow.exe, because the exe may
# still be cached/locked from the previous installation on Windows. # still be cached/locked from the previous installation on Windows.
Write-Cow "Starting CowAgent..." Write-Cow (T "正在启动 CowAgent..." "Starting CowAgent...")
& $PythonCmd -m cli.cli start & $PythonCmd -m cli.cli start
} }
# ── main ────────────────────────────────────────────────────────── # ── main ──────────────────────────────────────────────────────────
Initialize-UiLang
switch ($Command.ToLower()) { switch ($Command.ToLower()) {
"" { Install-Mode } "" { Install-Mode }
"start" { Invoke-CowCommand "start" } "start" { Invoke-CowCommand "start" }
@@ -496,15 +738,17 @@ switch ($Command.ToLower()) {
Assert-Python Assert-Python
Install-Dependencies Install-Dependencies
Select-Model Select-Model
Configure-Model
Select-Channel Select-Channel
Configure-Channel
New-ConfigFile New-ConfigFile
$r = Read-Host "Restart service now? [Y/n]" $r = Read-Host (T "现在重启服务吗?[Y/n]" "Restart service now? [Y/n]")
if ($r -ne "n" -and $r -ne "N") { Invoke-CowCommand "restart" } if ($r -ne "n" -and $r -ne "N") { Invoke-CowCommand "restart" }
} }
"update" { Update-Project } "update" { Update-Project }
"help" { Show-Usage } "help" { Show-Usage }
default { default {
Write-Err "Unknown command: $Command" Write-Err ((T "未知命令" "Unknown command") + ": $Command")
Show-Usage Show-Usage
exit 1 exit 1
} }