chore: stop tracking opinion doc and one-click deploy scripts
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
20
.gitignore
vendored
20
.gitignore
vendored
@@ -19,3 +19,23 @@ ai-service/.env
|
|||||||
|
|
||||||
*.exe~
|
*.exe~
|
||||||
|
|
||||||
|
# 本地意见稿 / 不入库
|
||||||
|
database_fastapi修改意见.md
|
||||||
|
|
||||||
|
# 一键部署脚本(本机/运维侧保留,不推远程)
|
||||||
|
start.sh
|
||||||
|
stop.sh
|
||||||
|
restart.sh
|
||||||
|
pull-and-restart.sh
|
||||||
|
reload-config.sh
|
||||||
|
deploy-menu.sh
|
||||||
|
start.bat
|
||||||
|
start.ps1
|
||||||
|
stop.bat
|
||||||
|
stop.ps1
|
||||||
|
restart.bat
|
||||||
|
restart.ps1
|
||||||
|
start-host.ps1
|
||||||
|
scripts/lib-aijz-deploy.sh
|
||||||
|
scripts/linux/
|
||||||
|
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
# database_fastapi 修改意见
|
|
||||||
|
|
||||||
> 针对文件:`E:\project\yh_one\new_project\yuhengyihao_client\yxd\app_fastapi\database_fastapi.py`
|
|
||||||
> 背景:需要与线上主库强一致;断网可记日志,恢复后合并回放至线上;主写在线上。
|
|
||||||
> 对照能力:宇信达智建平台 `dbsync` 出站队列 / 冲突 / 对账(见 `ai建站/docs/数据同步-中间件.md`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 落地状态(已实现)
|
|
||||||
|
|
||||||
| 项 | 状态 | 位置 |
|
|
||||||
|----|------|------|
|
|
||||||
| 统一写网关 `apply_write` | ✅ | `db_write_gateway.py` |
|
|
||||||
| 写接口收口 | ✅ | insert / update / update/id / delete / sql(DML) |
|
|
||||||
| 库绑定防串库 | ✅ | `sync_binding.py` → `cache/db_bindings/bindings.json` |
|
|
||||||
| 配置 | ✅ | `sync_config.py`(环境变量) |
|
|
||||||
| 断网 pending | ✅ | 复用 `QueueDatabaseManager`,库名默认 `sync_pending` |
|
|
||||||
| 手动回放 | ✅ | `POST /database/sync/replay` |
|
|
||||||
| 绑定/状态 API | ✅ | `/database/sync/*` |
|
|
||||||
| 后台自动回放 | ✅ 可选 | `sync_replay_worker.py`,需 `YXD_SYNC_REPLAY_WORKER=1` |
|
|
||||||
| DDL / 批量 import 收口 | ⏳ 未做 | 仍可直写本地;后续按需收口 |
|
|
||||||
| 冲突队列 API | ⏳ 未做 | 回放失败即停保序,暂无独立 conflicts 列表 |
|
|
||||||
|
|
||||||
**默认安全**:`YXD_SYNC_MODE` 未配且无 `YXD_ONLINE_API_BASE` 时为 **`local_only`**(只写本地,仍会 ensure 绑定信息)。
|
|
||||||
配了 `YXD_ONLINE_API_BASE` 且未显式写 `YXD_SYNC_MODE` 时自动升为 **`online_primary`**。
|
|
||||||
|
|
||||||
### 环境变量
|
|
||||||
|
|
||||||
| 变量 | 说明 | 默认 |
|
|
||||||
|------|------|------|
|
|
||||||
| `YXD_SYNC_MODE` | `local_only` \| `online_primary` | `local_only`(有 ONLINE_BASE 且未设 mode 则升 primary) |
|
|
||||||
| `YXD_ONLINE_API_BASE` | 线上 API 根,如 `https://api.example.com` | 空 |
|
|
||||||
| `YXD_ONLINE_TIMEOUT_SEC` | 线上写超时 | `8` |
|
|
||||||
| `YXD_OFFLINE_POLICY` | `pending_log` \| `reject` | `pending_log` |
|
|
||||||
| `YXD_SYNC_ENFORCE_BINDING` | 是否强制已绑定 | `true` |
|
|
||||||
| `YXD_SYNC_AUTO_ENSURE_BINDING` | 写时自动 ensure | `true` |
|
|
||||||
| `YXD_SYNC_PENDING_QUEUE` | pending 队列库名 | `sync_pending` |
|
|
||||||
| `YXD_ONLINE_TOKEN_HEADER` | 透传 token 头 | `Authorization` |
|
|
||||||
| `YXD_SYNC_REPLAY_WORKER` | 启后台回放 | 关 |
|
|
||||||
| `YXD_SYNC_REPLAY_INTERVAL_SEC` | 回放间隔秒 | `30` |
|
|
||||||
|
|
||||||
### 新增/改动文件
|
|
||||||
|
|
||||||
```text
|
|
||||||
yxd/app_fastapi/
|
|
||||||
db_write_gateway.py # apply_write / replay_pending
|
|
||||||
sync_config.py
|
|
||||||
sync_binding.py
|
|
||||||
sync_context.py # Request → WriteContext
|
|
||||||
sync_replay_worker.py
|
|
||||||
database_fastapi.py # 写路由改走网关 + /sync/* API
|
|
||||||
```
|
|
||||||
|
|
||||||
### 同步 API
|
|
||||||
|
|
||||||
- `GET /database/sync/status`
|
|
||||||
- `POST /database/sync/binding/ensure`
|
|
||||||
- `GET /database/sync/binding`
|
|
||||||
- `GET /database/sync/pending`
|
|
||||||
- `POST /database/sync/replay` body: `{ "limit": 50 }`
|
|
||||||
|
|
||||||
请求头需带:`Database-ID`、`User-ID`(或应用已登录)、可选 `Tenant-ID` / `Authorization`。
|
|
||||||
|
|
||||||
### 写路径行为(`online_primary`)
|
|
||||||
|
|
||||||
```text
|
|
||||||
联网:先写线上(Database-ID=online_db_id)→ 成功再写本地镜像 → synced=true
|
|
||||||
离线 pending_log:不写正式本地业务结果 → 入 sync_pending 队列 → pending=true
|
|
||||||
离线 reject:直接失败
|
|
||||||
回放:按 id 升序提交线上 → 成功后写本地并删队列记录;失败即停保序
|
|
||||||
```
|
|
||||||
|
|
||||||
绑定键:`(tenant_id, user_id, local_database_id) → online_db_id`(全局唯一)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、原状结论(改造前)
|
|
||||||
|
|
||||||
| 项 | 原状 |
|
|
||||||
|----|------|
|
|
||||||
| 写入口 | `/database/permanent/table/data/insert\|update\|update/id\|delete`、`/permanent/sql` 等 |
|
|
||||||
| 落库方式 | 直接 `PermanentDatabaseManager`,**只写本地 SQLite** |
|
|
||||||
| 队列能力 | `queue_database_fastapi.py` 与 permanent **未打通** |
|
|
||||||
| 线上主写 / 断网日志 | **未实现** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、目标模型
|
|
||||||
|
|
||||||
```text
|
|
||||||
联网:API 写 → 先写线上主库 → 再写本地镜像
|
|
||||||
断网:API 写 → pending 日志(queue_database)→ 不 formal commit
|
|
||||||
恢复:有序回放到线上主 → 成功再对齐本地
|
|
||||||
```
|
|
||||||
|
|
||||||
硬约束:主写在线上;断网日志只能回放到线上主;断网窗口非强一致。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三~十、设计说明(保留)
|
|
||||||
|
|
||||||
原 P0–P4、响应约定、勿做事项、分阶段 M1–M4 已按上文落地;M5(SQL/导入全收口 + 冲突监控)待续。
|
|
||||||
|
|
||||||
与智建平台 dbsync:客户端强一致仍以「API 同步写线上」为准,不要只靠触发器异步 outbox 充当主路径。
|
|
||||||
|
|
||||||
本意见文档:`E:\project\ai建站\database_fastapi修改意见.md`。
|
|
||||||
实现目录:`yuhengyihao_client\yxd\app_fastapi\`。
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 跳转到同级 ops 多项目菜单
|
|
||||||
set -euo pipefail
|
|
||||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
MENU="${OPS_MENU:-$ROOT/../ops/deploy.sh}"
|
|
||||||
if [ ! -f "$MENU" ]; then
|
|
||||||
echo "找不到菜单: $MENU" >&2
|
|
||||||
echo "请把 ops/ 放在与本项目同级,或设置 OPS_MENU=/path/to/ops/deploy.sh" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exec bash "$MENU" "$@"
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# AI 建站 — 拉取代码并重启
|
|
||||||
# 用法:./pull-and-restart.sh
|
|
||||||
# 默认仓库:https://gitea.yuxindazhineng.com/whm/ai_site.git
|
|
||||||
# 环境变量:
|
|
||||||
# AIJZ_GIT_URL 覆盖仓库地址
|
|
||||||
# GIT_BRANCH 分支(默认 main)
|
|
||||||
# FORCE_GIT_RESET 默认 1,与远程硬对齐
|
|
||||||
set -euo pipefail
|
|
||||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
cd "$ROOT"
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
. "$ROOT/scripts/lib-aijz-deploy.sh"
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
echo " AI 建站 拉取并重启"
|
|
||||||
echo " 路径: $ROOT"
|
|
||||||
echo " 仓库: ${AIJZ_GIT_URL:-https://gitea.yuxindazhineng.com/whm/ai_site.git}"
|
|
||||||
echo " 分支: ${GIT_BRANCH:-main}"
|
|
||||||
echo "========================================"
|
|
||||||
|
|
||||||
ensure_docker
|
|
||||||
git_pull_hard
|
|
||||||
stack_down
|
|
||||||
sleep 1
|
|
||||||
stack_up 1
|
|
||||||
healthcheck
|
|
||||||
print_endpoints
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 轻量重载配置(不 rebuild 镜像、不重建 web/dist、不 down 整栈)
|
|
||||||
# 用法:
|
|
||||||
# ./reload-config.sh # 全部
|
|
||||||
# ./reload-config.sh web # 容器 nginx(web/nginx.conf)
|
|
||||||
# ./reload-config.sh platform # restart platform(platform/etc/platform.docker.yaml)
|
|
||||||
# ./reload-config.sh gateway # restart gateway
|
|
||||||
# ./reload-config.sh ai # recreate ai(.env)
|
|
||||||
# ./reload-config.sh host-nginx # 宿主机 Nginx + 证书 + verify-root
|
|
||||||
# 行尾:LF
|
|
||||||
set -euo pipefail
|
|
||||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
cd "$ROOT"
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
. "$ROOT/scripts/lib-aijz-deploy.sh"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<EOF
|
|
||||||
用法: $0 [web|platform|gateway|ai|host-nginx|all]
|
|
||||||
|
|
||||||
web 容器内 nginx -s reload(改 web/nginx.conf 后)
|
|
||||||
platform restart platform(改 platform/etc/platform.docker.yaml 或 AIJZ_PUBLIC_BASE_URL)
|
|
||||||
gateway restart gateway(改 gateway/etc/gateway.docker.yaml)
|
|
||||||
ai recreate ai(改 .env 密钥或 etc/llm.yaml 供应商)
|
|
||||||
host-nginx 同步证书 + 生成站点 + reload 宿主机 nginx(需 AIJZ_ENABLE_HOST_NGINX=1)
|
|
||||||
all 以上全部(默认)
|
|
||||||
|
|
||||||
详见 nginx/README.md
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_docker
|
|
||||||
ensure_env_file
|
|
||||||
export_defaults
|
|
||||||
|
|
||||||
TARGET="${1:-all}"
|
|
||||||
case "$TARGET" in
|
|
||||||
-h|--help|help) usage; exit 0 ;;
|
|
||||||
web)
|
|
||||||
reload_web_nginx
|
|
||||||
;;
|
|
||||||
platform)
|
|
||||||
apply_public_base_url
|
|
||||||
restart_service platform
|
|
||||||
;;
|
|
||||||
gateway)
|
|
||||||
restart_service gateway
|
|
||||||
;;
|
|
||||||
ai)
|
|
||||||
compose_cmd up -d --force-recreate ai
|
|
||||||
echo "已 recreate ai(.env + etc/llm.yaml)"
|
|
||||||
;;
|
|
||||||
host-nginx|host)
|
|
||||||
reload_host_nginx
|
|
||||||
;;
|
|
||||||
all)
|
|
||||||
reload_all_config
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "未知目标: $TARGET" >&2
|
|
||||||
usage >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
cd /d "%~dp0"
|
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0restart.ps1" %*
|
|
||||||
23
restart.ps1
23
restart.ps1
@@ -1,23 +0,0 @@
|
|||||||
# restart: stop then start
|
|
||||||
param(
|
|
||||||
[switch]$Rebuild,
|
|
||||||
[switch]$UseHost,
|
|
||||||
[switch]$NoBrowser,
|
|
||||||
[switch]$ShowConsole
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Continue"
|
|
||||||
$Root = $PSScriptRoot
|
|
||||||
Set-Location -LiteralPath $Root
|
|
||||||
|
|
||||||
Write-Host "==> Restart: stopping..." -ForegroundColor Yellow
|
|
||||||
& (Join-Path $Root "scripts\Stop-Stack.ps1")
|
|
||||||
if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne $null) {
|
|
||||||
Write-Host " (stop reported leftovers; continuing restart anyway)" -ForegroundColor DarkGray
|
|
||||||
}
|
|
||||||
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
|
|
||||||
Write-Host "==> Restart: starting..." -ForegroundColor Cyan
|
|
||||||
& (Join-Path $Root "start.ps1") -Rebuild:$Rebuild -UseHost:$UseHost -NoBrowser:$NoBrowser -ShowConsole:$ShowConsole
|
|
||||||
exit $LASTEXITCODE
|
|
||||||
31
restart.sh
31
restart.sh
@@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# AI 建站 — 重启(不拉代码:停 → 构建 → 起)
|
|
||||||
# 用法:./restart.sh [--rebuild]
|
|
||||||
set -euo pipefail
|
|
||||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
cd "$ROOT"
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
. "$ROOT/scripts/lib-aijz-deploy.sh"
|
|
||||||
|
|
||||||
REBUILD=1
|
|
||||||
for a in "$@"; do
|
|
||||||
case "$a" in
|
|
||||||
--no-rebuild) REBUILD=0 ;;
|
|
||||||
-h|--help)
|
|
||||||
echo "用法: $0 [--no-rebuild]"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
echo " AI 建站 重启"
|
|
||||||
echo " 路径: $ROOT"
|
|
||||||
echo "========================================"
|
|
||||||
|
|
||||||
ensure_docker
|
|
||||||
stack_down
|
|
||||||
sleep 1
|
|
||||||
stack_up "$REBUILD"
|
|
||||||
healthcheck
|
|
||||||
print_endpoints
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
# shellcheck shell=bash
|
|
||||||
# AI 建站 Linux 部署公共函数。由 start/restart/stop/pull-and-restart 在 ROOT 下 source。
|
|
||||||
|
|
||||||
run_sudo() {
|
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
|
||||||
"$@"
|
|
||||||
else
|
|
||||||
sudo "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_runtime_dirs() {
|
|
||||||
mkdir -p "$ROOT/.runtime/pgdata" "$ROOT/.runtime/uploads" "$ROOT/.runtime/logs" \
|
|
||||||
"$ROOT/.runtime/logs/generate" "$ROOT/web/dist"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_env_file() {
|
|
||||||
if [ ! -f "$ROOT/.env" ]; then
|
|
||||||
if [ -f "$ROOT/.env.example" ]; then
|
|
||||||
cp "$ROOT/.env.example" "$ROOT/.env"
|
|
||||||
echo "已从 .env.example 创建 .env,请按需填写 API Key。"
|
|
||||||
else
|
|
||||||
cat >"$ROOT/.env" <<'EOF'
|
|
||||||
LLM_PROVIDER=deepseek
|
|
||||||
DEEPSEEK_API_KEY=
|
|
||||||
MINIMAX_API_KEY=
|
|
||||||
EOF
|
|
||||||
echo "已创建默认 .env"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
sed -i 's/\r$//' "$ROOT/.env" 2>/dev/null || true
|
|
||||||
# 供 compose 变量替换与本脚本健康检查使用
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
[ -f "$ROOT/.env" ] && . "$ROOT/.env"
|
|
||||||
set +a
|
|
||||||
export AIJZ_WEB_PUBLISH="${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}"
|
|
||||||
export AIJZ_GATEWAY_PUBLISH="${AIJZ_GATEWAY_PUBLISH:-127.0.0.1:8180}"
|
|
||||||
export AIJZ_PLATFORM_PUBLISH="${AIJZ_PLATFORM_PUBLISH:-127.0.0.1:8888}"
|
|
||||||
export AIJZ_AI_PUBLISH="${AIJZ_AI_PUBLISH:-127.0.0.1:8001}"
|
|
||||||
export AIJZ_PG_PUBLISH="${AIJZ_PG_PUBLISH:-127.0.0.1:5432}"
|
|
||||||
export AIJZ_ENABLE_HOST_NGINX="${AIJZ_ENABLE_HOST_NGINX:-0}"
|
|
||||||
export AIJZ_DOMAIN="${AIJZ_DOMAIN:-}"
|
|
||||||
export AIJZ_PUBLIC_BASE_URL="${AIJZ_PUBLIC_BASE_URL:-}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 若 .env 设了 AIJZ_PUBLIC_BASE_URL,同步到 platform.docker.yaml(挂载文件,restart platform 即生效)
|
|
||||||
apply_public_base_url() {
|
|
||||||
local base="${AIJZ_PUBLIC_BASE_URL:-}"
|
|
||||||
local pf="$ROOT/platform/etc/platform.docker.yaml"
|
|
||||||
[ -n "$base" ] || return 0
|
|
||||||
[ -f "$pf" ] || return 0
|
|
||||||
base="${base%/}"
|
|
||||||
local storage_base="${base}/api/v1/storage"
|
|
||||||
if grep -q '^PublicBaseURL:' "$pf"; then
|
|
||||||
sed -i "s|^PublicBaseURL:.*|PublicBaseURL: \"${base}\"|" "$pf"
|
|
||||||
fi
|
|
||||||
if grep -q '^ PublicBase:' "$pf"; then
|
|
||||||
sed -i "s|^ PublicBase:.*| PublicBase: \"${storage_base}\"|" "$pf"
|
|
||||||
fi
|
|
||||||
echo "已同步 PublicBaseURL -> ${base}(platform/etc/platform.docker.yaml)"
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_ssl_certs() {
|
|
||||||
local domain="${AIJZ_DOMAIN:-}"
|
|
||||||
[ -n "$domain" ] || return 0
|
|
||||||
local ssl_dir="/etc/ssl/aijianzhan/$domain"
|
|
||||||
run_sudo mkdir -p "$ssl_dir"
|
|
||||||
if [ -f "$ROOT/nginx/$domain.pem" ] && [ -f "$ROOT/nginx/$domain.key" ]; then
|
|
||||||
run_sudo cp -f "$ROOT/nginx/$domain.pem" "$ssl_dir/fullchain.pem"
|
|
||||||
run_sudo cp -f "$ROOT/nginx/$domain.key" "$ssl_dir/privkey.pem"
|
|
||||||
elif [ -f "$ROOT/nginx/fullchain.pem" ] && [ -f "$ROOT/nginx/privkey.pem" ]; then
|
|
||||||
run_sudo cp -f "$ROOT/nginx/fullchain.pem" "$ROOT/nginx/privkey.pem" "$ssl_dir/"
|
|
||||||
elif [ -f "$ROOT/nginx/$domain/fullchain.pem" ] && [ -f "$ROOT/nginx/$domain/privkey.pem" ]; then
|
|
||||||
run_sudo cp -f "$ROOT/nginx/$domain/fullchain.pem" "$ROOT/nginx/$domain/privkey.pem" "$ssl_dir/"
|
|
||||||
else
|
|
||||||
echo "提示: 未找到 nginx/ 下证书,宿主机 HTTPS 需手动放入 $ssl_dir" >&2
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
run_sudo chmod 644 "$ssl_dir/fullchain.pem" 2>/dev/null || true
|
|
||||||
run_sudo chmod 600 "$ssl_dir/privkey.pem" 2>/dev/null || true
|
|
||||||
echo "证书已同步 -> $ssl_dir"
|
|
||||||
}
|
|
||||||
|
|
||||||
host_nginx_online() {
|
|
||||||
command -v systemctl >/dev/null 2>&1 || return 1
|
|
||||||
systemctl is-active nginx >/dev/null 2>&1 && return 0
|
|
||||||
systemctl is-active nginx.service >/dev/null 2>&1 && return 0
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_host_nginx_started() {
|
|
||||||
command -v nginx >/dev/null 2>&1 || {
|
|
||||||
echo "错误: 未安装 nginx,无法启用 AIJZ_ENABLE_HOST_NGINX" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if host_nginx_online; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo "宿主机 Nginx 未在线,尝试启动..."
|
|
||||||
run_sudo systemctl start nginx 2>/dev/null || run_sudo systemctl start nginx.service
|
|
||||||
run_sudo systemctl enable nginx 2>/dev/null || true
|
|
||||||
host_nginx_online || {
|
|
||||||
echo "错误: 无法启动宿主机 nginx" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
install_host_nginx_site_conf() {
|
|
||||||
local domain="${AIJZ_DOMAIN:-}"
|
|
||||||
[ -n "$domain" ] || return 0
|
|
||||||
[ "${AIJZ_ENABLE_HOST_NGINX:-0}" = "1" ] || return 0
|
|
||||||
local tpl="$ROOT/nginx/aijz.host.conf"
|
|
||||||
local out="/etc/nginx/conf.d/aijz_${domain}.conf"
|
|
||||||
local web_port
|
|
||||||
web_port="$(publish_host_port "${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}")"
|
|
||||||
[ -f "$tpl" ] || {
|
|
||||||
echo "未找到 $tpl,跳过宿主机 Nginx 配置" >&2
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
mkdir -p "$ROOT/verify-root"
|
|
||||||
sync_ssl_certs
|
|
||||||
sed -e "s|__DOMAIN__|${domain}|g" \
|
|
||||||
-e "s|__WEB_PORT__|${web_port}|g" \
|
|
||||||
-e "s|__VERIFY_ROOT__|${ROOT}/verify-root|g" \
|
|
||||||
"$tpl" | run_sudo tee "$out" >/dev/null
|
|
||||||
if ! run_sudo nginx -t 2>/dev/null; then
|
|
||||||
echo "错误: nginx -t 失败,请检查 $out" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if host_nginx_online; then
|
|
||||||
run_sudo systemctl reload nginx 2>/dev/null && echo "宿主机 Nginx 已重载($out)"
|
|
||||||
else
|
|
||||||
ensure_host_nginx_started
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
reload_web_nginx() {
|
|
||||||
local cid
|
|
||||||
cid="$(compose_cmd ps -q web 2>/dev/null | head -1)"
|
|
||||||
if [ -z "$cid" ]; then
|
|
||||||
echo "web 容器未运行,跳过 reload" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
run_sudo docker exec "$cid" nginx -t >/dev/null 2>&1 || {
|
|
||||||
echo "错误: 容器内 nginx -t 失败" >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
run_sudo docker exec "$cid" nginx -s reload
|
|
||||||
echo "web 容器 nginx 已 reload(web/nginx.conf)"
|
|
||||||
}
|
|
||||||
|
|
||||||
restart_service() {
|
|
||||||
local svc="$1"
|
|
||||||
compose_cmd restart "$svc"
|
|
||||||
echo "已 restart $svc"
|
|
||||||
}
|
|
||||||
|
|
||||||
reload_host_nginx() {
|
|
||||||
[ "${AIJZ_ENABLE_HOST_NGINX:-0}" = "1" ] || {
|
|
||||||
echo "AIJZ_ENABLE_HOST_NGINX 未启用,跳过宿主机 Nginx" >&2
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
install_host_nginx_site_conf
|
|
||||||
}
|
|
||||||
|
|
||||||
reload_all_config() {
|
|
||||||
ensure_env_file
|
|
||||||
apply_public_base_url
|
|
||||||
reload_host_nginx || true
|
|
||||||
reload_web_nginx || true
|
|
||||||
restart_service platform
|
|
||||||
restart_service gateway
|
|
||||||
compose_cmd up -d --force-recreate ai >/dev/null 2>&1 || compose_cmd restart ai
|
|
||||||
echo "配置重载完成(未 rebuild 镜像、未重建 web/dist)"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_docker() {
|
|
||||||
if command -v docker >/dev/null 2>&1 && run_sudo docker info >/dev/null 2>&1; then
|
|
||||||
echo "Docker 已就绪."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if command -v docker >/dev/null 2>&1; then
|
|
||||||
echo "Docker 守护进程未连接,尝试启动..."
|
|
||||||
run_sudo systemctl start docker 2>/dev/null || run_sudo systemctl start podman 2>/dev/null || true
|
|
||||||
if run_sudo docker info >/dev/null 2>&1; then
|
|
||||||
echo "Docker 已就绪."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo "错误: Docker 不可用。请先安装并启动 docker 后再执行。" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_compose_cmd() {
|
|
||||||
if run_sudo docker compose version >/dev/null 2>&1; then
|
|
||||||
echo "docker compose"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if command -v docker-compose >/dev/null 2>&1; then
|
|
||||||
echo "docker-compose"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ -x /usr/local/bin/docker-compose ]; then
|
|
||||||
echo "/usr/local/bin/docker-compose"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
COMPOSE_CMD=""
|
|
||||||
compose_cmd() {
|
|
||||||
if [ -z "$COMPOSE_CMD" ]; then
|
|
||||||
COMPOSE_CMD="$(resolve_compose_cmd)"
|
|
||||||
fi
|
|
||||||
if [ -z "$COMPOSE_CMD" ]; then
|
|
||||||
echo "错误: 找不到 docker compose / docker-compose" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
run_sudo env \
|
|
||||||
DOCKER_BASE_REGISTRY="${DOCKER_BASE_REGISTRY}" \
|
|
||||||
GOPROXY="${GOPROXY}" \
|
|
||||||
AIJZ_WEB_PUBLISH="${AIJZ_WEB_PUBLISH}" \
|
|
||||||
AIJZ_GATEWAY_PUBLISH="${AIJZ_GATEWAY_PUBLISH}" \
|
|
||||||
AIJZ_PLATFORM_PUBLISH="${AIJZ_PLATFORM_PUBLISH}" \
|
|
||||||
AIJZ_AI_PUBLISH="${AIJZ_AI_PUBLISH}" \
|
|
||||||
AIJZ_PG_PUBLISH="${AIJZ_PG_PUBLISH}" \
|
|
||||||
$COMPOSE_CMD "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
export_defaults() {
|
|
||||||
export DOCKER_BASE_REGISTRY="${DOCKER_BASE_REGISTRY:-docker.m.daocloud.io/library}"
|
|
||||||
export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
|
|
||||||
export REGISTRY_MIRROR="${REGISTRY_MIRROR:-docker.m.daocloud.io/library/}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 从 "127.0.0.1:5173" 或 "5173" 取出宿主机端口号
|
|
||||||
publish_host_port() {
|
|
||||||
local pub="$1"
|
|
||||||
echo "$pub" | awk -F: '{print $NF}'
|
|
||||||
}
|
|
||||||
|
|
||||||
port_in_use() {
|
|
||||||
local port="$1"
|
|
||||||
if command -v ss >/dev/null 2>&1; then
|
|
||||||
run_sudo ss -tlnH "sport = :$port" 2>/dev/null | grep -q . && return 0
|
|
||||||
ss -tlnH "sport = :$port" 2>/dev/null | grep -q . && return 0
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if command -v lsof >/dev/null 2>&1; then
|
|
||||||
lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 检查 AI 建站将占用的宿主机端口;若已被本项目容器占用则放行
|
|
||||||
check_aijz_ports() {
|
|
||||||
local pairs=(
|
|
||||||
"web:$AIJZ_WEB_PUBLISH"
|
|
||||||
"gateway:$AIJZ_GATEWAY_PUBLISH"
|
|
||||||
"platform:$AIJZ_PLATFORM_PUBLISH"
|
|
||||||
"ai:$AIJZ_AI_PUBLISH"
|
|
||||||
"postgres:$AIJZ_PG_PUBLISH"
|
|
||||||
)
|
|
||||||
echo "端口规划核对(相对 yh_web):AI建站 5173/8180/8888/8001/5432;宇恒 8088/9080/9081 + 宿主机 80/443。"
|
|
||||||
local name pub port conflict=0
|
|
||||||
local self_running=0
|
|
||||||
if compose_cmd ps -q 2>/dev/null | grep -q .; then
|
|
||||||
self_running=1
|
|
||||||
fi
|
|
||||||
for item in "${pairs[@]}"; do
|
|
||||||
name="${item%%:*}"
|
|
||||||
pub="${item#*:}"
|
|
||||||
port="$(publish_host_port "$pub")"
|
|
||||||
[ -z "$port" ] && continue
|
|
||||||
if port_in_use "$port"; then
|
|
||||||
if [ "$self_running" -eq 1 ]; then
|
|
||||||
echo " 提示: 端口 $port($name)已占用,疑为本栈,继续。"
|
|
||||||
else
|
|
||||||
echo " 错误: 宿主机端口 $port 已被占用(服务 $name ← $pub)。" >&2
|
|
||||||
echo " 请改 .env 中对应 AIJZ_*_PUBLISH,或停掉占用进程。" >&2
|
|
||||||
conflict=1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " OK $name → $pub"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ "$conflict" -eq 0 ] || exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 构建 web/dist:优先本机 npm;否则用 Docker node 镜像
|
|
||||||
build_web_dist() {
|
|
||||||
echo "构建 web dist ..."
|
|
||||||
if command -v npm >/dev/null 2>&1; then
|
|
||||||
(
|
|
||||||
cd "$ROOT/web"
|
|
||||||
if [ -f package-lock.json ]; then
|
|
||||||
npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps
|
|
||||||
else
|
|
||||||
npm install --legacy-peer-deps
|
|
||||||
fi
|
|
||||||
npm run build
|
|
||||||
)
|
|
||||||
else
|
|
||||||
echo "本机无 npm,使用 Docker node 构建..."
|
|
||||||
run_sudo docker run --rm \
|
|
||||||
-v "$ROOT/web:/app" \
|
|
||||||
-v "$ROOT/web/dist:/app/dist" \
|
|
||||||
-w /app \
|
|
||||||
"${REGISTRY_MIRROR}node:20-alpine" \
|
|
||||||
sh -c "(npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps) && npm run build"
|
|
||||||
fi
|
|
||||||
if [ ! -f "$ROOT/web/dist/index.html" ]; then
|
|
||||||
echo "错误: web/dist/index.html 不存在,构建失败。" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "web/dist 就绪."
|
|
||||||
}
|
|
||||||
|
|
||||||
stack_up() {
|
|
||||||
local rebuild="${1:-0}"
|
|
||||||
export_defaults
|
|
||||||
ensure_runtime_dirs
|
|
||||||
ensure_env_file
|
|
||||||
apply_public_base_url
|
|
||||||
check_aijz_ports
|
|
||||||
build_web_dist
|
|
||||||
if [ "$rebuild" = "1" ]; then
|
|
||||||
compose_cmd up -d --build --force-recreate
|
|
||||||
else
|
|
||||||
if ! compose_cmd up -d; then
|
|
||||||
echo "compose up 失败,尝试 --build ..."
|
|
||||||
compose_cmd up -d --build
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
install_host_nginx_site_conf || true
|
|
||||||
}
|
|
||||||
|
|
||||||
stack_down() {
|
|
||||||
compose_cmd down --remove-orphans 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
healthcheck() {
|
|
||||||
local ok=0 i
|
|
||||||
local gw_port web_port
|
|
||||||
gw_port="$(publish_host_port "${AIJZ_GATEWAY_PUBLISH:-127.0.0.1:8180}")"
|
|
||||||
web_port="$(publish_host_port "${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}")"
|
|
||||||
echo "等待 gateway(:$gw_port) / web(:$web_port) ..."
|
|
||||||
for i in $(seq 1 60); do
|
|
||||||
if curl -fsS --max-time 2 "http://127.0.0.1:${gw_port}/gateway/health" >/dev/null 2>&1 \
|
|
||||||
&& curl -fsS --max-time 2 "http://127.0.0.1:${web_port}/" >/dev/null 2>&1; then
|
|
||||||
ok=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
if [ "$ok" -eq 1 ]; then
|
|
||||||
echo "健康检查通过."
|
|
||||||
else
|
|
||||||
echo "警告: 健康检查超时,请查看: docker compose -f $ROOT/docker-compose.yml logs" >&2
|
|
||||||
fi
|
|
||||||
compose_cmd exec -T postgres psql -U platform -d platform -c 'ALTER USER platform CREATEDB;' >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
print_endpoints() {
|
|
||||||
local pub_line=""
|
|
||||||
if [ -n "${AIJZ_DOMAIN:-}" ] && [ "${AIJZ_ENABLE_HOST_NGINX:-0}" = "1" ]; then
|
|
||||||
pub_line=" Public https://${AIJZ_DOMAIN}"
|
|
||||||
fi
|
|
||||||
cat <<EOF
|
|
||||||
|
|
||||||
========================================
|
|
||||||
Web http://127.0.0.1:$(publish_host_port "${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}")
|
|
||||||
Gateway http://127.0.0.1:$(publish_host_port "${AIJZ_GATEWAY_PUBLISH:-127.0.0.1:8180}")
|
|
||||||
${pub_line}
|
|
||||||
Account demo / demo123
|
|
||||||
Data $ROOT/.runtime
|
|
||||||
Stop ./stop.sh
|
|
||||||
重载配置 ./reload-config.sh(不整栈 rebuild)
|
|
||||||
绑定 默认 127.0.0.1(与 yh_web 同机不抢 80/443/8088)
|
|
||||||
========================================
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# 默认代码源(可用 AIJZ_GIT_URL / GIT_BRANCH 覆盖)
|
|
||||||
AIJZ_GIT_URL_DEFAULT="https://gitea.yuxindazhineng.com/whm/ai_site.git"
|
|
||||||
|
|
||||||
ensure_git_origin() {
|
|
||||||
local url="${1:-${AIJZ_GIT_URL:-$AIJZ_GIT_URL_DEFAULT}}"
|
|
||||||
if ! command -v git >/dev/null 2>&1; then
|
|
||||||
echo "错误: 未安装 git,无法拉取代码。请先安装 git。" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
||||||
echo "当前目录不是 git 仓库,初始化并关联 origin -> $url"
|
|
||||||
git init
|
|
||||||
git remote add origin "$url"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if git remote get-url origin >/dev/null 2>&1; then
|
|
||||||
local cur
|
|
||||||
cur="$(git remote get-url origin)"
|
|
||||||
if [ "$cur" != "$url" ]; then
|
|
||||||
echo "更新 origin: $cur -> $url"
|
|
||||||
git remote set-url origin "$url"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "添加 origin -> $url"
|
|
||||||
git remote add origin "$url"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 从 Gitea ai_site 拉取并与远程对齐(默认硬重置,保留未跟踪文件如 .env)
|
|
||||||
# 环境变量:
|
|
||||||
# AIJZ_GIT_URL 仓库地址(默认 https://gitea.yuxindazhineng.com/whm/ai_site.git)
|
|
||||||
# GIT_BRANCH 分支(默认 main)
|
|
||||||
# FORCE_GIT_RESET 1=reset --hard 对齐远程(默认 1);0=仅快进合并
|
|
||||||
git_pull_hard() {
|
|
||||||
local url="${AIJZ_GIT_URL:-$AIJZ_GIT_URL_DEFAULT}"
|
|
||||||
local branch="${GIT_BRANCH:-main}"
|
|
||||||
echo "拉取代码($url branch=$branch)..."
|
|
||||||
ensure_git_origin "$url" || return 1
|
|
||||||
if ! git fetch origin --progress; then
|
|
||||||
echo "错误: git fetch 失败。私有仓库请配置凭据,或设置 AIJZ_GIT_URL 为带 token 的地址。" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! git rev-parse --verify "origin/$branch" >/dev/null 2>&1; then
|
|
||||||
echo "错误: 远程不存在分支 origin/$branch" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [ "${FORCE_GIT_RESET:-1}" = "1" ]; then
|
|
||||||
git checkout -B "$branch" "origin/$branch"
|
|
||||||
git reset --hard "origin/$branch"
|
|
||||||
else
|
|
||||||
git checkout "$branch" 2>/dev/null || git checkout -b "$branch" "origin/$branch"
|
|
||||||
git merge --ff-only "origin/$branch"
|
|
||||||
fi
|
|
||||||
echo "当前: $(git rev-parse --short HEAD) $(git log -1 --pretty=%s)"
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 宇信达智建 — Linux 公共函数(宿主机物理目录模式,不依赖 Docker)
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
||||||
RUNTIME="$ROOT/.runtime"
|
|
||||||
LOG_DIR="$RUNTIME/logs"
|
|
||||||
PID_DIR="$RUNTIME/pids"
|
|
||||||
VENV_DIR="$RUNTIME/venv"
|
|
||||||
PGDATA="$RUNTIME/postgres"
|
|
||||||
UPLOAD_DIR="$ROOT/platform/data/uploads"
|
|
||||||
PG_PORT="${AJZ_PG_PORT:-5432}"
|
|
||||||
PG_USER="${AJZ_PG_USER:-platform}"
|
|
||||||
PG_PASS="${AJZ_PG_PASS:-platform}"
|
|
||||||
PG_DB="${AJZ_PG_DB:-platform}"
|
|
||||||
|
|
||||||
mkdir -p "$LOG_DIR" "$PID_DIR" "$UPLOAD_DIR"
|
|
||||||
|
|
||||||
c_info() { printf '\033[36m==> %s\033[0m\n' "$*"; }
|
|
||||||
c_ok() { printf '\033[32m OK %s\033[0m\n' "$*"; }
|
|
||||||
c_warn() { printf '\033[33m !! %s\033[0m\n' "$*"; }
|
|
||||||
c_err() { printf '\033[31m ERR %s\033[0m\n' "$*" >&2; }
|
|
||||||
|
|
||||||
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
|
||||||
|
|
||||||
docker_ready() {
|
|
||||||
have_cmd docker || return 1
|
|
||||||
docker info >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
port_listen() {
|
|
||||||
local port="$1"
|
|
||||||
if have_cmd ss; then
|
|
||||||
ss -ltn "( sport = :$port )" 2>/dev/null | grep -q ":$port"
|
|
||||||
elif have_cmd netstat; then
|
|
||||||
netstat -ltn 2>/dev/null | grep -q "[.:]$port "
|
|
||||||
else
|
|
||||||
(echo >/dev/tcp/127.0.0.1/"$port") >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_port() {
|
|
||||||
local port="$1" name="$2" sec="${3:-40}"
|
|
||||||
local i=0
|
|
||||||
while (( i < sec * 2 )); do
|
|
||||||
if port_listen "$port"; then
|
|
||||||
c_ok "$name 端口 $port 已监听"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 0.5
|
|
||||||
((i++)) || true
|
|
||||||
done
|
|
||||||
c_warn "$name 端口 $port 未在 ${sec}s 内就绪"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_http() {
|
|
||||||
local url="$1" name="$2" sec="${3:-40}"
|
|
||||||
local i=0
|
|
||||||
while (( i < sec * 2 )); do
|
|
||||||
if curl -fsS --max-time 2 "$url" >/dev/null 2>&1; then
|
|
||||||
c_ok "$name 就绪 $url"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 0.5
|
|
||||||
((i++)) || true
|
|
||||||
done
|
|
||||||
c_warn "$name 未在 ${sec}s 内就绪: $url"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_os() {
|
|
||||||
if [[ -f /etc/os-release ]]; then
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
. /etc/os-release
|
|
||||||
echo "${ID:-unknown}"
|
|
||||||
else
|
|
||||||
echo "unknown"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
sudo_run() {
|
|
||||||
if [[ "$(id -u)" -eq 0 ]]; then
|
|
||||||
"$@"
|
|
||||||
elif have_cmd sudo; then
|
|
||||||
sudo "$@"
|
|
||||||
else
|
|
||||||
c_err "需要 root 或 sudo 才能安装系统包: $*"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
export ROOT RUNTIME LOG_DIR PID_DIR VENV_DIR PGDATA UPLOAD_DIR
|
|
||||||
export PG_PORT PG_USER PG_PASS PG_DB
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 在宿主机物理目录自动配置环境(无 Docker)
|
|
||||||
# 数据落盘:$ROOT/.runtime/{postgres,venv,logs,pids} 与 platform/data/uploads
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
# shellcheck source=common.sh
|
|
||||||
source "$SCRIPT_DIR/common.sh"
|
|
||||||
|
|
||||||
c_info "根目录(物理): $ROOT"
|
|
||||||
c_info "运行时目录: $RUNTIME"
|
|
||||||
c_info "上传目录: $UPLOAD_DIR"
|
|
||||||
|
|
||||||
need_pkgs=()
|
|
||||||
have_cmd curl || need_pkgs+=(curl)
|
|
||||||
have_cmd git || need_pkgs+=(git)
|
|
||||||
have_cmd go || need_pkgs+=(golang-go)
|
|
||||||
have_cmd python3 || need_pkgs+=(python3)
|
|
||||||
have_cmd pip3 || need_pkgs+=(python3-pip)
|
|
||||||
python3 -c "import venv" 2>/dev/null || need_pkgs+=(python3-venv)
|
|
||||||
have_cmd node || need_pkgs+=(nodejs)
|
|
||||||
have_cmd npm || need_pkgs+=(npm)
|
|
||||||
have_cmd psql || need_pkgs+=(postgresql-client)
|
|
||||||
have_cmd initdb || have_cmd pg_ctl || need_pkgs+=(postgresql)
|
|
||||||
|
|
||||||
install_pkgs() {
|
|
||||||
local os
|
|
||||||
os="$(detect_os)"
|
|
||||||
c_info "安装系统包 (OS=$os): ${need_pkgs[*]}"
|
|
||||||
case "$os" in
|
|
||||||
ubuntu|debian|linuxmint|pop)
|
|
||||||
sudo_run apt-get update -y
|
|
||||||
local pkgs=()
|
|
||||||
for p in "${need_pkgs[@]}"; do
|
|
||||||
case "$p" in
|
|
||||||
golang-go) pkgs+=(golang-go) ;;
|
|
||||||
python3) pkgs+=(python3 python3-venv python3-dev) ;;
|
|
||||||
python3-pip) pkgs+=(python3-pip) ;;
|
|
||||||
python3-venv) pkgs+=(python3-venv) ;;
|
|
||||||
nodejs) pkgs+=(nodejs) ;;
|
|
||||||
npm) pkgs+=(npm) ;;
|
|
||||||
postgresql) pkgs+=(postgresql postgresql-contrib) ;;
|
|
||||||
postgresql-client) pkgs+=(postgresql-client) ;;
|
|
||||||
*) pkgs+=("$p") ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
sudo_run apt-get install -y "${pkgs[@]}"
|
|
||||||
;;
|
|
||||||
fedora|rhel|centos|rocky|almalinux)
|
|
||||||
local pkgs=()
|
|
||||||
for p in "${need_pkgs[@]}"; do
|
|
||||||
case "$p" in
|
|
||||||
golang-go) pkgs+=(golang) ;;
|
|
||||||
python3-pip) pkgs+=(python3-pip) ;;
|
|
||||||
python3-venv) pkgs+=(python3) ;;
|
|
||||||
postgresql) pkgs+=(postgresql-server postgresql) ;;
|
|
||||||
postgresql-client) pkgs+=(postgresql) ;;
|
|
||||||
*) pkgs+=("$p") ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
if have_cmd dnf; then sudo_run dnf install -y "${pkgs[@]}"
|
|
||||||
else sudo_run yum install -y "${pkgs[@]}"; fi
|
|
||||||
;;
|
|
||||||
arch|manjaro)
|
|
||||||
local pkgs=()
|
|
||||||
for p in "${need_pkgs[@]}"; do
|
|
||||||
case "$p" in
|
|
||||||
golang-go) pkgs+=(go) ;;
|
|
||||||
python3) pkgs+=(python) ;;
|
|
||||||
python3-pip) pkgs+=(python-pip) ;;
|
|
||||||
python3-venv) ;;
|
|
||||||
postgresql-client) pkgs+=(postgresql) ;;
|
|
||||||
*) pkgs+=("$p") ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
sudo_run pacman -Sy --noconfirm "${pkgs[@]}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
c_err "未识别发行版 $os,请手动安装: go python3 node npm postgresql curl"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((${#need_pkgs[@]} > 0)); then
|
|
||||||
install_pkgs
|
|
||||||
else
|
|
||||||
c_ok "系统依赖已满足"
|
|
||||||
fi
|
|
||||||
|
|
||||||
missing=()
|
|
||||||
for c in go python3 curl; do have_cmd "$c" || missing+=("$c"); done
|
|
||||||
have_cmd node || have_cmd nodejs || missing+=("node")
|
|
||||||
have_cmd npm || missing+=("npm")
|
|
||||||
if ((${#missing[@]} > 0)); then
|
|
||||||
c_err "仍缺少命令: ${missing[*]}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
c_ok "$(go version)"
|
|
||||||
|
|
||||||
c_info "配置 Python venv → $VENV_DIR"
|
|
||||||
if [[ ! -d "$VENV_DIR" ]]; then
|
|
||||||
python3 -m venv "$VENV_DIR"
|
|
||||||
fi
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
source "$VENV_DIR/bin/activate"
|
|
||||||
pip install -U pip wheel >/dev/null
|
|
||||||
pip install -r "$ROOT/ai-service/requirements.txt"
|
|
||||||
pip install -r "$ROOT/agent_sdk/requirements.txt"
|
|
||||||
c_ok "venv 依赖已安装"
|
|
||||||
|
|
||||||
c_info "安装前端依赖 → $ROOT/web/node_modules"
|
|
||||||
if [[ ! -d "$ROOT/web/node_modules" ]]; then
|
|
||||||
(cd "$ROOT/web" && npm install)
|
|
||||||
else
|
|
||||||
c_ok "web/node_modules 已存在"
|
|
||||||
fi
|
|
||||||
|
|
||||||
find_pg_bin() {
|
|
||||||
local name="$1"
|
|
||||||
if have_cmd "$name"; then command -v "$name"; return; fi
|
|
||||||
local d
|
|
||||||
for d in /usr/lib/postgresql/*/bin /usr/pgsql-*/bin; do
|
|
||||||
if [[ -x "$d/$name" ]]; then echo "$d/$name"; return; fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
INITDB="$(find_pg_bin initdb || true)"
|
|
||||||
PG_CTL="$(find_pg_bin pg_ctl || true)"
|
|
||||||
PSQL="$(find_pg_bin psql || true)"
|
|
||||||
|
|
||||||
ensure_local_postgres() {
|
|
||||||
if [[ -z "$INITDB" || -z "$PG_CTL" || -z "$PSQL" ]]; then
|
|
||||||
c_warn "未找到 initdb/pg_ctl/psql,跳过本地库初始化(请自备 Postgres)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 已有可连库则复用
|
|
||||||
if PGPASSWORD="$PG_PASS" "$PSQL" -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1" >/dev/null 2>&1; then
|
|
||||||
c_ok "复用已有 Postgres $PG_USER@$PG_DB:$PG_PORT"
|
|
||||||
PGPASSWORD="$PG_PASS" "$PSQL" -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
|
||||||
-c "ALTER USER $PG_USER CREATEDB;" >/dev/null 2>&1 || true
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$PGDATA" "$RUNTIME/pg_sockets"
|
|
||||||
if [[ ! -f "$PGDATA/PG_VERSION" ]]; then
|
|
||||||
c_info "initdb → $PGDATA (物理目录)"
|
|
||||||
"$INITDB" -D "$PGDATA" --auth=trust -U postgres
|
|
||||||
{
|
|
||||||
echo ""
|
|
||||||
echo "listen_addresses = '127.0.0.1'"
|
|
||||||
echo "port = $PG_PORT"
|
|
||||||
echo "unix_socket_directories = '$RUNTIME/pg_sockets'"
|
|
||||||
} >> "$PGDATA/postgresql.conf"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! "$PG_CTL" -D "$PGDATA" status >/dev/null 2>&1; then
|
|
||||||
c_info "启动本地 Postgres"
|
|
||||||
"$PG_CTL" -D "$PGDATA" -l "$LOG_DIR/postgres.log" -o "-k $RUNTIME/pg_sockets" start
|
|
||||||
sleep 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local sock="$RUNTIME/pg_sockets"
|
|
||||||
"$PSQL" -h "$sock" -U postgres -d postgres -v ON_ERROR_STOP=1 <<SQL
|
|
||||||
DO \$\$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$PG_USER') THEN
|
|
||||||
CREATE ROLE $PG_USER LOGIN PASSWORD '$PG_PASS' CREATEDB;
|
|
||||||
ELSE
|
|
||||||
ALTER ROLE $PG_USER WITH LOGIN PASSWORD '$PG_PASS' CREATEDB;
|
|
||||||
END IF;
|
|
||||||
END
|
|
||||||
\$\$;
|
|
||||||
SELECT 'ok' FROM pg_database WHERE datname = '$PG_DB';
|
|
||||||
SQL
|
|
||||||
if ! "$PSQL" -h "$sock" -U postgres -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='$PG_DB'" | grep -q 1; then
|
|
||||||
"$PSQL" -h "$sock" -U postgres -d postgres -c "CREATE DATABASE $PG_DB OWNER $PG_USER;"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 校验 TCP
|
|
||||||
if ! PGPASSWORD="$PG_PASS" "$PSQL" -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1" >/dev/null 2>&1; then
|
|
||||||
# 补充密码认证
|
|
||||||
cat > "$PGDATA/pg_hba.conf" <<EOF
|
|
||||||
local all all trust
|
|
||||||
host all all 127.0.0.1/32 trust
|
|
||||||
host all all ::1/128 trust
|
|
||||||
EOF
|
|
||||||
"$PG_CTL" -D "$PGDATA" reload || "$PG_CTL" -D "$PGDATA" restart -m fast
|
|
||||||
sleep 1
|
|
||||||
fi
|
|
||||||
PGPASSWORD="$PG_PASS" "$PSQL" -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1" >/dev/null
|
|
||||||
c_ok "Postgres 就绪 (物理 data=$PGDATA)"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_local_postgres
|
|
||||||
|
|
||||||
c_info "编译 platform / gateway"
|
|
||||||
(cd "$ROOT/platform" && go build -o platform .)
|
|
||||||
(cd "$ROOT/gateway" && go build -o gateway .)
|
|
||||||
c_ok "二进制已生成"
|
|
||||||
|
|
||||||
if [[ ! -f "$ROOT/ai-service/sample_inventory.xlsx" ]]; then
|
|
||||||
(cd "$ROOT/ai-service" && "$VENV_DIR/bin/python" make_sample_excel.py) || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
date -Iseconds > "$RUNTIME/setup.ok"
|
|
||||||
cat > "$RUNTIME/env.sh" <<EOF
|
|
||||||
# 由 setup-host.sh 生成 — 物理目录运行时
|
|
||||||
export AJZ_ROOT="$ROOT"
|
|
||||||
export AJZ_RUNTIME="$RUNTIME"
|
|
||||||
export PATH="$VENV_DIR/bin:\$PATH"
|
|
||||||
export PGHOST=127.0.0.1
|
|
||||||
export PGPORT=$PG_PORT
|
|
||||||
export PGUSER=$PG_USER
|
|
||||||
export PGPASSWORD=$PG_PASS
|
|
||||||
export PGDATABASE=$PG_DB
|
|
||||||
EOF
|
|
||||||
|
|
||||||
c_info "环境配置完成(无 Docker,全部落在物理目录)"
|
|
||||||
c_ok "下一步: ./start.sh"
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 宿主机物理目录模式(Docker 不可用或 ./start.sh --host)
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
# shellcheck source=common.sh
|
|
||||||
source "$SCRIPT_DIR/common.sh"
|
|
||||||
|
|
||||||
REBUILD=0
|
|
||||||
NO_BROWSER=0
|
|
||||||
for arg in "$@"; do
|
|
||||||
case "$arg" in
|
|
||||||
--rebuild|-Rebuild) REBUILD=1 ;;
|
|
||||||
--no-browser) NO_BROWSER=1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
start_bg() {
|
|
||||||
local name="$1" logfile="$2"
|
|
||||||
shift 2
|
|
||||||
local pidfile="$PID_DIR/$name.pid"
|
|
||||||
if [[ -f "$pidfile" ]] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then
|
|
||||||
c_warn "$name 已在运行 pid=$(cat "$pidfile")"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
c_info "启动 $name"
|
|
||||||
nohup "$@" >>"$logfile" 2>&1 &
|
|
||||||
echo $! >"$pidfile"
|
|
||||||
c_ok "$name pid=$(cat "$pidfile") 日志 $logfile"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_postgres_running() {
|
|
||||||
local pg_ctl=""
|
|
||||||
pg_ctl="$(command -v pg_ctl 2>/dev/null || true)"
|
|
||||||
if [[ -z "$pg_ctl" ]]; then
|
|
||||||
for d in /usr/lib/postgresql/*/bin /usr/pgsql-*/bin; do
|
|
||||||
[[ -x "$d/pg_ctl" ]] && pg_ctl="$d/pg_ctl" && break
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
if [[ -n "$pg_ctl" && -f "$PGDATA/PG_VERSION" ]]; then
|
|
||||||
if ! "$pg_ctl" -D "$PGDATA" status >/dev/null 2>&1; then
|
|
||||||
c_info "启动物理目录 Postgres → $PGDATA"
|
|
||||||
mkdir -p "$RUNTIME/pg_sockets"
|
|
||||||
"$pg_ctl" -D "$PGDATA" -l "$LOG_DIR/postgres.log" -o "-k $RUNTIME/pg_sockets" start
|
|
||||||
sleep 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
c_warn "宿主机模式:数据落在 $RUNTIME"
|
|
||||||
if [[ ! -f "$RUNTIME/setup.ok" || "$REBUILD" -eq 1 ]]; then
|
|
||||||
bash "$SCRIPT_DIR/setup-host.sh"
|
|
||||||
fi
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
[[ -f "$RUNTIME/env.sh" ]] && source "$RUNTIME/env.sh"
|
|
||||||
|
|
||||||
ensure_postgres_running
|
|
||||||
|
|
||||||
if [[ "$REBUILD" -eq 1 || ! -x "$ROOT/platform/platform" ]]; then
|
|
||||||
(cd "$ROOT/platform" && go build -o platform .)
|
|
||||||
fi
|
|
||||||
if [[ "$REBUILD" -eq 1 || ! -x "$ROOT/gateway/gateway" ]]; then
|
|
||||||
(cd "$ROOT/gateway" && go build -o gateway .)
|
|
||||||
fi
|
|
||||||
|
|
||||||
PY="$VENV_DIR/bin/python"
|
|
||||||
[[ -x "$PY" ]] || PY="$(command -v python3)"
|
|
||||||
|
|
||||||
start_bg platform "$LOG_DIR/platform.log" \
|
|
||||||
bash -c "cd '$ROOT/platform' && exec ./platform -f etc/platform.yaml"
|
|
||||||
start_bg ai "$LOG_DIR/ai.log" \
|
|
||||||
bash -c "cd '$ROOT/ai-service' && exec '$PY' -m uvicorn app:app --host 127.0.0.1 --port 8001"
|
|
||||||
start_bg gateway "$LOG_DIR/gateway.log" \
|
|
||||||
bash -c "cd '$ROOT/gateway' && exec ./gateway -f etc/gateway.yaml"
|
|
||||||
start_bg web "$LOG_DIR/web.log" \
|
|
||||||
bash -c "cd '$ROOT/web' && exec npm run dev -- --host 127.0.0.1 --port 5173"
|
|
||||||
|
|
||||||
wait_port 8888 platform 45 || true
|
|
||||||
wait_http "http://127.0.0.1:8001/health" ai-service 45 || true
|
|
||||||
wait_http "http://127.0.0.1:8180/gateway/health" gateway 45 || true
|
|
||||||
wait_port 5173 web 60 || true
|
|
||||||
|
|
||||||
cat <<EOF
|
|
||||||
|
|
||||||
========================================
|
|
||||||
前端 http://127.0.0.1:5173
|
|
||||||
网关 http://127.0.0.1:8180
|
|
||||||
账号 demo / demo123
|
|
||||||
模式 宿主机物理目录
|
|
||||||
停止 ./stop.sh
|
|
||||||
========================================
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [[ "$NO_BROWSER" -eq 0 ]] && have_cmd xdg-open; then
|
|
||||||
xdg-open "http://127.0.0.1:5173" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 停止宿主机模式进程
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
||||||
# shellcheck source=common.sh
|
|
||||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
|
|
||||||
|
|
||||||
c_info "停止宿主机模式服务..."
|
|
||||||
|
|
||||||
stop_pidfile() {
|
|
||||||
local name="$1"
|
|
||||||
local f="$PID_DIR/$name.pid"
|
|
||||||
if [[ -f "$f" ]]; then
|
|
||||||
local pid
|
|
||||||
pid="$(cat "$f")"
|
|
||||||
if kill -0 "$pid" 2>/dev/null; then
|
|
||||||
kill "$pid" 2>/dev/null || true
|
|
||||||
sleep 0.3
|
|
||||||
kill -9 "$pid" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
rm -f "$f"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
for n in web gateway ai platform; do
|
|
||||||
stop_pidfile "$n"
|
|
||||||
done
|
|
||||||
|
|
||||||
for port in 5173 8180 8001 8888; do
|
|
||||||
if have_cmd fuser; then
|
|
||||||
fuser -k "${port}/tcp" >/dev/null 2>&1 || true
|
|
||||||
elif have_cmd lsof; then
|
|
||||||
pids="$(lsof -t -iTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)"
|
|
||||||
[[ -n "$pids" ]] && kill $pids 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "${1:-}" == "--with-db" ]]; then
|
|
||||||
pg_ctl_bin="$(command -v pg_ctl 2>/dev/null || true)"
|
|
||||||
if [[ -z "$pg_ctl_bin" ]]; then
|
|
||||||
for d in /usr/lib/postgresql/*/bin /usr/pgsql-*/bin; do
|
|
||||||
[[ -x "$d/pg_ctl" ]] && pg_ctl_bin="$d/pg_ctl" && break
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
if [[ -n "$pg_ctl_bin" && -f "$PGDATA/PG_VERSION" ]]; then
|
|
||||||
"$pg_ctl_bin" -D "$PGDATA" stop -m fast || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
c_ok "已停止。数据仍在: $RUNTIME"
|
|
||||||
199
start-host.ps1
199
start-host.ps1
@@ -1,199 +0,0 @@
|
|||||||
# native process mode (called by start.ps1 when Docker is unavailable)
|
|
||||||
param(
|
|
||||||
[switch]$Rebuild,
|
|
||||||
[switch]$NoBrowser,
|
|
||||||
[switch]$ShowConsole # show service windows (debug); default: hidden
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$Root = $PSScriptRoot
|
|
||||||
Set-Location -LiteralPath $Root
|
|
||||||
$LogDir = Join-Path $Root ".runtime\logs"
|
|
||||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
|
||||||
|
|
||||||
# Always wipe previous native/docker leftovers before binding ports
|
|
||||||
Write-Host "==> Pre-clean before native start" -ForegroundColor DarkGray
|
|
||||||
& (Join-Path $Root "scripts\Stop-Stack.ps1") -Quiet
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
|
|
||||||
Write-Host "==> Native mode: need Go / Python / Node / PostgreSQL" -ForegroundColor Yellow
|
|
||||||
if (-not $ShowConsole) {
|
|
||||||
Write-Host " services run hidden; logs -> .runtime\logs\" -ForegroundColor DarkGray
|
|
||||||
}
|
|
||||||
|
|
||||||
function Clear-Port([int]$port) {
|
|
||||||
$conns = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
|
|
||||||
foreach ($c in $conns) {
|
|
||||||
$procId = $c.OwningProcess
|
|
||||||
if ($procId -gt 0) {
|
|
||||||
Write-Host " free port $port (pid=$procId)"
|
|
||||||
& taskkill.exe /F /T /PID $procId 2>$null | Out-Null
|
|
||||||
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "==> Clearing ports 8888/8001/8180/5173"
|
|
||||||
foreach ($p in 8888, 8001, 8180, 5173) { Clear-Port $p }
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
|
|
||||||
function Reset-LogFile([string]$path) {
|
|
||||||
try {
|
|
||||||
if (Test-Path -LiteralPath $path) {
|
|
||||||
# 句柄未释放时 Set-Content 会失败:先挪走旧文件
|
|
||||||
$prev = "$path.prev"
|
|
||||||
Remove-Item -LiteralPath $prev -Force -ErrorAction SilentlyContinue
|
|
||||||
Move-Item -LiteralPath $path -Destination $prev -Force -ErrorAction Stop
|
|
||||||
}
|
|
||||||
Set-Content -LiteralPath $path -Value "" -Encoding UTF8
|
|
||||||
return $path
|
|
||||||
} catch {
|
|
||||||
$alt = ($path -replace '\.log$', ".$(Get-Date -Format 'HHmmss').log")
|
|
||||||
Set-Content -LiteralPath $alt -Value "" -Encoding UTF8
|
|
||||||
return $alt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Start-Svc($name, $workDir, $filePath, $argumentList) {
|
|
||||||
$outLog = Reset-LogFile (Join-Path $LogDir "$name.out.log")
|
|
||||||
$errLog = Reset-LogFile (Join-Path $LogDir "$name.err.log")
|
|
||||||
$pidFile = Join-Path $LogDir "$name.pid"
|
|
||||||
|
|
||||||
if ($ShowConsole) {
|
|
||||||
$argStr = ""
|
|
||||||
if ($argumentList) {
|
|
||||||
$parts = @()
|
|
||||||
foreach ($a in $argumentList) {
|
|
||||||
if ($a -match '\s') { $parts += ('"' + $a + '"') } else { $parts += $a }
|
|
||||||
}
|
|
||||||
$argStr = $parts -join ' '
|
|
||||||
}
|
|
||||||
$cmd = "Set-Location -LiteralPath '$workDir'; Write-Host '[$name]' -ForegroundColor Cyan; & '$filePath' $argStr"
|
|
||||||
$p = Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
|
||||||
"-NoExit", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $cmd
|
|
||||||
) -WorkingDirectory $workDir -PassThru
|
|
||||||
} else {
|
|
||||||
$p = Start-Process -FilePath $filePath `
|
|
||||||
-ArgumentList $argumentList `
|
|
||||||
-WorkingDirectory $workDir `
|
|
||||||
-WindowStyle Hidden `
|
|
||||||
-RedirectStandardOutput $outLog `
|
|
||||||
-RedirectStandardError $errLog `
|
|
||||||
-PassThru
|
|
||||||
}
|
|
||||||
if ($p) { Set-Content -LiteralPath $pidFile -Value $p.Id -Encoding ASCII }
|
|
||||||
Write-Host " started $name (pid=$($p.Id))" -ForegroundColor DarkGray
|
|
||||||
}
|
|
||||||
|
|
||||||
$plat = Join-Path $Root "platform\platform.exe"
|
|
||||||
$gw = Join-Path $Root "gateway\gateway.exe"
|
|
||||||
# 默认重编译,避免改 Go 源码后 restart 仍跑旧 exe
|
|
||||||
$needPlat = $Rebuild -or -not (Test-Path $plat)
|
|
||||||
$needGw = $Rebuild -or -not (Test-Path $gw)
|
|
||||||
if (-not $needPlat -and (Test-Path $plat)) {
|
|
||||||
$exeTime = (Get-Item $plat).LastWriteTime
|
|
||||||
$newer = Get-ChildItem -Path (Join-Path $Root "platform") -Recurse -Include *.go -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object { $_.LastWriteTime -gt $exeTime } |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($newer) { $needPlat = $true }
|
|
||||||
}
|
|
||||||
if (-not $needGw -and (Test-Path $gw)) {
|
|
||||||
$exeTime = (Get-Item $gw).LastWriteTime
|
|
||||||
$newer = Get-ChildItem -Path (Join-Path $Root "gateway") -Recurse -Include *.go -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object { $_.LastWriteTime -gt $exeTime } |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($newer) { $needGw = $true }
|
|
||||||
}
|
|
||||||
if ($needPlat) {
|
|
||||||
Write-Host " building platform.exe ..." -ForegroundColor DarkGray
|
|
||||||
Push-Location (Join-Path $Root "platform")
|
|
||||||
go build -o platform.exe .
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
if ($needGw) {
|
|
||||||
Write-Host " building gateway.exe ..." -ForegroundColor DarkGray
|
|
||||||
Push-Location (Join-Path $Root "gateway")
|
|
||||||
go build -o gateway.exe .
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "==> Ensure web frontend (vite /#/preview)" -ForegroundColor DarkGray
|
|
||||||
& (Join-Path $Root "scripts\Ensure-Web.ps1")
|
|
||||||
# $? 判断 PS 脚本成败;勿单靠 $LASTEXITCODE(会残留 docker/compose 等 native 失败码)
|
|
||||||
if (-not $?) { throw "Ensure-Web failed" }
|
|
||||||
|
|
||||||
$pyCmd = Get-Command python -ErrorAction SilentlyContinue
|
|
||||||
if (-not $pyCmd) { $pyCmd = Get-Command py -ErrorAction SilentlyContinue }
|
|
||||||
if (-not $pyCmd) { throw "python not found" }
|
|
||||||
$py = $pyCmd.Source
|
|
||||||
|
|
||||||
$npmCmd = Get-Command npm.cmd -ErrorAction SilentlyContinue
|
|
||||||
if (-not $npmCmd) { $npmCmd = Get-Command npm -ErrorAction SilentlyContinue }
|
|
||||||
if (-not $npmCmd) { throw "npm not found" }
|
|
||||||
|
|
||||||
# AI 截图直达本机 Vite
|
|
||||||
$env:WEB_BASE = "http://127.0.0.1:5173"
|
|
||||||
$env:PLATFORM_BASE = "http://127.0.0.1:8888"
|
|
||||||
$env:GATEWAY_BASE = "http://127.0.0.1:8180"
|
|
||||||
$env:GENERATE_LOG_DIR = Join-Path $Root ".runtime\logs\generate"
|
|
||||||
$env:FIDELITY_SHOT_DIR = Join-Path $Root ".runtime\fidelity_shots"
|
|
||||||
|
|
||||||
Start-Svc "platform" (Join-Path $Root "platform") $plat @("-f", "etc/platform.yaml")
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
Start-Svc "ai-service" (Join-Path $Root "ai-service") $py @("-m", "uvicorn", "app:app", "--host", "127.0.0.1", "--port", "8001", "--reload")
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
Start-Svc "gateway" (Join-Path $Root "gateway") $gw @("-f", "etc/gateway.yaml")
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
Start-Svc "web" (Join-Path $Root "web") $npmCmd.Source @("run", "dev", "--", "--host", "127.0.0.1", "--port", "5173", "--strictPort")
|
|
||||||
|
|
||||||
Write-Host "==> Waiting for services..."
|
|
||||||
$deadline = (Get-Date).AddSeconds(45)
|
|
||||||
do {
|
|
||||||
$webOk = $false
|
|
||||||
$gwOk = $false
|
|
||||||
try {
|
|
||||||
$r = Invoke-WebRequest -Uri "http://127.0.0.1:5173/" -UseBasicParsing -TimeoutSec 2
|
|
||||||
if ($r.StatusCode -lt 500) { $webOk = $true }
|
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
$r = Invoke-WebRequest -Uri "http://127.0.0.1:8180/gateway/health" -UseBasicParsing -TimeoutSec 2
|
|
||||||
if ($r.StatusCode -lt 500) { $gwOk = $true }
|
|
||||||
} catch {}
|
|
||||||
if ($webOk -and $gwOk) { break }
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
} while ((Get-Date) -lt $deadline)
|
|
||||||
|
|
||||||
if ($webOk -and $gwOk) {
|
|
||||||
Write-Host "OK Web http://127.0.0.1:5173 Gateway http://127.0.0.1:8180 account demo/demo123" -ForegroundColor Green
|
|
||||||
Write-Host " logs $LogDir" -ForegroundColor DarkGray
|
|
||||||
} else {
|
|
||||||
Write-Host "WARN some services not ready yet. Check logs:" -ForegroundColor Yellow
|
|
||||||
Write-Host " $LogDir"
|
|
||||||
Write-Host " web=$webOk gateway=$gwOk"
|
|
||||||
Write-Host " (debug with visible windows: .\start-host.ps1 -ShowConsole)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 后台看门狗:ai-service / gateway 挂掉自动拉起
|
|
||||||
$watchPidFile = Join-Path $LogDir "watchdog.pid"
|
|
||||||
if (Test-Path $watchPidFile) {
|
|
||||||
$oldWatch = Get-Content $watchPidFile -ErrorAction SilentlyContinue
|
|
||||||
if ($oldWatch) { Stop-Process -Id ([int]$oldWatch) -Force -ErrorAction SilentlyContinue }
|
|
||||||
}
|
|
||||||
$watchScript = Join-Path $Root "watch-services.ps1"
|
|
||||||
$watchOut = Join-Path $LogDir "watchdog.out.log"
|
|
||||||
$watchErr = Join-Path $LogDir "watchdog.err.log"
|
|
||||||
Set-Content -LiteralPath $watchOut -Value "" -Encoding UTF8
|
|
||||||
Set-Content -LiteralPath $watchErr -Value "" -Encoding UTF8
|
|
||||||
$wp = Start-Process -FilePath "powershell.exe" `
|
|
||||||
-ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $watchScript) `
|
|
||||||
-WorkingDirectory $Root `
|
|
||||||
-WindowStyle Hidden `
|
|
||||||
-RedirectStandardOutput $watchOut `
|
|
||||||
-RedirectStandardError $watchErr `
|
|
||||||
-PassThru
|
|
||||||
if ($wp) {
|
|
||||||
Set-Content -LiteralPath $watchPidFile -Value $wp.Id -Encoding ASCII
|
|
||||||
Write-Host " started watchdog (pid=$($wp.Id)) - auto-restarts ai-service if down" -ForegroundColor DarkGray
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $NoBrowser) { Start-Process "http://127.0.0.1:5173" }
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
cd /d "%~dp0"
|
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0start.ps1" %*
|
|
||||||
175
start.ps1
175
start.ps1
@@ -1,175 +0,0 @@
|
|||||||
# aijianzhan one-click start (Docker Compose by default; auto-starts Docker Desktop)
|
|
||||||
param(
|
|
||||||
[switch]$Rebuild,
|
|
||||||
[switch]$UseHost,
|
|
||||||
[switch]$NoBrowser,
|
|
||||||
[switch]$ShowConsole, # native mode only: show service console windows
|
|
||||||
[int]$DockerWaitSec = 120
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$Root = $PSScriptRoot
|
|
||||||
Set-Location -LiteralPath $Root
|
|
||||||
|
|
||||||
function Write-Step([string]$msg) { Write-Host ""; Write-Host "==> $msg" -ForegroundColor Cyan }
|
|
||||||
function Write-Ok([string]$msg) { Write-Host " OK $msg" -ForegroundColor Green }
|
|
||||||
function Write-Warn([string]$msg) { Write-Host " !! $msg" -ForegroundColor Yellow }
|
|
||||||
|
|
||||||
function Test-DockerCli {
|
|
||||||
try {
|
|
||||||
$null = Get-Command docker -ErrorAction Stop
|
|
||||||
return $true
|
|
||||||
} catch {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test-DockerReady {
|
|
||||||
if (-not (Test-DockerCli)) { return $false }
|
|
||||||
try {
|
|
||||||
$old = $ErrorActionPreference
|
|
||||||
$ErrorActionPreference = "Continue"
|
|
||||||
$null = & docker info 2>&1
|
|
||||||
$ok = ($LASTEXITCODE -eq 0)
|
|
||||||
$ErrorActionPreference = $old
|
|
||||||
return $ok
|
|
||||||
} catch {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-DockerDesktopPath {
|
|
||||||
$pf86 = [Environment]::GetEnvironmentVariable("ProgramFiles(x86)")
|
|
||||||
$candidates = @(
|
|
||||||
(Join-Path $env:ProgramFiles "Docker\Docker\Docker Desktop.exe"),
|
|
||||||
$(if ($pf86) { Join-Path $pf86 "Docker\Docker\Docker Desktop.exe" } else { $null }),
|
|
||||||
(Join-Path $env:LOCALAPPDATA "Docker\Docker Desktop.exe")
|
|
||||||
)
|
|
||||||
foreach ($p in $candidates) {
|
|
||||||
if ($p -and (Test-Path -LiteralPath $p)) { return $p }
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Start-DockerDesktopIfNeeded {
|
|
||||||
if (Test-DockerReady) { return $true }
|
|
||||||
|
|
||||||
if (-not (Test-DockerCli)) {
|
|
||||||
Write-Warn "docker CLI not found; install Docker Desktop first"
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
$exe = Get-DockerDesktopPath
|
|
||||||
$proc = Get-Process -Name "Docker Desktop","com.docker.backend","com.docker.service" -ErrorAction SilentlyContinue
|
|
||||||
if (-not $proc) {
|
|
||||||
if (-not $exe) {
|
|
||||||
Write-Warn "Docker Desktop.exe not found"
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
Write-Step "Starting Docker Desktop"
|
|
||||||
Start-Process -FilePath $exe | Out-Null
|
|
||||||
Write-Ok "Docker Desktop launched; waiting for engine..."
|
|
||||||
} else {
|
|
||||||
Write-Step "Docker Desktop process found; waiting for engine..."
|
|
||||||
}
|
|
||||||
|
|
||||||
$started = Get-Date
|
|
||||||
$deadline = $started.AddSeconds([Math]::Max(30, $DockerWaitSec))
|
|
||||||
$n = 0
|
|
||||||
while ((Get-Date) -lt $deadline) {
|
|
||||||
if (Test-DockerReady) {
|
|
||||||
Write-Ok "Docker engine ready"
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
$n++
|
|
||||||
if ($n % 5 -eq 0) {
|
|
||||||
$elapsed = [int]((Get-Date) - $started).TotalSeconds
|
|
||||||
Write-Host (" ...waiting Docker ({0}s / {1}s)" -f $elapsed, $DockerWaitSec) -ForegroundColor DarkGray
|
|
||||||
}
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
}
|
|
||||||
Write-Warn ("Docker wait timeout ({0}s)" -f $DockerWaitSec)
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
function Start-NativeFallback([string]$reason) {
|
|
||||||
Write-Warn $reason
|
|
||||||
Write-Step "Fallback to native mode (start-host.ps1)"
|
|
||||||
& (Join-Path $Root "start-host.ps1") -Rebuild:$Rebuild -NoBrowser:$NoBrowser -ShowConsole:$ShowConsole
|
|
||||||
exit $LASTEXITCODE
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UseHost) {
|
|
||||||
Start-NativeFallback "UseHost specified"
|
|
||||||
}
|
|
||||||
|
|
||||||
$dockerReady = Start-DockerDesktopIfNeeded
|
|
||||||
if (-not $dockerReady) {
|
|
||||||
Start-NativeFallback "Docker not ready; fallback native. Fix Docker then rerun .\start.ps1"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Step "Apply China Docker registry mirrors"
|
|
||||||
& (Join-Path $Root "scripts\Apply-DockerMirrors.ps1") -NoRestartHint
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path (Join-Path $Root ".runtime\pgdata") | Out-Null
|
|
||||||
New-Item -ItemType Directory -Force -Path (Join-Path $Root ".runtime\uploads") | Out-Null
|
|
||||||
New-Item -ItemType Directory -Force -Path (Join-Path $Root ".runtime\logs") | Out-Null
|
|
||||||
|
|
||||||
# Wipe native leftovers so Docker and host never fight over the same ports
|
|
||||||
Write-Step "Pre-clean ports / native leftovers"
|
|
||||||
& (Join-Path $Root "scripts\Stop-Stack.ps1") -Quiet
|
|
||||||
|
|
||||||
Write-Step "Build web dist (auto, includes /#/preview)"
|
|
||||||
& (Join-Path $Root "scripts\Ensure-Web.ps1") -BuildDist
|
|
||||||
if (-not $?) {
|
|
||||||
Start-NativeFallback "web dist build failed; fallback native (vite)."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prefer DaoCloud-prefixed library images (see docker-compose.yml / Dockerfiles)
|
|
||||||
if (-not $env:DOCKER_BASE_REGISTRY) {
|
|
||||||
$env:DOCKER_BASE_REGISTRY = "docker.m.daocloud.io/library"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Step "Docker Compose up (data -> .runtime)"
|
|
||||||
# Prefer reuse of local images; --build only when -Rebuild (Hub often fails in CN)
|
|
||||||
$composeArgs = @("compose", "up", "-d")
|
|
||||||
if ($Rebuild) { $composeArgs += "--build" }
|
|
||||||
& docker @composeArgs
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
Write-Warn "compose up failed; retry once with --build"
|
|
||||||
& docker compose up -d --build
|
|
||||||
}
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
# Hub / network failures are common in CN; keep the stack usable via native mode
|
|
||||||
Start-NativeFallback "docker compose failed (Docker Hub/network). Auto fallback to native mode."
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Step "Waiting for gateway / web"
|
|
||||||
$ok = $false
|
|
||||||
for ($i = 0; $i -lt 60; $i++) {
|
|
||||||
try {
|
|
||||||
$h = Invoke-WebRequest -Uri "http://127.0.0.1:8180/gateway/health" -UseBasicParsing -TimeoutSec 2
|
|
||||||
$w = Invoke-WebRequest -Uri "http://127.0.0.1:5173/" -UseBasicParsing -TimeoutSec 2
|
|
||||||
if ($h.StatusCode -lt 500 -and $w.StatusCode -lt 500) { $ok = $true; break }
|
|
||||||
} catch {
|
|
||||||
# retry
|
|
||||||
}
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
}
|
|
||||||
if ($ok) { Write-Ok "services ready" } else { Write-Warn "health check timeout, see: docker compose logs" }
|
|
||||||
|
|
||||||
$sql = 'ALTER USER platform CREATEDB;'
|
|
||||||
& docker compose exec -T postgres psql -U platform -d platform -c $sql 2>&1 | Out-Null
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "========================================" -ForegroundColor Green
|
|
||||||
Write-Host " Web http://127.0.0.1:5173"
|
|
||||||
Write-Host " Gateway http://127.0.0.1:8180"
|
|
||||||
Write-Host " Account demo / demo123"
|
|
||||||
Write-Host " Data $Root\.runtime"
|
|
||||||
Write-Host " Stop .\stop.ps1"
|
|
||||||
Write-Host "========================================" -ForegroundColor Green
|
|
||||||
|
|
||||||
if (-not $NoBrowser) {
|
|
||||||
Start-Process "http://127.0.0.1:5173"
|
|
||||||
}
|
|
||||||
45
start.sh
45
start.sh
@@ -1,45 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# AI 建站 — Linux 一键启动(Docker Compose)
|
|
||||||
# 用法:cd 项目根 && ./start.sh [--rebuild] [--no-pull]
|
|
||||||
# 默认从 Gitea 拉取最新代码再启动(可用 --no-pull / AIJZ_SKIP_GIT_PULL=1 跳过)
|
|
||||||
# 行尾:LF
|
|
||||||
set -euo pipefail
|
|
||||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
cd "$ROOT"
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
. "$ROOT/scripts/lib-aijz-deploy.sh"
|
|
||||||
|
|
||||||
REBUILD=0
|
|
||||||
DO_PULL=1
|
|
||||||
for a in "$@"; do
|
|
||||||
case "$a" in
|
|
||||||
--rebuild|-r) REBUILD=1 ;;
|
|
||||||
--no-pull) DO_PULL=0 ;;
|
|
||||||
--pull) DO_PULL=1 ;;
|
|
||||||
-h|--help)
|
|
||||||
echo "用法: $0 [--rebuild] [--no-pull|--pull]"
|
|
||||||
echo " 默认拉取: ${AIJZ_GIT_URL:-https://gitea.yuxindazhineng.com/whm/ai_site.git} 分支 ${GIT_BRANCH:-main}"
|
|
||||||
echo " 跳过拉取: --no-pull 或 AIJZ_SKIP_GIT_PULL=1"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
if [ "${AIJZ_SKIP_GIT_PULL:-0}" = "1" ]; then
|
|
||||||
DO_PULL=0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
echo " AI 建站 启动"
|
|
||||||
echo " 路径: $ROOT"
|
|
||||||
echo "========================================"
|
|
||||||
|
|
||||||
if [ "$DO_PULL" = "1" ]; then
|
|
||||||
git_pull_hard
|
|
||||||
else
|
|
||||||
echo "已跳过代码拉取(--no-pull / AIJZ_SKIP_GIT_PULL)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_docker
|
|
||||||
stack_up "$REBUILD"
|
|
||||||
healthcheck
|
|
||||||
print_endpoints
|
|
||||||
5
stop.bat
5
stop.bat
@@ -1,5 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
cd /d "%~dp0"
|
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0stop.ps1"
|
|
||||||
pause
|
|
||||||
6
stop.ps1
6
stop.ps1
@@ -1,6 +0,0 @@
|
|||||||
# Stop all stack processes (Docker + native). Thin wrapper around scripts\Stop-Stack.ps1.
|
|
||||||
$ErrorActionPreference = "Continue"
|
|
||||||
$Root = $PSScriptRoot
|
|
||||||
Set-Location -LiteralPath $Root
|
|
||||||
& (Join-Path $Root "scripts\Stop-Stack.ps1")
|
|
||||||
exit $LASTEXITCODE
|
|
||||||
Reference in New Issue
Block a user