diff --git a/cli/commands/install.py b/cli/commands/install.py index ed22f296..b72bcba1 100644 --- a/cli/commands/install.py +++ b/cli/commands/install.py @@ -118,9 +118,11 @@ def run_install_browser( 0 on success, 1 on fatal failure (pip or chromium install failed). """ 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 + from common import i18n _t = i18n.t stream = stream or _default_stream diff --git a/cli/commands/process.py b/cli/commands/process.py index 9d22b67f..94e8126b 100644 --- a/cli/commands/process.py +++ b/cli/commands/process.py @@ -276,9 +276,12 @@ def status(): """Show CowAgent running status.""" from cli import __version__ 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 + from common import i18n _t = i18n.t pid = _read_pid() diff --git a/cli/commands/skill.py b/cli/commands/skill.py index fa5a3167..61f92fb7 100644 --- a/cli/commands/skill.py +++ b/cli/commands/skill.py @@ -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): """Print a unified install success message with description and source.""" 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 + from common import i18n _t = i18n.t skills_dir = get_skills_dir() diff --git a/config-template.json b/config-template.json index dcd19774..8be82885 100644 --- a/config-template.json +++ b/config-template.json @@ -1,6 +1,6 @@ { "cow_lang": "auto", - "channel_type": "weixin", + "channel_type": "web", "model": "deepseek-v4-flash", "deepseek_api_key": "", "deepseek_api_base": "https://api.deepseek.com/v1", diff --git a/run.sh b/run.sh index 5dff9785..21bb5a9f 100755 --- a/run.sh +++ b/run.sh @@ -28,6 +28,261 @@ if [ -z "$BASH_VERSION" ]; then exit 1 fi +# ============================ +# i18n: install-flow language +# ============================ +# UI_LANG controls the language of install prompts/menus. Detected on first run +# (or chosen by the user), defaults to auto-detection. "zh" or "en". +UI_LANG="" + +# A terminal we can read from. When the script runs via `curl | bash`, stdin is +# the script pipe (EOF on read), so interactive prompts must read from the tty. +TTY_DEV="/dev/tty" +HAS_TTY=false +if [ -r /dev/tty ] && [ -w /dev/tty ]; then + HAS_TTY=true +fi + +# Detect default UI language from environment (best-effort, mirrors common/i18n). +detect_ui_lang() { + local loc="" + # macOS: prefer AppleLocale, which reflects the real UI language + if [ "$(uname)" = "Darwin" ] && command -v defaults &> /dev/null; then + loc=$(defaults read -g AppleLocale 2>/dev/null || true) + fi + [ -z "$loc" ] && loc="${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" + case "$loc" in + zh* | *zh_* | *_CN* | *_TW* | *_HK* | *Hans* | *Hant*) echo "zh" ;; + *) echo "en" ;; + esac +} + +# Translation helper: t +t() { + if [ "$UI_LANG" = "en" ]; then + printf '%s' "$2" + else + printf '%s' "$1" + fi +} + +# Read a line from the controlling terminal (works under `curl | bash`). +# Usage: tty_read VAR "prompt" +tty_read() { + local __var=$1 __prompt=$2 __input="" + if [ "$HAS_TTY" = true ]; then + # Ensure the tty is in normal line mode. A preceding arrow-key menu + # may have left it in cbreak/-echo mode; without this, `read` could + # return immediately or not echo typed characters. + stty sane < "$TTY_DEV" 2>/dev/null || true + # Print the prompt explicitly (not via read -p, whose prompt can be + # swallowed right after an arrow-key menu) and read from the tty. + # `|| true` so a non-zero read (EOF) does NOT trip `set -e`. + printf '%s' "$__prompt" > /dev/tty + read -r __input < "$TTY_DEV" || true + else + read -r -p "$__prompt" __input || true + fi + printf -v "$__var" '%s' "$__input" +} + +# Arrow-key selectable menu with number fallback. +# Usage: select_menu OUT_VAR "Title" "opt1" "opt2" ... +# Result: OUT_VAR is set to the selected index (1-based). +select_menu() { + # Interactive function: never let a non-zero command (read EOF, arithmetic + # evaluating to 0, etc.) abort the caller under `set -e`. + set +e + local __out=$1; shift + local title=$1; shift + local options=("$@") + local count=${#options[@]} + # Initial highlight: MENU_DEFAULT (1-based) if set, else first option. + local cur=0 + if [[ "${MENU_DEFAULT:-}" =~ ^[0-9]+$ ]] && (( MENU_DEFAULT >= 1 && MENU_DEFAULT <= count )); then + cur=$((MENU_DEFAULT - 1)) + fi + MENU_DEFAULT="" + + # Fallback to numbered input when no interactive terminal is available + # (e.g. CI, non-tty pipe). Arrow-key rendering needs a real tty. + if [ "$HAS_TTY" != true ] || [ ! -t 1 ]; then + local def=$((cur + 1)) + echo -e "${CYAN}${BOLD}${title}${NC}" + local i=1 + for opt in "${options[@]}"; do + echo -e " ${YELLOW}${i})${NC} ${opt}" + i=$((i + 1)) + done + local choice="" + while true; do + tty_read choice "$(t "请输入序号" "Enter number") [1-${count}, $(t "默认" "default") ${def}]: " + choice=${choice:-$def} + if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= count )); then + break + fi + echo -e "${RED}$(t "无效选择,请输入" "Invalid choice, enter") 1-${count}${NC}" + done + printf -v "$__out" '%s' "$choice" + return + fi + + # Interactive arrow-key menu. + # Use literal escape characters (via $'...') and printf instead of + # `echo -e`, because `echo`'s backslash handling is not portable and + # leaks raw "\e[K" text on some shells/terminals. + local ESC=$'\033' + local UP="${ESC}[A" # move cursor up one line + local CLR="${ESC}[K" # clear to end of line + + # fd 3 is a long-lived (read) handle to the controlling terminal, opened + # once by menu_session_begin() before the install flow. Reusing one fd + # across all menus avoids the bash 3.2 bug where re-opening /dev/tty per + # menu makes the second menu read EOF and auto-select the default. + # Detect whether fd 3 is already open using a READ redirection (fd 3 is + # read-only; testing with `>&3` would wrongly report it as closed). + local _own_fd3=false + if ! { : <&3; } 2>/dev/null; then + exec 3<"$TTY_DEV" + _own_fd3=true + fi + + # Put the terminal into cbreak/raw input mode so single keystrokes arrive + # immediately and are not echoed. + # -echo : don't echo keystrokes (otherwise arrow keys leak as ^[[A) + # -icanon : disable line buffering + # min 1 time 0 : read returns as soon as 1 byte is available + local _restore="tput cnorm 2>/dev/null; stty echo icanon <${TTY_DEV} 2>/dev/null" + trap "$_restore" EXIT INT TERM + tput civis 2>/dev/null || true + stty -echo -icanon min 1 time 0 <&3 2>/dev/null || true + + printf '%b\n' "${CYAN}${BOLD}${title}${NC}" + printf '%b\n' "${CYAN}$(t "↑/↓ 选择,Enter 确认" "Use ↑/↓ to move, Enter to select")${NC}" + + local first_draw=true + while true; do + # Move cursor up to the top of the option block to redraw it. + if [ "$first_draw" = false ]; then + local i=0 + while [ $i -lt $count ]; do + printf '%s' "$UP" + i=$((i + 1)) + done + fi + first_draw=false + + local idx=0 + for opt in "${options[@]}"; do + if [ $idx -eq $cur ]; then + printf '%s%b\n' "$CLR" " ${GREEN}${BOLD}❯ ${opt}${NC}" + else + printf '%s%b\n' "$CLR" " ${opt}" + fi + idx=$((idx + 1)) + done + + # Read one key from the shared terminal fd 3. + local key="" + IFS= read -rsn1 key <&3 + local rc=$? + if [ $rc -ne 0 ]; then + # No usable terminal: restore and fall back to numbered input. + eval "$_restore"; trap - EXIT INT TERM + [ "${_own_fd3:-}" = true ] && exec 3<&- 2>/dev/null + local choice="" + while true; do + tty_read choice "$(t "请输入序号" "Enter number") [1-${count}]: " + choice=${choice:-$((cur + 1))} + if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= count )); then + break + fi + done + printf -v "$__out" '%s' "$choice" + return + fi + + # Empty key means Enter/Return (read -n1 strips the newline delimiter). + if [ -z "$key" ]; then + break + fi + + case "$key" in + "$ESC") + # Arrow key: ESC [ A/B (or ESC O A/B). Read the two trailing + # bytes one at a time, no timeout (bash 3.2 has no fractional + # read -t; in cbreak mode the bytes are already buffered). + local b2="" b3="" + IFS= read -rsn1 b2 <&3 2>/dev/null || b2="" + IFS= read -rsn1 b3 <&3 2>/dev/null || b3="" + case "${b2}${b3}" in + "[A" | "OA") cur=$(( (cur - 1 + count) % count )) ;; # up + "[B" | "OB") cur=$(( (cur + 1) % count )) ;; # down + esac + ;; + $'\n' | $'\r') + break + ;; + [0-9]) + if (( key >= 1 && key <= count )); then + cur=$((key - 1)) + break + fi + ;; + $'\003') + # Ctrl-C: restore and abort. + eval "$_restore"; trap - EXIT INT TERM + [ "${_own_fd3:-}" = true ] && exec 3<&- 2>/dev/null + printf '\n%b\n' "${RED}$(t "已取消安装" "Installation cancelled")${NC}" + exit 130 + ;; + esac + done + + eval "$_restore" + trap - EXIT INT TERM + [ "${_own_fd3:-}" = true ] && exec 3<&- 2>/dev/null + printf -v "$__out" '%s' "$((cur + 1))" +} + +# Open/close a long-lived terminal handle (fd 3) shared by all menus in an +# install/config session. Opening fd 3 once avoids per-menu re-open issues on +# bash 3.2 (second menu reading EOF). Safe no-ops when there is no tty. +menu_session_begin() { + [ "$HAS_TTY" = true ] || return 0 + exec 3<"$TTY_DEV" 2>/dev/null || true +} +menu_session_end() { + exec 3<&- 2>/dev/null || true +} + +# Ask the user to choose the install/UI language (first step of install). +select_language() { + # Order is fixed (English first, Chinese second). The default highlight + # follows detection, but conservatively: only a confident "zh" signal + # (macOS AppleLocale / Linux zh_* locale) preselects Chinese; everything + # else (English, empty/C/POSIX locale, server images) defaults to English. + local detected + detected=$(detect_ui_lang) + if [ "$detected" = "zh" ]; then + MENU_DEFAULT=2 + UI_LANG="zh" + else + MENU_DEFAULT=1 + UI_LANG="en" + fi + + local lang_choice + select_menu lang_choice "Select Language / 选择语言" "English" "中文 (Chinese)" + case "$lang_choice" in + 1) UI_LANG="en" ;; + 2) UI_LANG="zh" ;; + *) UI_LANG="en" ;; + esac + # Remember for the rest of the flow (config write happens later) + INSTALL_LANG="$UI_LANG" +} + # Cross-platform timeout: prefer GNU timeout/gtimeout, fallback to a pure-bash implementation # that uses background process + sleep to enforce a hard time limit. if command -v timeout &> /dev/null; then @@ -49,8 +304,20 @@ else } fi -# Get current script directory -export BASE_DIR=$(cd "$(dirname "$0")"; pwd) +# Get current script directory. +# When launched via process substitution (`bash <(curl ...)`) or a pipe, +# $0 points at /dev/fd/* or "bash", so dirname is meaningless. Fall back to +# the current working directory in that case (remote install will cd into +# the cloned project dir and reset BASE_DIR afterwards). +_script_src="$0" +case "$_script_src" in + /dev/fd/* | /proc/self/fd/* | bash | sh | -* | "") + export BASE_DIR="$(pwd)" + ;; + *) + export BASE_DIR=$(cd "$(dirname "$_script_src")" 2>/dev/null && pwd || pwd) + ;; +esac # Detect if in project directory IS_PROJECT_DIR=false @@ -151,28 +418,11 @@ clone_project() { echo -e "${GREEN}🔍 Cloning CowAgent project...${NC}" if [ -d "CowAgent" ]; then - echo -e "${YELLOW}⚠️ Directory 'CowAgent' already exists.${NC}" - read -p "Choose action: overwrite(o), backup(b), or quit(q)? [press Enter for default: b]: " choice - choice=${choice:-b} - case "$choice" in - o|O) - echo -e "${YELLOW}🗑️ Overwriting 'CowAgent' directory...${NC}" - rm -rf CowAgent - ;; - b|B) - backup_dir="CowAgent_backup_$(date +%s)" - echo -e "${YELLOW}🔀 Backing up to '$backup_dir'...${NC}" - mv CowAgent "$backup_dir" - ;; - q|Q) - echo -e "${RED}❌ Installation cancelled.${NC}" - exit 1 - ;; - *) - echo -e "${RED}❌ Invalid choice. Exiting.${NC}" - exit 1 - ;; - esac + # An existing directory is automatically backed up (no prompt) so the + # installer stays one-shot / hands-off. + local backup_dir="CowAgent_backup_$(date +%s)" + echo -e "${YELLOW}⚠️ $(t "目录 'CowAgent' 已存在,自动备份到" "Directory 'CowAgent' exists, backing up to") '$backup_dir'...${NC}" + mv CowAgent "$backup_dir" fi check_and_install_tool git @@ -188,9 +438,25 @@ clone_project() { echo -e "${RED}❌ Cannot download project. Please install Git, wget, or curl.${NC}" exit 1 fi - unzip CowAgent.zip - mv CowAgent-master CowAgent - rm CowAgent.zip + # Unzip: prefer `unzip`, otherwise fall back to Python's zipfile (no + # extra dependency) so minimal environments without unzip still work. + if command -v unzip &> /dev/null; then + unzip CowAgent.zip + elif command -v python3 &> /dev/null; then + python3 -m zipfile -e CowAgent.zip . + elif command -v python &> /dev/null; then + python -m zipfile -e CowAgent.zip . + else + echo -e "${RED}❌ Cannot extract archive. Please install 'unzip' or Python.${NC}" + exit 1 + fi + # Archive top-level dir name may vary (CowAgent-master, etc.); detect it. + local _extracted="CowAgent-master" + if [ ! -d "$_extracted" ]; then + _extracted=$(ls -d CowAgent-*/ 2>/dev/null | head -1 | sed 's:/*$::') + fi + [ -n "$_extracted" ] && [ -d "$_extracted" ] && mv "$_extracted" CowAgent + rm -f CowAgent.zip else local clone_ok=false # Detect and temporarily disable invalid git proxy settings @@ -240,15 +506,37 @@ clone_project() { # Install dependencies install_dependencies() { echo -e "${GREEN}📦 Installing dependencies...${NC}" + # Pick the pip index by install language, then fall back to the other if the + # preferred one is unreachable: + # - zh users: Tsinghua mirror first (fast in China), official PyPI fallback + # - others : official PyPI first, Tsinghua mirror fallback local PIP_MIRROR="" - if curl -s --connect-timeout 5 https://pypi.tuna.tsinghua.edu.cn/simple/ > /dev/null 2>&1; then - PIP_MIRROR="-i https://pypi.tuna.tsinghua.edu.cn/simple" + local _tuna="https://pypi.tuna.tsinghua.edu.cn/simple" + local _pypi="https://pypi.org/simple" + if [ "$UI_LANG" = "zh" ]; then + # Prefer Tsinghua; if it's down, fall back to official PyPI (pip default). + if curl -s --connect-timeout 5 "${_tuna}/" > /dev/null 2>&1; then + PIP_MIRROR="-i $_tuna" + fi + else + # Prefer official PyPI; only use Tsinghua if PyPI is unreachable. + if ! curl -s --connect-timeout 5 "${_pypi}/" > /dev/null 2>&1 \ + && curl -s --connect-timeout 5 "${_tuna}/" > /dev/null 2>&1; then + PIP_MIRROR="-i $_tuna" + fi + fi + if [ -n "$PIP_MIRROR" ]; then + echo -e "${YELLOW}Using pip mirror: ${_tuna}${NC}" fi + # Only pass --break-system-packages if this pip actually supports it + # (pip >= 23.x). Older pip versions error out with "no such option", + # which previously dumped a confusing usage message and failed the install. PIP_EXTRA_ARGS="" - if $PYTHON_CMD -c "import sys; exit(0 if sys.version_info >= (3, 11) else 1)" 2>/dev/null; then + if $PYTHON_CMD -c "import sys; exit(0 if sys.version_info >= (3, 11) else 1)" 2>/dev/null \ + && $PYTHON_CMD -m pip install --help 2>/dev/null | grep -q -- "--break-system-packages"; then PIP_EXTRA_ARGS="--break-system-packages" - echo -e "${YELLOW}Python 3.11+ detected, using --break-system-packages for pip installations${NC}" + echo -e "${YELLOW}Python 3.11+ with break-system-packages support detected${NC}" fi echo -e "${YELLOW}Upgrading pip and basic tools...${NC}" @@ -306,199 +594,221 @@ install_dependencies() { # Select model select_model() { echo "" - echo -e "${CYAN}${BOLD}=========================================${NC}" - echo -e "${CYAN}${BOLD} Select AI Model${NC}" - echo -e "${CYAN}${BOLD}=========================================${NC}" - echo -e "${YELLOW}1) DeepSeek (deepseek-v4-flash, deepseek-v4-pro, etc.)${NC}" - echo -e "${YELLOW}2) MiniMax (MiniMax-M2.7, MiniMax-M2.5, etc.)${NC}" - echo -e "${YELLOW}3) Claude (claude-opus-4-8, claude-opus-4-7, claude-sonnet-4-6, etc.)${NC}" - echo -e "${YELLOW}4) Gemini (gemini-3.1-flash-lite-preview, gemini-3.1-pro-preview, etc.)${NC}" - echo -e "${YELLOW}5) OpenAI GPT (gpt-5.4, gpt-5.2, gpt-4.1, etc.)${NC}" - echo -e "${YELLOW}6) Zhipu AI (glm-5.1, glm-5-turbo, glm-5, etc.)${NC}" - echo -e "${YELLOW}7) Qwen (qwen3.6-plus, qwen3.5-plus, qwen3-max, qwq-plus, etc.)${NC}" - echo -e "${YELLOW}8) Doubao (doubao-seed-2-0-code-preview-260215, etc.)${NC}" - echo -e "${YELLOW}9) Kimi (kimi-k2.6, kimi-k2.5, kimi-k2, etc.)${NC}" - echo -e "${YELLOW}10) LinkAI (access multiple models via one API)${NC}" - echo "" - - while true; do - read -p "Enter your choice [press Enter for default: 1 - DeepSeek]: " model_choice - model_choice=${model_choice:-1} - case "$model_choice" in - 1|2|3|4|5|6|7|8|9|10) - break - ;; - *) - echo -e "${RED}Invalid choice. Please enter 1-10.${NC}" - ;; - esac - done + local title sel + title="$(t "选择 AI 模型" "Select AI Model")" + # The 11th option is "skip" -> configure later in the web console. + select_menu sel "$title" \ + "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)")" + model_choice="$sel" } # Read model config: provider, default_model, key_variable_name read_model_config() { local provider=$1 default_model=$2 key_var=$3 - echo -e "${GREEN}Configuring ${provider}...${NC}" - read -p "Enter ${provider} API Key: " _api_key - read -p "Enter model name [press Enter for default: ${default_model}]: " model_name - model_name=${model_name:-$default_model} - MODEL_NAME="$model_name" - eval "${key_var}=\"\$_api_key\"" + echo -e "${GREEN}$(t "正在配置" "Configuring") ${provider}...${NC}" + # Only ask for the API key here; the model name and API base default to + # sensible values and can be changed later in the web console. + local _api_key + tty_read _api_key "$(t "请输入" "Enter") ${provider} API Key ($(t "回车跳过,稍后在 Web 控制台填写" "press Enter to skip, set later in web console")): " + MODEL_NAME="$default_model" + # printf -v (not eval) so keys containing quotes/backticks/$() are safe. + printf -v "${key_var}" '%s' "$_api_key" } -# Read optional API base URL -read_api_base() { - local base_var=$1 default_url=$2 - read -p "Enter API Base URL [press Enter for default: ${default_url}]: " api_base - api_base=${api_base:-$default_url} - eval "${base_var}=\"\$api_base\"" -} - -# Configure model +# Configure model. The "skip" choice leaves the model empty so the user can +# finish configuration in the web console after first start. configure_model() { case "$model_choice" in 1) read_model_config "DeepSeek" "deepseek-v4-flash" "DEEPSEEK_KEY" ;; - 2) read_model_config "MiniMax" "MiniMax-M2.7" "MINIMAX_KEY" ;; - 3) - read_model_config "Claude" "claude-opus-4-8" "CLAUDE_KEY" - read_api_base "CLAUDE_BASE" "https://api.anthropic.com/v1" - ;; - 4) - read_model_config "Gemini" "gemini-3.1-pro-preview" "GEMINI_KEY" - read_api_base "GEMINI_BASE" "https://generativelanguage.googleapis.com" - ;; - 5) - read_model_config "OpenAI GPT" "gpt-5.4" "OPENAI_KEY" - read_api_base "OPENAI_BASE" "https://api.openai.com/v1" - ;; + 2) read_model_config "Claude" "claude-opus-4-8" "CLAUDE_KEY" ;; + 3) read_model_config "Gemini" "gemini-3.1-pro-preview" "GEMINI_KEY" ;; + 4) read_model_config "OpenAI GPT" "gpt-5.4" "OPENAI_KEY" ;; + 5) read_model_config "MiniMax" "MiniMax-M2.7" "MINIMAX_KEY" ;; 6) read_model_config "Zhipu AI" "glm-5.1" "ZHIPU_KEY" ;; 7) read_model_config "Qwen (DashScope)" "qwen3.6-plus" "DASHSCOPE_KEY" ;; 8) read_model_config "Doubao (Volcengine Ark)" "doubao-seed-2-0-code-preview-260215" "ARK_KEY" ;; 9) read_model_config "Kimi (Moonshot)" "kimi-k2.6" "MOONSHOT_KEY" ;; 10) + # Show where to obtain a LinkAI key (zh users -> console page). + echo -e "${CYAN}$(t "获取 LinkAI Key" "Get your LinkAI Key"): https://link-ai.tech/console/interface${NC}" read_model_config "LinkAI" "deepseek-v4-flash" "LINKAI_KEY" USE_LINKAI="true" ;; + 11) + # Skip: leave model unset, will be configured in web console + MODEL_SKIPPED="true" + MODEL_NAME="" + echo -e "${YELLOW}$(t "已跳过模型配置,稍后可在 Web 控制台填写" "Model configuration skipped, you can set it later in the web console")${NC}" + ;; esac } -# Select channel -select_channel() { - echo "" - echo -e "${CYAN}${BOLD}=========================================${NC}" - echo -e "${CYAN}${BOLD} Select Communication Channel${NC}" - echo -e "${CYAN}${BOLD}=========================================${NC}" - echo -e "${YELLOW}1) Weixin (微信)${NC}" - echo -e "${YELLOW}2) Feishu (飞书)${NC}" - echo -e "${YELLOW}3) DingTalk (钉钉)${NC}" - echo -e "${YELLOW}4) WeCom Bot (企微智能机器人)${NC}" - echo -e "${YELLOW}5) QQ (QQ 机器人)${NC}" - echo -e "${YELLOW}6) WeCom App (企微自建应用)${NC}" - echo -e "${YELLOW}7) Web (网页)${NC}" - echo "" - - while true; do - read -p "Enter your choice [press Enter for default: 1 - Weixin]: " channel_choice - channel_choice=${channel_choice:-1} - case "$channel_choice" in - 1|2|3|4|5|6|7) - break - ;; - *) - echo -e "${RED}Invalid choice. Please enter 1-7.${NC}" - ;; - esac - done +# Channel label by stable key (independent of menu order). +channel_label() { + case "$1" in + web) t "Web 网页控制台(推荐,开箱即用)" "Web Console (recommended, ready to use)" ;; + weixin) t "微信" "WeChat (Weixin)" ;; + feishu) t "飞书" "Feishu / Lark" ;; + dingtalk) t "钉钉" "DingTalk" ;; + wecom_bot) t "企微智能机器人" "WeCom Bot" ;; + qq) printf '%s' "QQ" ;; + wechatcom_app) t "企微自建应用" "WeCom App" ;; + telegram) printf '%s' "Telegram" ;; + slack) printf '%s' "Slack" ;; + discord) printf '%s' "Discord" ;; + skip) t "⏭ 跳过(稍后在 Web 控制台配置)" "⏭ Skip (configure later in the web console)" ;; + esac } -# Configure channel +# 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 (CHANNEL_KEYS) decouples the menu order from the config +# logic, so reordering the menu never breaks configure_channel(). +select_channel() { + echo "" + local title sel + title="$(t "选择接入渠道" "Select Communication Channel")" + if [ "$UI_LANG" = "en" ]; then + CHANNEL_KEYS=(web telegram discord slack weixin feishu dingtalk wecom_bot qq wechatcom_app skip) + else + CHANNEL_KEYS=(web weixin feishu dingtalk wecom_bot qq wechatcom_app telegram slack discord skip) + fi + local labels=() k + for k in "${CHANNEL_KEYS[@]}"; do + labels+=("$(channel_label "$k")") + done + select_menu sel "$title" "${labels[@]}" + # Map the 1-based menu position back to the stable channel key. + channel_choice="${CHANNEL_KEYS[$((sel - 1))]}" +} + +# Configure channel, dispatched by stable channel key (not menu position). configure_channel() { case "$channel_choice" in - 1) + web|skip) + # Web (also the default when skipped). Use the default port with + # no prompt; it can be changed later in the web console / config. + CHANNEL_TYPE="web" + WEB_PORT="9899" + ACCESS_INFO="$(t "Web 控制台地址" "Web console") : http://localhost:9899/chat" + ;; + weixin) # Weixin CHANNEL_TYPE="weixin" - ACCESS_INFO="Weixin channel configured. Scan QR code in terminal or web console to login." + ACCESS_INFO="$(t "微信渠道已配置,请在终端或 Web 控制台扫码登录" "Weixin channel configured. Scan QR code in terminal or web console to login.")" ;; - 2) + feishu) # Feishu (WebSocket mode) CHANNEL_TYPE="feishu" - echo -e "${GREEN}Configure Feishu (WebSocket mode)...${NC}" - read -p "Enter Feishu App ID: " fs_app_id - read -p "Enter Feishu App Secret: " fs_app_secret - + echo -e "${GREEN}$(t "配置飞书(WebSocket 模式)" "Configure Feishu (WebSocket mode)")...${NC}" + local fs_app_id fs_app_secret + tty_read fs_app_id "$(t "请输入飞书 App ID" "Enter Feishu App ID"): " + tty_read fs_app_secret "$(t "请输入飞书 App Secret" "Enter Feishu App Secret"): " FEISHU_APP_ID="$fs_app_id" FEISHU_APP_SECRET="$fs_app_secret" FEISHU_EVENT_MODE="websocket" - ACCESS_INFO="Feishu channel configured (WebSocket mode)" + ACCESS_INFO="$(t "飞书渠道已配置(WebSocket 模式)" "Feishu channel configured (WebSocket mode)")" ;; - 3) + dingtalk) # DingTalk CHANNEL_TYPE="dingtalk" - echo -e "${GREEN}Configure DingTalk...${NC}" - read -p "Enter DingTalk Client ID: " dt_client_id - read -p "Enter DingTalk Client Secret: " dt_client_secret - + echo -e "${GREEN}$(t "配置钉钉" "Configure DingTalk")...${NC}" + local dt_client_id dt_client_secret + tty_read dt_client_id "$(t "请输入钉钉 Client ID" "Enter DingTalk Client ID"): " + tty_read dt_client_secret "$(t "请输入钉钉 Client Secret" "Enter DingTalk Client Secret"): " DT_CLIENT_ID="$dt_client_id" DT_CLIENT_SECRET="$dt_client_secret" - ACCESS_INFO="DingTalk channel configured" + ACCESS_INFO="$(t "钉钉渠道已配置" "DingTalk channel configured")" ;; - 4) + wecom_bot) # WeCom Bot CHANNEL_TYPE="wecom_bot" - echo -e "${GREEN}Configure WeCom Bot...${NC}" - read -p "Enter WeCom Bot ID: " wecom_bot_id - read -p "Enter WeCom Bot Secret: " wecom_bot_secret - + echo -e "${GREEN}$(t "配置企微智能机器人" "Configure WeCom Bot")...${NC}" + local wecom_bot_id wecom_bot_secret + tty_read wecom_bot_id "$(t "请输入 WeCom Bot ID" "Enter WeCom Bot ID"): " + tty_read wecom_bot_secret "$(t "请输入 WeCom Bot Secret" "Enter WeCom Bot Secret"): " WECOM_BOT_ID="$wecom_bot_id" WECOM_BOT_SECRET="$wecom_bot_secret" - ACCESS_INFO="WeCom Bot channel configured" + ACCESS_INFO="$(t "企微智能机器人渠道已配置" "WeCom Bot channel configured")" ;; - 5) + qq) # QQ CHANNEL_TYPE="qq" - echo -e "${GREEN}Configure QQ Bot...${NC}" - read -p "Enter QQ App ID: " qq_app_id - read -p "Enter QQ App Secret: " qq_app_secret - + echo -e "${GREEN}$(t "配置 QQ 机器人" "Configure QQ Bot")...${NC}" + local qq_app_id qq_app_secret + tty_read qq_app_id "$(t "请输入 QQ App ID" "Enter QQ App ID"): " + tty_read qq_app_secret "$(t "请输入 QQ App Secret" "Enter QQ App Secret"): " QQ_APP_ID="$qq_app_id" QQ_APP_SECRET="$qq_app_secret" - ACCESS_INFO="QQ Bot channel configured" + ACCESS_INFO="$(t "QQ 机器人渠道已配置" "QQ Bot channel configured")" ;; - 6) + wechatcom_app) # WeCom App CHANNEL_TYPE="wechatcom_app" - echo -e "${GREEN}Configure WeCom App...${NC}" - read -p "Enter WeChat Corp ID: " corp_id - read -p "Enter WeChat Com App Token: " com_token - read -p "Enter WeChat Com App Secret: " com_secret - read -p "Enter WeChat Com App Agent ID: " com_agent_id - read -p "Enter WeChat Com App AES Key: " com_aes_key - read -p "Enter WeChat Com App Port [press Enter for default: 9898]: " com_port + echo -e "${GREEN}$(t "配置企微自建应用" "Configure WeCom App")...${NC}" + local corp_id com_token com_secret com_agent_id com_aes_key com_port + tty_read corp_id "$(t "请输入企业 Corp ID" "Enter WeChat Corp ID"): " + tty_read com_token "$(t "请输入应用 Token" "Enter WeChat Com App Token"): " + tty_read com_secret "$(t "请输入应用 Secret" "Enter WeChat Com App Secret"): " + tty_read com_agent_id "$(t "请输入应用 Agent ID" "Enter WeChat Com App Agent ID"): " + tty_read com_aes_key "$(t "请输入应用 AES Key" "Enter WeChat Com App AES Key"): " + tty_read com_port "$(t "请输入应用端口" "Enter WeChat Com App Port") [$(t "默认" "default"): 9898]: " com_port=${com_port:-9898} - WECHATCOM_CORP_ID="$corp_id" WECHATCOM_TOKEN="$com_token" WECHATCOM_SECRET="$com_secret" WECHATCOM_AGENT_ID="$com_agent_id" WECHATCOM_AES_KEY="$com_aes_key" WECHATCOM_PORT="$com_port" - ACCESS_INFO="WeCom App channel configured on port ${com_port}" + ACCESS_INFO="$(t "企微自建应用渠道已配置,端口" "WeCom App channel configured on port") ${com_port}" ;; - 7) - # Web - CHANNEL_TYPE="web" - read -p "Enter web port [press Enter for default: 9899]: " web_port - web_port=${web_port:-9899} - - WEB_PORT="$web_port" - ACCESS_INFO="Web interface will be available at: http://localhost:${web_port}/chat" + telegram) + # Telegram + CHANNEL_TYPE="telegram" + echo -e "${GREEN}$(t "配置 Telegram" "Configure Telegram")...${NC}" + local tg_token + tty_read tg_token "$(t "请输入 Telegram Bot Token" "Enter Telegram Bot Token"): " + TELEGRAM_TOKEN="$tg_token" + ACCESS_INFO="$(t "Telegram 渠道已配置" "Telegram channel configured")" + ;; + slack) + # Slack + CHANNEL_TYPE="slack" + echo -e "${GREEN}$(t "配置 Slack" "Configure Slack")...${NC}" + local slack_bot slack_app + tty_read slack_bot "$(t "请输入 Slack Bot Token" "Enter Slack Bot Token") (xoxb-...): " + tty_read slack_app "$(t "请输入 Slack App Token" "Enter Slack App Token") (xapp-...): " + SLACK_BOT_TOKEN="$slack_bot" + SLACK_APP_TOKEN="$slack_app" + ACCESS_INFO="$(t "Slack 渠道已配置" "Slack channel configured")" + ;; + discord) + # Discord + CHANNEL_TYPE="discord" + echo -e "${GREEN}$(t "配置 Discord" "Configure Discord")...${NC}" + local discord_token + tty_read discord_token "$(t "请输入 Discord Bot Token" "Enter Discord Bot Token"): " + DISCORD_TOKEN="$discord_token" + ACCESS_INFO="$(t "Discord 渠道已配置" "Discord channel configured")" ;; esac } # Generate config file create_config_file() { - echo -e "${GREEN}📝 Generating config.json...${NC}" + echo -e "${GREEN}📝 $(t "正在生成 config.json" "Generating config.json")...${NC}" CHANNEL_TYPE="$CHANNEL_TYPE" \ MODEL_NAME="$MODEL_NAME" \ @@ -532,12 +842,18 @@ create_config_file() { WECHATCOM_AGENT_ID="${WECHATCOM_AGENT_ID:-}" \ WECHATCOM_AES_KEY="${WECHATCOM_AES_KEY:-}" \ WECHATCOM_PORT="${WECHATCOM_PORT:-}" \ + TELEGRAM_TOKEN="${TELEGRAM_TOKEN:-}" \ + SLACK_BOT_TOKEN="${SLACK_BOT_TOKEN:-}" \ + SLACK_APP_TOKEN="${SLACK_APP_TOKEN:-}" \ + DISCORD_TOKEN="${DISCORD_TOKEN:-}" \ + COW_LANG="${INSTALL_LANG:-auto}" \ $PYTHON_CMD -c " import json, os e = os.environ.get base = { - 'channel_type': e('CHANNEL_TYPE'), - 'model': e('MODEL_NAME'), + 'channel_type': e('CHANNEL_TYPE') or 'web', + 'model': e('MODEL_NAME') or '', + 'cow_lang': e('COW_LANG', 'auto'), 'open_ai_api_key': e('OPENAI_KEY', ''), 'open_ai_api_base': e('OPENAI_BASE'), 'claude_api_key': e('CLAUDE_KEY', ''), @@ -571,19 +887,28 @@ channel_map = { 'wecom_bot': {'wecom_bot_id': 'WECOM_BOT_ID', 'wecom_bot_secret': 'WECOM_BOT_SECRET'}, 'qq': {'qq_app_id': 'QQ_APP_ID', 'qq_app_secret': 'QQ_APP_SECRET'}, 'wechatcom_app': {'wechatcom_corp_id': 'WECHATCOM_CORP_ID', 'wechatcomapp_token': 'WECHATCOM_TOKEN', 'wechatcomapp_secret': 'WECHATCOM_SECRET', 'wechatcomapp_agent_id': 'WECHATCOM_AGENT_ID', 'wechatcomapp_aes_key': 'WECHATCOM_AES_KEY', 'wechatcomapp_port': ('WECHATCOM_PORT', int)}, + 'telegram': {'telegram_token': 'TELEGRAM_TOKEN'}, + 'slack': {'slack_bot_token': 'SLACK_BOT_TOKEN', 'slack_app_token': 'SLACK_APP_TOKEN'}, + 'discord': {'discord_token': 'DISCORD_TOKEN'}, } -ch = e('CHANNEL_TYPE') +def _to_int(val, default): + try: + return int(val) + except (TypeError, ValueError): + return default +ch = e('CHANNEL_TYPE') or 'web' for key, spec in channel_map.get(ch, {}).items(): if isinstance(spec, tuple): env_name, conv = spec - base[key] = conv(e(env_name)) + # Guard int() against non-numeric input; fall back to a sane port. + base[key] = _to_int(e(env_name), 9899 if key == 'web_port' else 9898) if conv is int else conv(e(env_name)) else: base[key] = e(spec, '') with open('config.json', 'w') as f: json.dump(base, f, indent=2, ensure_ascii=False) " - echo -e "${GREEN}✅ Configuration file created successfully.${NC}" + echo -e "${GREEN}✅ $(t "配置文件创建成功" "Configuration file created successfully").${NC}" } # Start project @@ -622,29 +947,37 @@ start_project() { sleep 2 echo "" echo -e "${CYAN}${BOLD}=========================================${NC}" - echo -e "${GREEN}${EMOJI_CHECK} CowAgent is now running in background!${NC}" - echo -e "${GREEN}${EMOJI_CHECK} Process will continue after closing terminal.${NC}" + echo -e "${GREEN}${EMOJI_CHECK} $(t "CowAgent 已在后台运行" "CowAgent is now running in background")!${NC}" + echo -e "${GREEN}${EMOJI_CHECK} $(t "关闭终端后进程仍会继续运行" "Process will continue after closing terminal").${NC}" echo -e "${CYAN}$ACCESS_INFO${NC}" + + # If the model was skipped, guide the user to finish setup in the web console. + if [ "${MODEL_SKIPPED:-}" = "true" ]; then + local _port="${WEB_PORT:-9899}" + echo "" + echo -e "${YELLOW}${EMOJI_WARN} $(t "尚未配置模型,请在 Web 控制台完成配置" "Model not configured yet, please finish setup in the web console"):${NC}" + echo -e "${CYAN} http://localhost:${_port}/chat${NC}" + fi echo "" - echo -e "${CYAN}${BOLD}Management Commands:${NC}" + echo -e "${CYAN}${BOLD}$(t "管理命令" "Management Commands"):${NC}" if $USE_COW; then - echo -e " ${GREEN}cow stop${NC} Stop the service" - echo -e " ${GREEN}cow restart${NC} Restart the service" - echo -e " ${GREEN}cow status${NC} Check status" - echo -e " ${GREEN}cow logs${NC} View logs" - echo -e " ${GREEN}cow update${NC} Update and restart" - echo -e " ${GREEN}cow install-browser${NC} Install browser tool" + echo -e " ${GREEN}cow stop${NC} $(t "停止服务" "Stop the service")" + echo -e " ${GREEN}cow restart${NC} $(t "重启服务" "Restart the service")" + echo -e " ${GREEN}cow status${NC} $(t "查看状态" "Check status")" + echo -e " ${GREEN}cow logs${NC} $(t "查看日志" "View logs")" + echo -e " ${GREEN}cow update${NC} $(t "更新并重启" "Update and restart")" + echo -e " ${GREEN}cow install-browser${NC} $(t "安装浏览器工具" "Install browser tool")" else - echo -e " ${GREEN}./run.sh stop${NC} Stop the service" - echo -e " ${GREEN}./run.sh restart${NC} Restart the service" - echo -e " ${GREEN}./run.sh status${NC} Check status" - echo -e " ${GREEN}./run.sh logs${NC} View logs" - echo -e " ${GREEN}./run.sh update${NC} Update and restart" + echo -e " ${GREEN}./run.sh stop${NC} $(t "停止服务" "Stop the service")" + echo -e " ${GREEN}./run.sh restart${NC} $(t "重启服务" "Restart the service")" + echo -e " ${GREEN}./run.sh status${NC} $(t "查看状态" "Check status")" + echo -e " ${GREEN}./run.sh logs${NC} $(t "查看日志" "View logs")" + echo -e " ${GREEN}./run.sh update${NC} $(t "更新并重启" "Update and restart")" fi echo -e "${CYAN}${BOLD}=========================================${NC}" echo "" - echo -e "${YELLOW}Showing recent logs (Ctrl+C to exit, agent keeps running):${NC}" + echo -e "${YELLOW}$(t "显示最近日志(Ctrl+C 退出,Agent 继续运行)" "Showing recent logs (Ctrl+C to exit, agent keeps running)"):${NC}" sleep 2 tail -n 30 -f "${BASE_DIR}/nohup.out" } @@ -655,20 +988,20 @@ show_usage() { echo -e "${CYAN}${BOLD} ${EMOJI_COW} CowAgent Management Script${NC}" echo -e "${CYAN}${BOLD}=========================================${NC}" echo "" - echo -e "${YELLOW}Usage:${NC}" - echo -e " ${GREEN}./run.sh${NC} ${CYAN}# Install/Configure project${NC}" - echo -e " ${GREEN}./run.sh ${NC} ${CYAN}# Execute management command${NC}" + echo -e "${YELLOW}$(t "用法" "Usage"):${NC}" + echo -e " ${GREEN}./run.sh${NC} ${CYAN}# $(t "安装/配置项目" "Install/Configure project")${NC}" + echo -e " ${GREEN}./run.sh ${NC} ${CYAN}# $(t "执行管理命令" "Execute management command")${NC}" echo "" - echo -e "${YELLOW}Commands:${NC}" - echo -e " ${GREEN}start${NC} Start the service" - echo -e " ${GREEN}stop${NC} Stop the service" - echo -e " ${GREEN}restart${NC} Restart the service" - echo -e " ${GREEN}status${NC} Check service status" - echo -e " ${GREEN}logs${NC} View logs (tail -f)" - echo -e " ${GREEN}config${NC} Reconfigure project" - echo -e " ${GREEN}update${NC} Update and restart" + echo -e "${YELLOW}$(t "命令" "Commands"):${NC}" + echo -e " ${GREEN}start${NC} $(t "启动服务" "Start the service")" + echo -e " ${GREEN}stop${NC} $(t "停止服务" "Stop the service")" + echo -e " ${GREEN}restart${NC} $(t "重启服务" "Restart the service")" + echo -e " ${GREEN}status${NC} $(t "查看服务状态" "Check service status")" + echo -e " ${GREEN}logs${NC} $(t "查看日志 (tail -f)" "View logs (tail -f)")" + echo -e " ${GREEN}config${NC} $(t "重新配置项目" "Reconfigure project")" + echo -e " ${GREEN}update${NC} $(t "更新并重启" "Update and restart")" echo "" - echo -e "${YELLOW}Examples:${NC}" + echo -e "${YELLOW}$(t "示例" "Examples"):${NC}" echo -e " ${GREEN}./run.sh start${NC}" echo -e " ${GREEN}./run.sh logs${NC}" echo -e " ${GREEN}./run.sh status${NC}" @@ -701,8 +1034,8 @@ has_cow() { # Start service cmd_start() { if [ ! -f "${BASE_DIR}/config.json" ]; then - echo -e "${RED}${EMOJI_CROSS} config.json not found${NC}" - echo -e "${YELLOW}Please run './run.sh' to configure first${NC}" + echo -e "${RED}${EMOJI_CROSS} $(t "未找到 config.json" "config.json not found")${NC}" + echo -e "${YELLOW}$(t "请先运行 './run.sh' 进行配置" "Please run './run.sh' to configure first")${NC}" exit 1 fi @@ -711,8 +1044,8 @@ cmd_start() { cow start else if is_running; then - echo -e "${YELLOW}${EMOJI_WARN} CowAgent is already running (PID: $(get_pid))${NC}" - echo -e "${YELLOW}Use './run.sh restart' to restart${NC}" + echo -e "${YELLOW}${EMOJI_WARN} $(t "CowAgent 已在运行中" "CowAgent is already running") (PID: $(get_pid))${NC}" + echo -e "${YELLOW}$(t "使用 './run.sh restart' 重启" "Use './run.sh restart' to restart")${NC}" return fi check_python_version @@ -722,34 +1055,37 @@ cmd_start() { # Stop service cmd_stop() { + # Don't let kill/return non-zero (e.g. process already gone) abort the + # caller (cmd_restart) under `set -e`. + set +e if has_cow; then cd "${BASE_DIR}" cow stop else - echo -e "${GREEN}${EMOJI_STOP} Stopping CowAgent...${NC}" + echo -e "${GREEN}${EMOJI_STOP} $(t "正在停止 CowAgent" "Stopping CowAgent")...${NC}" if ! is_running; then - echo -e "${YELLOW}${EMOJI_WARN} CowAgent is not running${NC}" - return + echo -e "${YELLOW}${EMOJI_WARN} $(t "CowAgent 未在运行" "CowAgent is not running")${NC}" + return 0 fi pid=$(get_pid) if [ -z "$pid" ] || ! echo "$pid" | grep -qE '^[0-9]+$'; then - echo -e "${RED}❌ Failed to get valid PID (got: ${pid})${NC}" - return 1 + echo -e "${RED}❌ $(t "获取有效 PID 失败" "Failed to get valid PID") (${pid})${NC}" + return 0 fi - echo -e "${GREEN}Found running process (PID: ${pid})${NC}" + echo -e "${GREEN}$(t "找到运行中的进程" "Found running process") (PID: ${pid})${NC}" - kill ${pid} + kill ${pid} 2>/dev/null || true sleep 3 if ps -p ${pid} > /dev/null 2>&1; then - echo -e "${YELLOW}⚠️ Process not stopped, forcing termination...${NC}" - kill -9 ${pid} + echo -e "${YELLOW}⚠️ $(t "进程未停止,强制终止" "Process not stopped, forcing termination")...${NC}" + kill -9 ${pid} 2>/dev/null || true fi - echo -e "${GREEN}${EMOJI_CHECK} CowAgent stopped${NC}" + echo -e "${GREEN}${EMOJI_CHECK} $(t "CowAgent 已停止" "CowAgent stopped")${NC}" fi } @@ -777,20 +1113,21 @@ cmd_status() { if is_running; then pid=$(get_pid) - echo -e "${GREEN}Status:${NC} ✅ Running" + echo -e "${GREEN}$(t "状态" "Status"):${NC} ✅ $(t "运行中" "Running")" echo -e "${GREEN}PID:${NC} ${pid}" if [ -f "${BASE_DIR}/nohup.out" ]; then - echo -e "${GREEN}Logs:${NC} ${BASE_DIR}/nohup.out" + echo -e "${GREEN}$(t "日志" "Logs"):${NC} ${BASE_DIR}/nohup.out" fi else - echo -e "${YELLOW}Status:${NC} ⭐ Stopped" + echo -e "${YELLOW}$(t "状态" "Status"):${NC} ⭐ $(t "已停止" "Stopped")" fi if [ -f "${BASE_DIR}/config.json" ]; then - model=$(grep -o '"model"[[:space:]]*:[[:space:]]*"[^"]*"' "${BASE_DIR}/config.json" | cut -d'"' -f4) - channel=$(grep -o '"channel_type"[[:space:]]*:[[:space:]]*"[^"]*"' "${BASE_DIR}/config.json" | cut -d'"' -f4) - echo -e "${GREEN}Model:${NC} ${model}" - echo -e "${GREEN}Channel:${NC} ${channel}" + # `|| true`: grep returns 1 when the key is absent (set -e safe). + model=$(grep -o '"model"[[:space:]]*:[[:space:]]*"[^"]*"' "${BASE_DIR}/config.json" 2>/dev/null | cut -d'"' -f4 || true) + channel=$(grep -o '"channel_type"[[:space:]]*:[[:space:]]*"[^"]*"' "${BASE_DIR}/config.json" 2>/dev/null | cut -d'"' -f4 || true) + echo -e "${GREEN}$(t "模型" "Model"):${NC} ${model:-$(t "(未配置)" "(not set)")}" + echo -e "${GREEN}$(t "渠道" "Channel"):${NC} ${channel:-$(t "(未配置)" "(not set)")}" fi echo -e "${CYAN}${BOLD}=========================================${NC}" @@ -804,22 +1141,30 @@ cmd_logs() { cow logs -f else if [ -f "${BASE_DIR}/nohup.out" ]; then - echo -e "${YELLOW}Viewing logs (Ctrl+C to exit):${NC}" + echo -e "${YELLOW}$(t "查看日志(Ctrl+C 退出)" "Viewing logs (Ctrl+C to exit)"):${NC}" tail -f "${BASE_DIR}/nohup.out" else - echo -e "${RED}❌ Log file not found: ${BASE_DIR}/nohup.out${NC}" + echo -e "${RED}❌ $(t "日志文件未找到" "Log file not found"): ${BASE_DIR}/nohup.out${NC}" fi fi } # Reconfigure cmd_config() { - echo -e "${YELLOW}${EMOJI_WRENCH} Reconfiguring CowAgent...${NC}" + # Interactive flow: disable `set -e` (see install_mode for rationale). + set +e + # One shared terminal handle for all menus in this session. + menu_session_begin + + # Choose language first so the rest of the flow is localized. + select_language + echo "" + echo -e "${YELLOW}${EMOJI_WRENCH} $(t "正在重新配置 CowAgent" "Reconfiguring CowAgent")...${NC}" if [ -f "${BASE_DIR}/config.json" ]; then backup_file="${BASE_DIR}/config.json.backup.$(date +%s)" cp "${BASE_DIR}/config.json" "${backup_file}" - echo -e "${GREEN}✅ Backed up config to: ${backup_file}${NC}" + echo -e "${GREEN}✅ $(t "已备份配置到" "Backed up config to"): ${backup_file}${NC}" fi check_python_version @@ -828,10 +1173,12 @@ cmd_config() { configure_model select_channel configure_channel + menu_session_end create_config_file echo "" - read -p "Restart service now? [Y/n]: " restart_now + local restart_now + tty_read restart_now "$(t "现在重启服务" "Restart service now")? [Y/n]: " if [[ ! $restart_now == [Nn]* ]]; then cmd_restart fi @@ -839,27 +1186,27 @@ cmd_config() { # Update project cmd_update() { - echo -e "${GREEN}${EMOJI_WRENCH} Updating CowAgent...${NC}" + echo -e "${GREEN}${EMOJI_WRENCH} $(t "正在更新 CowAgent" "Updating CowAgent")...${NC}" cd "${BASE_DIR}" # Pull latest code first (service still running) local pull_ok=false if [ -d .git ]; then - echo -e "${GREEN}🔄 Pulling latest code...${NC}" + echo -e "${GREEN}🔄 $(t "正在拉取最新代码" "Pulling latest code")...${NC}" if git pull; then pull_ok=true else - echo -e "${YELLOW}⚠️ git pull failed, trying Gitee mirror...${NC}" + echo -e "${YELLOW}⚠️ $(t "git pull 失败,尝试 Gitee 镜像" "git pull failed, trying Gitee mirror")...${NC}" git remote set-url origin https://gitee.com/zhayujie/CowAgent.git if git pull; then pull_ok=true else - echo -e "${RED}❌ Failed to pull code. Update aborted.${NC}" + echo -e "${RED}❌ $(t "拉取代码失败,更新已中止" "Failed to pull code. Update aborted").${NC}" exit 1 fi fi else - echo -e "${YELLOW}⚠️ Not a git repository, skipping code update${NC}" + echo -e "${YELLOW}⚠️ $(t "非 git 仓库,跳过代码更新" "Not a git repository, skipping code update")${NC}" fi # Re-exec with the updated run.sh to pick up new logic @@ -885,24 +1232,38 @@ cmd_post_update() { # Installation mode install_mode() { + # Interactive flow: disable `set -e` so a single non-zero command (e.g. an + # arithmetic `(( ))` evaluating to 0, a `read` hitting EOF, or an optional + # step failing) does not silently abort the whole installer. + set +e clear echo -e "${CYAN}${BOLD}=========================================${NC}" echo -e "${CYAN}${BOLD} ${EMOJI_COW} CowAgent Installation${NC}" echo -e "${CYAN}${BOLD}=========================================${NC}" echo "" + + # Open one shared terminal handle for ALL menus in this session (language, + # model, channel). One long-lived fd 3 avoids per-menu re-open issues on + # bash 3.2. Closed on early return and before config generation. + menu_session_begin + + # Step 0: choose the install/UI language. Everything after this is localized. + select_language + echo "" sleep 1 if [ "$IS_PROJECT_DIR" = true ]; then - echo -e "${GREEN}✅ Detected existing project directory.${NC}" + echo -e "${GREEN}✅ $(t "检测到已有项目目录" "Detected existing project directory").${NC}" if [ -f "${BASE_DIR}/config.json" ]; then - echo -e "${GREEN}✅ Project already configured${NC}" + menu_session_end + echo -e "${GREEN}✅ $(t "项目已配置" "Project already configured")${NC}" echo "" show_usage return fi - echo -e "${YELLOW}📝 No config.json found. Let's configure your project!${NC}" + echo -e "${YELLOW}📝 $(t "未找到 config.json,开始配置项目" "No config.json found. Let's configure your project")!${NC}" echo "" # Project directory already exists, skip clone @@ -919,34 +1280,44 @@ install_mode() { configure_model select_channel configure_channel + menu_session_end create_config_file + # Auto-start after configuration for a true out-of-the-box experience. echo "" - read -p "Start CowAgent now? [Y/n]: " start_now - if [[ ! $start_now == [Nn]* ]]; then - start_project - else - echo -e "${GREEN}✅ Installation complete!${NC}" - echo "" - echo -e "${CYAN}${BOLD}To start manually:${NC}" - echo -e "${YELLOW} cd ${BASE_DIR}${NC}" - echo -e "${YELLOW} ./run.sh start${NC}" - echo "" - echo -e "${CYAN}Or use nohup directly:${NC}" - echo -e "${YELLOW} nohup $PYTHON_CMD app.py > nohup.out 2>&1 & tail -f nohup.out${NC}" - fi + start_project } # Require running inside the project directory require_project_dir() { if [ "$IS_PROJECT_DIR" = false ]; then - echo -e "${RED}${EMOJI_CROSS} Must run in project directory${NC}" + echo -e "${RED}${EMOJI_CROSS} $(t "必须在项目目录下运行" "Must run in project directory")${NC}" exit 1 fi } +# Initialize UI_LANG for management commands: prefer cow_lang from an existing +# config.json, otherwise fall back to environment detection. The install flow +# overrides this later via select_language(). +init_ui_lang() { + [ -n "$UI_LANG" ] && return + local cfg_lang="" + if [ -f "${BASE_DIR}/config.json" ]; then + # `|| true`: grep returns 1 when cow_lang is absent, which would abort + # the whole script under `set -e` at the very first management command. + cfg_lang=$(grep -o '"cow_lang"[[:space:]]*:[[:space:]]*"[^"]*"' "${BASE_DIR}/config.json" 2>/dev/null | cut -d'"' -f4 || true) + fi + case "$cfg_lang" in + zh) UI_LANG="zh" ;; + en) UI_LANG="en" ;; + *) UI_LANG=$(detect_ui_lang) ;; + esac +} + # Main function main() { + init_ui_lang + case "$1" in start|stop|restart|status|logs|config|update|_post_update) require_project_dir @@ -969,7 +1340,7 @@ main() { install_mode ;; *) - echo -e "${RED}${EMOJI_CROSS} Unknown command: $1${NC}" + echo -e "${RED}${EMOJI_CROSS} $(t "未知命令" "Unknown command"): $1${NC}" echo "" show_usage exit 1 diff --git a/scripts/run.ps1 b/scripts/run.ps1 index 7c5f0b06..f2171258 100644 --- a/scripts/run.ps1 +++ b/scripts/run.ps1 @@ -18,10 +18,19 @@ param( $ErrorActionPreference = "Stop" -# ── ensure UTF-8 console encoding on Windows ───────────────────── -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +# ── ensure UTF-8 everywhere on Windows ─────────────────────────── +# 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" -chcp 65001 | Out-Null # ── colours ────────────────────────────────────────────────────── 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-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 -> 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 ───────────────────────────────────── $ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path } $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") } +# 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 ───────────────────────────────────────────── function Find-Python { foreach ($cmd in @("python3", "python")) { @@ -59,45 +212,36 @@ function Find-Python { $PythonCmd = Find-Python function Assert-Python { if (-not $PythonCmd) { - Write-Err "Python 3.9-3.13 not found. Please install from https://www.python.org/downloads/" - Read-Host "Press Enter to exit" + 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 (T "按回车退出" "Press Enter to exit") exit 1 } - Write-Cow "Found Python: $PythonCmd" + Write-Cow ((T "找到 Python" "Found Python") + ": $PythonCmd") } # ── clone project ──────────────────────────────────────────────── function Install-Project { if (Test-Path "CowAgent") { - Write-Warn "Directory 'CowAgent' already exists." - $choice = Read-Host "Overwrite(o), backup(b), or quit(q)? [default: b]" - if (-not $choice) { $choice = "b" } - switch ($choice.ToLower()) { - "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 } - } + # Auto-backup the existing directory without prompting. + $backup = "CowAgent_backup_$(Get-Date -Format 'yyyyMMddHHmmss')" + Rename-Item "CowAgent" $backup + Write-Warn ((T "已存在 CowAgent 目录,已自动备份为" "Existing 'CowAgent' directory backed up to") + " '$backup'") } $gitBin = Get-Command git -ErrorAction SilentlyContinue if (-not $gitBin) { - Write-Err "Git not found. Please install from https://git-scm.com/download/win" - Read-Host "Press Enter to exit" + Write-Err (T "未找到 Git,请从 https://git-scm.com/download/win 安装" "Git not found. Please install from https://git-scm.com/download/win") + Read-Host (T "按回车退出" "Press Enter to exit") exit 1 } - Write-Cow "Cloning CowAgent project..." + Write-Cow (T "正在克隆 CowAgent 项目..." "Cloning CowAgent project...") $cloneOk = $false # Test GitHub connectivity before attempting clone try { $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" git clone --depth 10 --progress "https://github.com/zhayujie/CowAgent.git" 2>&1 | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -eq 0) { $cloneOk = $true } @@ -108,7 +252,7 @@ function Install-Project { } catch {} 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" git clone --depth 10 --progress "https://gitee.com/zhayujie/CowAgent.git" 2>&1 | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -eq 0) { $cloneOk = $true } @@ -119,35 +263,83 @@ function Install-Project { } if (-not $cloneOk) { - Write-Err "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" - Read-Host "Press Enter to exit" + Write-Err (T "GitHub 和 Gitee 均克隆失败,请检查网络连接。" "Clone failed from both GitHub and Gitee. Please check your network connection.") + 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 (T "按回车退出" "Press Enter to exit") exit 1 } Set-Location "CowAgent" $script:BaseDir = $PWD.Path $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 ───────────────────────────────────────── 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" - & $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 $ErrorActionPreference = $prevEAP 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" - & $PythonCmd -m pip install -e $BaseDir 2>&1 | Out-Null + & $PythonCmd -m pip install -e $BaseDir @pipMirror 2>&1 | Out-Null $ErrorActionPreference = $prevEAP # Ensure Python Scripts dir is in PATH for this session @@ -160,139 +352,206 @@ function Install-Dependencies { $cowBin = Get-Command cow -ErrorAction SilentlyContinue if ($cowBin) { - Write-Cow "cow CLI registered: $($cowBin.Source)" + Write-Cow ((T "cow CLI 注册成功" "cow CLI registered") + ": $($cowBin.Source)") } else { - Write-Warn "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 "cow CLI 不在 PATH 中,你可以使用" "cow CLI not in PATH. You can use") + ": $PythonCmd -m cli.cli") + Write-Warn (T "如需永久修复,请将 Python Scripts 目录加入系统 PATH。" "To fix permanently, add Python Scripts directory to your system PATH.") } } # ── 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 = @{ - "1" = @{ Provider = "DeepSeek"; Default = "deepseek-v4-flash"; Key = "DEEPSEEK_KEY" } - "2" = @{ Provider = "MiniMax"; Default = "MiniMax-M2.7"; Key = "MINIMAX_KEY" } - "3" = @{ Provider = "Zhipu AI"; Default = "glm-5.1"; Key = "ZHIPU_KEY" } - "4" = @{ Provider = "Kimi (Moonshot)"; Default = "kimi-k2.6"; Key = "MOONSHOT_KEY" } - "5" = @{ Provider = "Doubao (Volcengine Ark)"; Default = "doubao-seed-2-0-code-preview-260215"; Key = "ARK_KEY" } - "6" = @{ Provider = "Qwen (DashScope)"; Default = "qwen3.6-plus"; Key = "DASHSCOPE_KEY" } - "7" = @{ Provider = "Claude"; Default = "claude-opus-4-8"; Key = "CLAUDE_KEY"; Base = "https://api.anthropic.com/v1" } - "8" = @{ Provider = "Gemini"; Default = "gemini-3.1-pro-preview"; Key = "GEMINI_KEY"; Base = "https://generativelanguage.googleapis.com" } - "9" = @{ Provider = "OpenAI GPT"; Default = "gpt-5.4"; Key = "OPENAI_KEY"; Base = "https://api.openai.com/v1" } - "10" = @{ Provider = "LinkAI"; Default = "deepseek-v4-flash"; Key = "LINKAI_KEY" } + 1 = @{ Provider = "DeepSeek"; Default = "deepseek-v4-flash"; Field = "deepseek_api_key" } + 2 = @{ Provider = "Claude"; Default = "claude-opus-4-8"; Field = "claude_api_key"; BaseField = "claude_api_base" } + 3 = @{ Provider = "Gemini"; Default = "gemini-3.1-pro-preview"; Field = "gemini_api_key"; BaseField = "gemini_api_base" } + 4 = @{ Provider = "OpenAI GPT"; Default = "gpt-5.4"; Field = "open_ai_api_key"; BaseField = "open_ai_api_base" } + 5 = @{ Provider = "MiniMax"; Default = "MiniMax-M2.7"; Field = "minimax_api_key" } + 6 = @{ Provider = "Zhipu AI"; Default = "glm-5.1"; Field = "zhipu_ai_api_key" } + 7 = @{ Provider = "Qwen (DashScope)"; Default = "qwen3.6-plus"; Field = "dashscope_api_key" } + 8 = @{ Provider = "Doubao (Volcengine Ark)"; Default = "doubao-seed-2-0-code-preview-260215"; Field = "ark_api_key" } + 9 = @{ Provider = "Kimi (Moonshot)"; Default = "kimi-k2.6"; Field = "moonshot_api_key" } + 10 = @{ Provider = "LinkAI"; Default = "deepseek-v4-flash"; Field = "linkai_api_key"; Linkai = $true } } 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 "" + $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 { - $choice = Read-Host "Enter your choice [default: 1 - DeepSeek]" - if (-not $choice) { $choice = "1" } - } while ($choice -notmatch '^([1-9]|10)$') +# Configure model. Only ask for the API key; model name and base default to +# sensible values and can be changed later in the web console. +function Configure-Model { + # Reset model-related state + $script:ModelName = "" + $script:ModelField = "" + $script:ApiKey = "" + $script:ApiBase = "" + $script:ApiBaseField = "" + $script:UseLinkai = $false - $m = $ModelChoices[$choice] - Write-Cow "Configuring $($m.Provider)..." - - $script:ApiKey = Read-Host "Enter $($m.Provider) API Key" - $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 = "" + if ($script:ModelChoice -eq 11) { + # 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") + return } - $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 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 { Write-Host "" - Write-Info "=========================================" - Write-Info " Select Communication Channel" - Write-Info "=========================================" - Write-Host "1) Weixin" - Write-Host "2) Feishu" - Write-Host "3) DingTalk" - Write-Host "4) WeCom Bot" - Write-Host "5) QQ" - Write-Host "6) WeCom App" - Write-Host "7) Web" - Write-Host "" - - do { - $choice = Read-Host "Enter your choice [default: 1 - Weixin]" - if (-not $choice) { $choice = "1" } - } while ($choice -notmatch '^[1-7]$') + $title = T "选择接入渠道" "Select Communication Channel" + if ($script:UiLang -eq "en") { + $script:ChannelKeys = @("web", "telegram", "discord", "slack", "weixin", "feishu", "dingtalk", "wecom_bot", "qq", "wechatcom_app", "skip") + } else { + $script:ChannelKeys = @("web", "weixin", "feishu", "dingtalk", "wecom_bot", "qq", "wechatcom_app", "telegram", "slack", "discord", "skip") + } + $options = @($script:ChannelKeys | ForEach-Object { Get-ChannelLabel $_ }) + $idx = Select-Menu -Title $title -Options $options -Default 1 + # Map the 1-based menu position back to the stable channel key. + $script:ChannelChoice = $script:ChannelKeys[$idx - 1] +} +# Configure channel, dispatched by stable channel key (not menu position). +function Configure-Channel { $script:ChannelExtra = @{} + $script:AccessInfo = "" - switch ($choice) { - "1" { $script:ChannelType = "weixin" } - "2" { - $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" { + switch ($script:ChannelChoice) { + { $_ -eq "web" -or $_ -eq "skip" } { + # Web (also the default when skipped). Default port, no prompt. $script:ChannelType = "web" - $port = Read-Host "Enter web port [default: 9899]" - if (-not $port) { $port = "9899" } - $script:ChannelExtra["web_port"] = [int]$port + $script:ChannelExtra["web_port"] = 9899 + $script:AccessInfo = (T "Web 控制台地址" "Web console") + " : http://localhost:9899/chat" + } + "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 ───────────────────────────────────────── function New-ConfigFile { - Write-Cow "Generating config.json..." + Write-Cow (T "正在生成 config.json..." "Generating config.json...") $config = [ordered]@{ - channel_type = $ChannelType - model = $ModelName + channel_type = if ($script:ChannelType) { $script:ChannelType } else { "web" } + model = if ($script:ModelName) { $script:ModelName } else { "" } + cow_lang = if ($script:InstallLang) { $script:InstallLang } else { "auto" } open_ai_api_key = "" open_ai_api_base = "https://api.openai.com/v1" claude_api_key = "" @@ -311,7 +570,7 @@ function New-ConfigFile { voice_reply_voice = $false speech_recognition = $true group_speech_recognition = $false - use_linkai = $UseLinkai + use_linkai = [bool]$script:UseLinkai linkai_api_key = "" linkai_app_code = "" agent = $true @@ -320,51 +579,33 @@ function New-ConfigFile { agent_max_steps = 15 } - # Set the correct API key field - $keyMap = @{ - OPENAI_KEY = "open_ai_api_key" - 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" + # Set the API key into the right field (skipped models leave it empty). + if ($script:ModelField -and $config.Contains($script:ModelField)) { + $config[$script:ModelField] = $script:ApiKey } - if ($keyMap.ContainsKey($KeyName)) { - $config[$keyMap[$KeyName]] = $ApiKey - } - - # 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 + # Set API base if the model has a configurable base and the user changed it. + if ($script:ApiBase -and $script:ApiBaseField -and $config.Contains($script:ApiBaseField)) { + $config[$script:ApiBaseField] = $script:ApiBase } # Merge channel-specific fields - foreach ($k in $ChannelExtra.Keys) { - $config[$k] = $ChannelExtra[$k] + foreach ($k in $script:ChannelExtra.Keys) { + $config[$k] = $script:ChannelExtra[$k] } $jsonText = $config | ConvertTo-Json -Depth 5 [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 ───────────────────────────────────────────── function Start-CowAgent { - Write-Cow "Starting CowAgent..." + Write-Cow (T "正在启动 CowAgent..." "Starting CowAgent...") $cowBin = Get-Command cow -ErrorAction SilentlyContinue if ($cowBin) { & cow start } else { - Write-Warn "cow CLI not found, starting directly..." + Write-Warn (T "未找到 cow CLI,直接启动..." "cow CLI not found, starting directly...") & $PythonCmd "$BaseDir\app.py" } } @@ -376,7 +617,7 @@ function Invoke-CowCommand { if ($cowBin) { & cow $Cmd } 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 } } @@ -387,19 +628,19 @@ function Show-Usage { Write-Info " CowAgent Management Script (Windows)" Write-Info "=========================================" Write-Host "" - Write-Host "Usage:" - Write-Host " .\run.ps1 # Install / Configure" - Write-Host " .\run.ps1 # Management command" + Write-Host (T "用法:" "Usage:") + Write-Host (" .\run.ps1 # " + (T "安装 / 配置" "Install / Configure")) + Write-Host (" .\run.ps1 # " + (T "管理命令" "Management command")) Write-Host "" - Write-Host "Commands:" - Write-Host " start Start the service" - Write-Host " stop Stop the service" - Write-Host " restart Restart the service" - Write-Host " status Check service status" - Write-Host " logs View logs" - Write-Host " config Reconfigure project" - Write-Host " update Update and restart" - Write-Host " help Show this message" + Write-Host (T "命令:" "Commands:") + Write-Host (" start " + (T "启动服务" "Start the service")) + Write-Host (" stop " + (T "停止服务" "Stop the service")) + Write-Host (" restart " + (T "重启服务" "Restart the service")) + Write-Host (" status " + (T "查看状态" "Check service status")) + Write-Host (" logs " + (T "查看日志" "View logs")) + Write-Host (" config " + (T "重新配置项目" "Reconfigure project")) + Write-Host (" update " + (T "更新并重启" "Update and restart")) + Write-Host (" help " + (T "显示本帮助" "Show this message")) Write-Host "" } @@ -411,15 +652,19 @@ function Install-Mode { Write-Info "=========================================" Write-Host "" + # Step 0: choose the install/UI language. Everything after this is localized. + Select-Language + Write-Host "" + if ($IsProjectDir) { - Write-Cow "Detected existing project directory." + Write-Cow (T "检测到已有项目目录。" "Detected existing project directory.") if (Test-Path "$BaseDir\config.json") { - Write-Cow "Project already configured." + Write-Cow (T "项目已配置。" "Project already configured.") Write-Host "" Show-Usage 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 "" Assert-Python } else { @@ -429,25 +674,20 @@ function Install-Mode { Install-Dependencies Select-Model + Configure-Model Select-Channel + Configure-Channel New-ConfigFile + # Auto-start after configuration for a true out-of-the-box experience. Write-Host "" - $startNow = Read-Host "Start CowAgent now? [Y/n]" - if ($startNow -ne "n" -and $startNow -ne "N") { - Start-CowAgent - } else { - Write-Cow "Installation complete!" - Write-Host "" - Write-Host "To start manually:" - Write-Host " cd $BaseDir" - Write-Host " cow start" - } + if ($script:AccessInfo) { Write-Cow $script:AccessInfo } + Start-CowAgent } # ── update ──────────────────────────────────────────────────────── function Update-Project { - Write-Cow "Updating CowAgent..." + Write-Cow (T "正在更新 CowAgent..." "Updating CowAgent...") Set-Location $BaseDir # Stop if running @@ -459,20 +699,20 @@ function Update-Project { } if (Test-Path "$BaseDir\.git") { - Write-Cow "Pulling latest code..." + Write-Cow (T "正在拉取最新代码..." "Pulling latest code...") $prevEAP = $ErrorActionPreference; $ErrorActionPreference = "Continue" git pull 2>&1 | Out-Null $pullExit = $LASTEXITCODE $ErrorActionPreference = $prevEAP if ($pullExit -ne 0) { - Write-Warn "GitHub failed, trying Gitee..." + Write-Warn (T "GitHub 拉取失败,尝试 Gitee..." "GitHub failed, trying Gitee...") $ErrorActionPreference = "Continue" git remote set-url origin https://gitee.com/zhayujie/CowAgent.git 2>&1 | Out-Null git pull 2>&1 | Out-Null $ErrorActionPreference = $prevEAP } } else { - Write-Warn "Not a git repository, skipping code update." + Write-Warn (T "非 git 仓库,跳过代码更新。" "Not a git repository, skipping code update.") } Assert-Python @@ -480,11 +720,13 @@ function Update-Project { # Start via python -m cli.cli instead of cow.exe, because the exe may # 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 } # ── main ────────────────────────────────────────────────────────── +Initialize-UiLang + switch ($Command.ToLower()) { "" { Install-Mode } "start" { Invoke-CowCommand "start" } @@ -496,15 +738,17 @@ switch ($Command.ToLower()) { Assert-Python Install-Dependencies Select-Model + Configure-Model Select-Channel + Configure-Channel 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" } } "update" { Update-Project } "help" { Show-Usage } default { - Write-Err "Unknown command: $Command" + Write-Err ((T "未知命令" "Unknown command") + ": $Command") Show-Usage exit 1 }