docs: 新增 deploy-ai.sh README 与部署连通性检查(v2026.05.14)
- [新增] deploy-ai-readme.md 完整文档 - [新增] deploy-ai.sh 部署后自动验证 issue→assets API 连通性
This commit is contained in:
commit
a7e0651bd5
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# 云端数据库一键备份脚本
|
||||||
|
# 用法:bash backup-db.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
LOCAL_DIR="/Users/niuniu/programs/docker/db-backups"
|
||||||
|
|
||||||
|
log() { echo "[$(date '+%H:%M:%S')] $1"; }
|
||||||
|
|
||||||
|
log "开始备份云端数据库..."
|
||||||
|
|
||||||
|
# 云端打包
|
||||||
|
ssh txjp "mkdir -p /tmp/db-backup && \
|
||||||
|
cp /root/docker/assets-ai/data/assets.db /tmp/db-backup/assets-${TIMESTAMP}.db && \
|
||||||
|
cp /root/docker/issue-ai/data/issue.db /tmp/db-backup/issue-${TIMESTAMP}.db"
|
||||||
|
|
||||||
|
# 复制到本地
|
||||||
|
log "复制到本地..."
|
||||||
|
scp "txjp:/tmp/db-backup/assets-${TIMESTAMP}.db" "${LOCAL_DIR}/"
|
||||||
|
scp "txjp:/tmp/db-backup/issue-${TIMESTAMP}.db" "${LOCAL_DIR}/"
|
||||||
|
|
||||||
|
# 清理云端临时文件
|
||||||
|
ssh txjp "rm -rf /tmp/db-backup"
|
||||||
|
|
||||||
|
log "备份完成:${TIMESTAMP}"
|
||||||
|
ls -lh "${LOCAL_DIR}"/assets-${TIMESTAMP}.db "${LOCAL_DIR}"/issue-${TIMESTAMP}.db 2>/dev/null
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
# deploy-ai.sh — tlyq.ai 站点部署脚本
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
`deploy-ai.sh` 将本地代码部署到 tlyq.ai 云服务器(txjp,IP: 43.133.38.210)。采用"源码上传 → 服务器构建"模式,本地不做生产构建。
|
||||||
|
|
||||||
|
**核心流程**:打包源码 → 上传至 txjp → 服务器 `npm install` + `npm run build` → 容器重启生效
|
||||||
|
|
||||||
|
## 用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/deploy-ai.sh [选项]
|
||||||
|
```
|
||||||
|
|
||||||
|
| 选项 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 无参数 | 自动检测:初次构建 or 增量更新(源码快照比对) |
|
||||||
|
| `--force` | 强制完整构建(跳过快照比对) |
|
||||||
|
| `--restart` | 仅重启容器(跳过构建) |
|
||||||
|
| `--init` | 强制初次完整构建 |
|
||||||
|
|
||||||
|
## 站点选择
|
||||||
|
|
||||||
|
脚本启动后显示交互式菜单:
|
||||||
|
|
||||||
|
| 编号 | 站点 | 容器 | 构建方式 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| 1 | www.tlyq.ai | — | Next.js `output: 'export'` 静态站点 |
|
||||||
|
| 2 | cloud.tlyq.ai | — | 纯静态 HTML |
|
||||||
|
| 3 | token.tlyq.ai | — | 纯静态 HTML |
|
||||||
|
| 4 | issue.tlyq.ai | issue-ai | Next.js standalone(Docker) |
|
||||||
|
| 5 | assets.tlyq.ai | assets-ai | Next.js standalone(Docker) |
|
||||||
|
| 6 | oa.tlyq.ai | oa-ai | Next.js standalone(Docker) |
|
||||||
|
|
||||||
|
## 部署流程
|
||||||
|
|
||||||
|
### 通用步骤
|
||||||
|
|
||||||
|
1. **链接替换**:`localhost` → 正式域名(www/cloud/token 站点;OA 站点额外替换内部 API 地址)
|
||||||
|
2. **快照比对**:计算本地源码 MD5,与服务器上次快照对比,无变化则跳过构建
|
||||||
|
3. **打包上传**:tar 打包(排除 `node_modules`、`.next`、`.env` 等),scp 上传至 txjp
|
||||||
|
4. **解压同步**:rsync 同步源码到目标目录
|
||||||
|
5. **依赖安装**:服务器 `npm install --prefer-offline`
|
||||||
|
6. **环境变量**:写入生产环境所需的环境变量到 `.env`
|
||||||
|
7. **构建**:服务器 `npm run build`
|
||||||
|
8. **清理**:删除 Alpine musl 残留(sharp、swc)
|
||||||
|
9. **重启容器**:`docker compose up -d` + `docker compose restart`
|
||||||
|
10. **快照保存**:保存本次源码 MD5 用于下次比对
|
||||||
|
|
||||||
|
### issue-ai 专属
|
||||||
|
|
||||||
|
- 构建前写入 `ASSETS_API_URL=http://assets-ai:3000/api`
|
||||||
|
- 构建前写入 `NEXT_PUBLIC_ASSETS_URL=https://assets.tlyq.ai`
|
||||||
|
- **部署后自动验证 issue→assets API 连通性**(见下方)
|
||||||
|
|
||||||
|
### assets-ai 专属
|
||||||
|
|
||||||
|
- 构建前写入 `NEXT_PUBLIC_ISSUE_URL=https://issue.tlyq.ai/tickets`
|
||||||
|
- 构建前写入 `ISSUE_API_URL=http://issue-ai:3000/api`
|
||||||
|
|
||||||
|
### OA 专属
|
||||||
|
|
||||||
|
- 替换三类 URL:前端导航卡片(`localhost` → 生产域名)、服务端内部 API(`localhost` → 容器名)、退出登录重定向
|
||||||
|
|
||||||
|
## 部署后验证
|
||||||
|
|
||||||
|
### 公共 URL 检查
|
||||||
|
|
||||||
|
部署完成后检查站点 HTTP 状态码,200/301/307 视为成功。
|
||||||
|
|
||||||
|
### issue→assets API 连通性检查
|
||||||
|
|
||||||
|
部署 issue-ai 后自动执行:
|
||||||
|
|
||||||
|
1. 将连通性检查脚本发送到 txjp 宿主机
|
||||||
|
2. `docker cp` 到 issue-ai 容器内
|
||||||
|
3. 使用容器内的 `ASSETS_API_KEY` 调 `ASSETS_API_URL/assets?pageSize=1`
|
||||||
|
4. 返回 200 → 通过;返回 401 → **部署失败退出**,提示检查 API Key 配置
|
||||||
|
|
||||||
|
**错误输出示例**:
|
||||||
|
```
|
||||||
|
[✗] issue→assets API 连通性检查失败!请检查 ASSETS_API_KEY 是否在 assets-ai 中注册
|
||||||
|
连通失败: HTTP 401 (请检查 ASSETS_API_KEY 是否在 assets-ai 的 API Keys 中注册)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 排除文件
|
||||||
|
|
||||||
|
打包时排除以下目录和文件,不上传到服务器:
|
||||||
|
|
||||||
|
| 类别 | 排除项 |
|
||||||
|
|------|--------|
|
||||||
|
| 依赖 | `node_modules` |
|
||||||
|
| 构建产物 | `.next`、`out`、`data` |
|
||||||
|
| 环境配置 | `.env`、`.env.local`、`.env.development`、`.env.production` |
|
||||||
|
| 版本控制 | `.git` |
|
||||||
|
| 文档 | `docs` |
|
||||||
|
| 报告 | `reports/*.docx` |
|
||||||
|
| macOS | `._*` |
|
||||||
|
|
||||||
|
部署后服务器上的 `.env.local` / `.env.development` / `.env.production` 会被自动删除,防止覆盖 `.env` 中的生产配置。
|
||||||
|
|
||||||
|
## 源码快照机制
|
||||||
|
|
||||||
|
脚本计算本地源码文件的组合 MD5,保存到服务器的 `/tmp/.snapshot.{site}.md5`。下次部署时比对,若无变化则跳过构建仅重启容器。`--force` 可绕过此机制。
|
||||||
|
|
||||||
|
快照包含:所有源码文件(排除 `node_modules`、`.next`、`out`、`data`、`reports`、`docs`、`.env*`、`.git`)。
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 构建失败
|
||||||
|
```bash
|
||||||
|
# 查看服务器构建日志
|
||||||
|
ssh txjp "tail -100 /tmp/build_output.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 容器启动失败
|
||||||
|
```bash
|
||||||
|
ssh txjp "docker logs {container}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 连通性检查失败
|
||||||
|
```bash
|
||||||
|
# 手动验证 issue→assets 连通性
|
||||||
|
ssh txjp "docker exec issue-ai sh -c 'cd /app && NODE_PATH=/app/node_modules node -e \"
|
||||||
|
const http = require(\\\"http\\\");
|
||||||
|
const url = process.env.ASSETS_API_URL + \\\"/assets?pageSize=1\\\";
|
||||||
|
const key = process.env.ASSETS_API_KEY || \\\"\\\";
|
||||||
|
http.get(url, {headers:{\\\"Authorization\\\":\\\"Bearer \\\"+key}}, (res) => {
|
||||||
|
console.log(\\\"status:\\\", res.statusCode);
|
||||||
|
process.exit(res.statusCode === 200 ? 0 : 1);
|
||||||
|
}).on(\\\"error\\\", (e) => { console.error(e.message); process.exit(1); });
|
||||||
|
\"'"
|
||||||
|
|
||||||
|
# 确认 assets-ai 的 api_keys 表中是否有对应的 key
|
||||||
|
ssh txjp "docker exec assets-ai node -e \"
|
||||||
|
const db = require('better-sqlite3')('/app/data/assets.db');
|
||||||
|
const rows = db.prepare('SELECT id, name, is_active FROM api_keys').all();
|
||||||
|
rows.forEach(r => console.log(JSON.stringify(r)));
|
||||||
|
\""
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- **跨容器凭据**:禁止在代码或配置中硬编码另一服务的密码,通过运行时读取源容器环境变量获取
|
||||||
|
- **共享 JWT 密钥**:所有 `.tlyq.ai` 子站点的 `JWT_SECRET` 和 `COOKIE_DOMAIN` 必须一致
|
||||||
|
- **日期时区**:系统统一 UTC+8,禁止使用 `toISOString()` 和 `datetime('now')`
|
||||||
|
- **nginx 重载**:容器重启后需 `docker restart nginx-ai` 清除 DNS 缓存
|
||||||
|
- **新增 npm 依赖**:需重建 Docker 镜像(`docker compose build --no-cache`),因为容器内 `/app/node_modules/` 来自镜像构建时
|
||||||
|
|
@ -0,0 +1,409 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# 本地修改 → 上传服务器 → 构建/更新 → 部署 (tlyq.ai)
|
||||||
|
#
|
||||||
|
# 策略:服务器上直接 npm run build,复用容器内已安装的 node_modules
|
||||||
|
#
|
||||||
|
# 支持系统:
|
||||||
|
# - macOS (本地): zsh + bash 兼容模式
|
||||||
|
# - Ubuntu/Debian (服务器): dash (符号链接到 sh) + bash
|
||||||
|
# - Rocky Linux/CentOS/RHEL (服务器): bash
|
||||||
|
#
|
||||||
|
# 用法:bash deploy-ai.sh [选项]
|
||||||
|
# 无参数 — 自动检测(初次构建 or 增量更新)
|
||||||
|
# --force — 强制完整构建
|
||||||
|
# --restart — 仅重启容器(跳过构建)
|
||||||
|
# --init — 强制初次完整构建
|
||||||
|
|
||||||
|
# 不使用 set -e,改用显式错误处理,避免管道命令导致意外退出
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 颜色和日志
|
||||||
|
# ============================================================
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; }
|
||||||
|
warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; }
|
||||||
|
info() { printf "${CYAN}[i]${NC} %s\n" "$1"; }
|
||||||
|
error() { printf "${RED}[✗]${NC} %s\n" "$1"; }
|
||||||
|
timing(){ printf " ${CYAN}⏱${NC} %s\n" "$1"; }
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 检测 macOS 和 Linux sed 差异
|
||||||
|
# ============================================================
|
||||||
|
is_mac() {
|
||||||
|
[[ "$(uname)" == "Darwin" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# macOS 的 sed -i 需要空后缀 '',Linux 不需要
|
||||||
|
SED_I_BACKUP='-i'
|
||||||
|
if is_mac; then
|
||||||
|
SED_I_BACKUP='-i ""'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 解析参数
|
||||||
|
# ============================================================
|
||||||
|
FORCE_BUILD=0; ONLY_RESTART=0; INIT_BUILD=0
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--force) FORCE_BUILD=1; info "模式: 强制完整构建"; shift ;;
|
||||||
|
--restart) ONLY_RESTART=1; info "模式: 仅重启容器"; shift ;;
|
||||||
|
--init) INIT_BUILD=1; info "模式: 初次完整构建"; shift ;;
|
||||||
|
*) warn "未知参数: $1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 选择站点
|
||||||
|
# ============================================================
|
||||||
|
echo ""
|
||||||
|
printf "${CYAN}=========================================${NC}\n"
|
||||||
|
printf "${CYAN} 选择要部署的站点 (tlyq.ai)${NC}\n"
|
||||||
|
printf "${CYAN}=========================================${NC}\n"
|
||||||
|
echo ""
|
||||||
|
echo " 1) www.tlyq.ai (图灵引擎官网)"
|
||||||
|
echo " 2) cloud.tlyq.ai (图灵智算系统云平台)"
|
||||||
|
echo " 3) token.tlyq.ai (Token工厂)"
|
||||||
|
echo " 4) issue.tlyq.ai (工单系统)"
|
||||||
|
echo " 5) assets.tlyq.ai (资产管理系统)"
|
||||||
|
echo " 6) oa.tlyq.ai (OA 统一门户 — 含 nginx 配置)"
|
||||||
|
echo ""
|
||||||
|
printf "请输入编号 (1/2/3/4/5/6): "
|
||||||
|
read choice
|
||||||
|
|
||||||
|
SITE=""; LOCAL_DIR=""; REMOTE_DIR=""; CONTAINER=""
|
||||||
|
case "$choice" in
|
||||||
|
1) SITE="www"; LOCAL_DIR="/Users/niuniu/programs/docker/www-ai/src"; REMOTE_DIR="/tmp/project-extract-2/turing-engine" ;;
|
||||||
|
2) SITE="cloud"; LOCAL_DIR="/Users/niuniu/programs/docker/cloud-ai/html"; REMOTE_DIR="/root/docker/cloud-ai/html" ;;
|
||||||
|
3) SITE="token"; LOCAL_DIR="/Users/niuniu/programs/docker/token-ai/html"; REMOTE_DIR="/root/docker/token-ai/html" ;;
|
||||||
|
4) SITE="issue"; LOCAL_DIR="/Users/niuniu/programs/docker/issue-ai"; REMOTE_DIR="/root/docker/issue-ai"; CONTAINER="issue-ai" ;;
|
||||||
|
5) SITE="assets"; LOCAL_DIR="/Users/niuniu/programs/docker/assets-ai"; REMOTE_DIR="/root/docker/assets-ai"; CONTAINER="assets-ai" ;;
|
||||||
|
6) SITE="oa"; LOCAL_DIR="/Users/niuniu/programs/docker/oa-ai"; REMOTE_DIR="/root/docker/oa-ai"; CONTAINER="oa-ai" ;;
|
||||||
|
*) echo "无效选择"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "部署目标: $LOCAL_DIR → $REMOTE_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 链接替换
|
||||||
|
# ============================================================
|
||||||
|
replace_url() {
|
||||||
|
local mode="$1"
|
||||||
|
local from to
|
||||||
|
if [[ "$mode" == "prod" ]]; then
|
||||||
|
from="localhost"; to="https://www.tlyq.ai"
|
||||||
|
else
|
||||||
|
from="https://www.tlyq.ai"; to="localhost"
|
||||||
|
fi
|
||||||
|
case "$SITE" in
|
||||||
|
www)
|
||||||
|
cd "$LOCAL_DIR"
|
||||||
|
find src -type f -name '*.tsx' -exec sed $SED_I_BACKUP \
|
||||||
|
-e "s|${from}:6174|https://cloud.tlyq.ai|g" \
|
||||||
|
-e "s|${from}:6175|https://token.tlyq.ai|g" \
|
||||||
|
-e "s|https://cloud.tlyq.ai|${from}:6174|g" \
|
||||||
|
-e "s|https://token.tlyq.ai|${from}:6175|g" \
|
||||||
|
{} + 2>/dev/null || true ;;
|
||||||
|
cloud)
|
||||||
|
sed $SED_I_BACKUP "s|${from}:6173|https://www.tlyq.ai|g" "$LOCAL_DIR/index.html" 2>/dev/null || true
|
||||||
|
sed $SED_I_BACKUP "s|https://www.tlyq.ai|${from}:6173|g" "$LOCAL_DIR/index.html" 2>/dev/null || true ;;
|
||||||
|
token)
|
||||||
|
sed $SED_I_BACKUP "s|${from}:6173|https://www.tlyq.ai|g" "$LOCAL_DIR/index.html" 2>/dev/null || true
|
||||||
|
sed $SED_I_BACKUP "s|https://www.tlyq.ai|${from}:6173|g" "$LOCAL_DIR/index.html" 2>/dev/null || true ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# OA 专属 URL 替换(三类:前端导航 / 服务端内部 API / 退出登录)
|
||||||
|
replace_oa_urls() {
|
||||||
|
local mode="$1" # prod | local
|
||||||
|
cd "$LOCAL_DIR"
|
||||||
|
if [[ "$mode" == "prod" ]]; then
|
||||||
|
# A. 前端导航卡片 → 生产域名
|
||||||
|
sed $SED_I_BACKUP \
|
||||||
|
-e 's|http://localhost:6177|https://assets.tlyq.ai|g' \
|
||||||
|
-e 's|http://localhost:6176|https://issue.tlyq.ai|g' \
|
||||||
|
-e 's|http://localhost:6173|https://www.tlyq.ai|g' \
|
||||||
|
-e 's|http://localhost:6174|https://cloud.tlyq.ai|g' \
|
||||||
|
-e 's|http://localhost:6175|https://token.tlyq.ai|g' \
|
||||||
|
src/app/page.tsx
|
||||||
|
# B. 服务端内部 API 调用 → Docker 容器名(assets-ai:3000 / issue-ai:3000)
|
||||||
|
find src/app/api -name "*.ts" -exec sed $SED_I_BACKUP \
|
||||||
|
-e "s|'http://localhost:6177'|'http://assets-ai:3000'|g" \
|
||||||
|
-e "s|'http://localhost:6176'|'http://issue-ai:3000'|g" \
|
||||||
|
-e "s|\`http://localhost:\${site === 'assets' ? 6177 : 6176}\`|\`http://\${site}-ai:3000\`|g" \
|
||||||
|
{} +
|
||||||
|
# C. 退出登录重定向
|
||||||
|
sed $SED_I_BACKUP "s|'http://localhost:6179'|'https://oa.tlyq.ai'|g" src/app/api/auth/logout/route.ts
|
||||||
|
else
|
||||||
|
# 恢复本地 URL
|
||||||
|
sed $SED_I_BACKUP \
|
||||||
|
-e 's|https://assets.tlyq.ai|http://localhost:6177|g' \
|
||||||
|
-e 's|https://issue.tlyq.ai|http://localhost:6176|g' \
|
||||||
|
-e 's|https://www.tlyq.ai|http://localhost:6173|g' \
|
||||||
|
-e 's|https://cloud.tlyq.ai|http://localhost:6174|g' \
|
||||||
|
-e 's|https://token.tlyq.ai|http://localhost:6175|g' \
|
||||||
|
src/app/page.tsx
|
||||||
|
find src/app/api -name "*.ts" -exec sed $SED_I_BACKUP \
|
||||||
|
-e "s|'http://assets-ai:3000'|'http://localhost:6177'|g" \
|
||||||
|
-e "s|'http://issue-ai:3000'|'http://localhost:6176'|g" \
|
||||||
|
-e "s|\`http://\${site}-ai:3000\`|\`http://localhost:\${site === 'assets' ? 6177 : 6176}\`|g" \
|
||||||
|
{} +
|
||||||
|
sed $SED_I_BACKUP "s|'https://oa.tlyq.ai'|'http://localhost:6179'|g" src/app/api/auth/logout/route.ts
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
to_prod() { [[ "$SITE" != "issue" && "$SITE" != "assets" && "$SITE" != "oa" ]] && replace_url prod; }
|
||||||
|
to_local() { [[ "$SITE" != "issue" && "$SITE" != "assets" && "$SITE" != "oa" ]] && replace_url local; }
|
||||||
|
trap 'to_local; [[ "$SITE" == "oa" ]] && replace_oa_urls local' EXIT
|
||||||
|
[[ "$SITE" == "oa" ]] && replace_oa_urls prod
|
||||||
|
[[ "$SITE" != "issue" && "$SITE" != "assets" && "$SITE" != "oa" ]] && to_prod
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 核心构建函数(服务器上直接 npm run build)
|
||||||
|
# ============================================================
|
||||||
|
SNAPSHOT_FILE="/tmp/.snapshot.${SITE}.md5"
|
||||||
|
BUILD_START=$(date +%s)
|
||||||
|
|
||||||
|
build_on_server() {
|
||||||
|
local extra_env="${1:-}"
|
||||||
|
|
||||||
|
# 1. 计算源码快照
|
||||||
|
log "检查源码是否有变化..."
|
||||||
|
local_md5=$(find "$LOCAL_DIR" \
|
||||||
|
-not -path '*/node_modules/*' \
|
||||||
|
-not -path '*/.next/*' \
|
||||||
|
-not -path '*/out/*' \
|
||||||
|
-not -path '*/data/*' \
|
||||||
|
-not -path '*/reports/*' \
|
||||||
|
-not -path '*/docs/*' \
|
||||||
|
-not -path '*/.env*' \
|
||||||
|
-not -path '*/.git/*' \
|
||||||
|
-not -name '._*' \
|
||||||
|
-type f \
|
||||||
|
-exec md5 -q {} \; \
|
||||||
|
| sort \
|
||||||
|
| md5 -q)
|
||||||
|
echo " 源码快照: ${local_md5}"
|
||||||
|
|
||||||
|
prev_md5=$(ssh txjp "cat ${SNAPSHOT_FILE} 2>/dev/null" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ "$FORCE_BUILD" == "1" || "$INIT_BUILD" == "1" ]]; then
|
||||||
|
info "强制构建模式"
|
||||||
|
MODE="force"
|
||||||
|
elif [[ -z "$prev_md5" ]]; then
|
||||||
|
info "首次部署,执行完整构建"
|
||||||
|
MODE="init"
|
||||||
|
elif [[ "$prev_md5" == "$local_md5" ]]; then
|
||||||
|
info "源码无变化,跳过构建,仅重启容器"
|
||||||
|
MODE="skip"
|
||||||
|
else
|
||||||
|
info "检测到源码变化,执行增量构建"
|
||||||
|
MODE="update"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ONLY_RESTART" == "1" ]]; then
|
||||||
|
MODE="restart"
|
||||||
|
info "强制重建容器,跳过构建"
|
||||||
|
ssh txjp "cd $REMOTE_DIR && docker compose down && docker compose up -d"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$MODE" == "skip" || "$MODE" == "restart" ]]; then
|
||||||
|
ssh txjp "cd $REMOTE_DIR && docker compose down && docker compose up -d"
|
||||||
|
log "容器已重建"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. 打包源码(不含 node_modules)
|
||||||
|
log "打包源码..."
|
||||||
|
pack_start=$(date +%s)
|
||||||
|
COPYFILE_DISABLE=1 tar czf /tmp/${SITE}.tar.gz -C "$LOCAL_DIR" \
|
||||||
|
--exclude='node_modules' --exclude='.next' --exclude='out' \
|
||||||
|
--exclude='data' --exclude='./reports/*.docx' --exclude='docs' \
|
||||||
|
--exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \
|
||||||
|
--exclude='.git' --exclude='._*' . || {
|
||||||
|
error "打包失败"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
pack_end=$(date +%s)
|
||||||
|
pack_dur=$((pack_end - pack_start))
|
||||||
|
local pkg_size
|
||||||
|
pkg_size=$(du -h /tmp/${SITE}.tar.gz 2>/dev/null | cut -f1 || echo "未知")
|
||||||
|
timing "打包耗时: ${pack_dur}s (${pkg_size})"
|
||||||
|
|
||||||
|
# 3. 上传
|
||||||
|
log "上传源码包..."
|
||||||
|
scp /tmp/${SITE}.tar.gz txjp:/tmp/${SITE}.tar.gz || {
|
||||||
|
error "上传失败"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. 解压源码到 /tmp(独立于目标目录,避免 rsync --delete 误删源文件)
|
||||||
|
log "解压源码并准备依赖..."
|
||||||
|
ssh txjp "\
|
||||||
|
rm -rf /tmp/deploy_${SITE} && mkdir -p /tmp/deploy_${SITE} && \
|
||||||
|
tar xzf /tmp/${SITE}.tar.gz -C /tmp/deploy_${SITE} 2>/dev/null || true && \
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude='node_modules' --exclude='.next' --exclude='data' --exclude='/reports/*.docx' --exclude='docs' \
|
||||||
|
--exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \
|
||||||
|
--exclude='._*' \
|
||||||
|
/tmp/deploy_${SITE}/ ${REMOTE_DIR}/ && \
|
||||||
|
rm -rf /tmp/deploy_${SITE}"
|
||||||
|
|
||||||
|
# 首次:上传 docker-compose.yml(新增 .next 卷挂载),重启容器应用
|
||||||
|
ssh txjp "cd $REMOTE_DIR && docker compose up -d"
|
||||||
|
|
||||||
|
# 如果 host 上没有 node_modules,从容器内复制一份
|
||||||
|
ssh txjp "if [ ! -d '${REMOTE_DIR}/node_modules' ]; then
|
||||||
|
echo ' 首次:复制容器内 node_modules 到主机...'
|
||||||
|
docker cp ${CONTAINER}:/app/node_modules ${REMOTE_DIR}/node_modules
|
||||||
|
echo ' 完成'
|
||||||
|
fi"
|
||||||
|
|
||||||
|
# 安装可能新增的依赖(已有依赖走缓存,很快)
|
||||||
|
log "安装新增依赖..."
|
||||||
|
ssh txjp "cd ${REMOTE_DIR} && npm install --prefer-offline 2>&1 | tail -5 || true"
|
||||||
|
|
||||||
|
if [[ -n "$extra_env" ]]; then
|
||||||
|
ssh txjp "sed -i 's|ASSETS_API_URL=.*|ASSETS_API_URL=${extra_env}|' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
|| echo 'ASSETS_API_URL=${extra_env}' >> $REMOTE_DIR/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# assets 站点需要 NEXT_PUBLIC_ISSUE_URL(构建时内嵌到 JS bundle)和 ISSUE_API_URL(运行时,容器内网地址)
|
||||||
|
if [[ "$SITE" == "assets" ]]; then
|
||||||
|
ssh txjp "grep -q 'NEXT_PUBLIC_ISSUE_URL' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
&& sed -i 's|NEXT_PUBLIC_ISSUE_URL=.*|NEXT_PUBLIC_ISSUE_URL=https://issue.tlyq.ai/tickets|' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
|| echo 'NEXT_PUBLIC_ISSUE_URL=https://issue.tlyq.ai/tickets' >> $REMOTE_DIR/.env"
|
||||||
|
ssh txjp "grep -q 'ISSUE_API_URL' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
&& sed -i 's|ISSUE_API_URL=.*|ISSUE_API_URL=http://issue-ai:3000/api|' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
|| echo 'ISSUE_API_URL=http://issue-ai:3000/api' >> $REMOTE_DIR/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# issue 站点需要 NEXT_PUBLIC_ASSETS_URL(构建时内嵌到 JS bundle)
|
||||||
|
if [[ "$SITE" == "issue" ]]; then
|
||||||
|
ssh txjp "grep -q 'NEXT_PUBLIC_ASSETS_URL' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
&& sed -i 's|NEXT_PUBLIC_ASSETS_URL=.*|NEXT_PUBLIC_ASSETS_URL=https://assets.tlyq.ai|' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
|| echo 'NEXT_PUBLIC_ASSETS_URL=https://assets.tlyq.ai' >> $REMOTE_DIR/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 确保 AUTHELIA_URL 已设置(退出登录 cookie domain 依赖此变量)
|
||||||
|
ssh txjp "grep -q 'AUTHELIA_URL' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
&& sed -i 's|AUTHELIA_URL=.*|AUTHELIA_URL=https://sso.tlyq.ai|' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
|| echo 'AUTHELIA_URL=https://sso.tlyq.ai' >> $REMOTE_DIR/.env"
|
||||||
|
# 清理可能被上传的本地环境变量文件(.env.local 等会覆盖 .env 中的生产配置)
|
||||||
|
ssh txjp "rm -f $REMOTE_DIR/.env.local $REMOTE_DIR/.env.development $REMOTE_DIR/.env.production 2>/dev/null; echo '已清理本地环境文件'"
|
||||||
|
|
||||||
|
# 5. 服务器上直接 npm run build(不重建 Docker)
|
||||||
|
log "服务器上执行 npm run build..."
|
||||||
|
build_start=$(date +%s)
|
||||||
|
# 使用 tee 避免管道导致退出码丢失,并过滤无用输出
|
||||||
|
ssh txjp "cd ${REMOTE_DIR} && npm run build 2>&1" | \
|
||||||
|
grep -vE "^(info|warn|npm warn|audited|packages|funding|vulnerability|npm notice|New major)" | \
|
||||||
|
tee /tmp/build_output.txt | \
|
||||||
|
tail -15
|
||||||
|
build_exit_code=${PIPESTATUS[0]}
|
||||||
|
build_end=$(date +%s)
|
||||||
|
build_dur=$((build_end - build_start))
|
||||||
|
timing "构建耗时: ${build_dur}s"
|
||||||
|
|
||||||
|
if [[ "$build_exit_code" -ne 0 ]]; then
|
||||||
|
warn "构建可能有警告,请检查上面的输出"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 清理 standalone 中的 musl 原生模块(Alpine builder 残留,Debian runner 不兼容)
|
||||||
|
if [[ "$SITE" == "issue" || "$SITE" == "assets" || "$SITE" == "oa" ]]; then
|
||||||
|
ssh txjp "rm -rf $REMOTE_DIR/.next/standalone/node_modules/@img/sharp-linuxmusl-x64 \
|
||||||
|
$REMOTE_DIR/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64 \
|
||||||
|
$REMOTE_DIR/.next/standalone/node_modules/@next/swc-linux-x64-musl 2>/dev/null; echo '已清理 musl 残留'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. 重建容器(应用 .next 卷挂载;若存在 Dockerfile 则 --build 确保原生模块兼容;
|
||||||
|
# 无论有无 Dockerfile,restart 确保 Next.js 进程重新加载 .next)
|
||||||
|
log "重建容器..."
|
||||||
|
local build_flag=""
|
||||||
|
ssh txjp "test -f $REMOTE_DIR/Dockerfile" 2>/dev/null && build_flag="--build"
|
||||||
|
ssh txjp "cd $REMOTE_DIR && docker compose up -d $build_flag && docker compose restart"
|
||||||
|
|
||||||
|
# 7. 保存快照
|
||||||
|
ssh txjp "echo '$local_md5' > ${SNAPSHOT_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 执行部署
|
||||||
|
# ============================================================
|
||||||
|
echo ""
|
||||||
|
case "$SITE" in
|
||||||
|
www)
|
||||||
|
build_on_server ;;
|
||||||
|
cloud)
|
||||||
|
info "上传静态文件..."
|
||||||
|
scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html 2>/dev/null || true
|
||||||
|
ssh txjp "docker restart ${SITE}-ai" ;;
|
||||||
|
token)
|
||||||
|
info "上传静态文件..."
|
||||||
|
scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html 2>/dev/null || true
|
||||||
|
ssh txjp "docker restart ${SITE}-ai" ;;
|
||||||
|
issue)
|
||||||
|
build_on_server "http://assets-ai:3000/api" || exit 1
|
||||||
|
info "部署 nginx 路由..."
|
||||||
|
ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/issue-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true
|
||||||
|
docker exec nginx-ai nginx -s reload 2>/dev/null || true"
|
||||||
|
|
||||||
|
# 验证 issue→assets API 连通性(确保 API Key 有效、网络可达)
|
||||||
|
info "检查 issue→assets API 连通性..."
|
||||||
|
ssh txjp 'cat > /tmp/check-conn.js' << 'CHECKEOF'
|
||||||
|
const http = require("http");
|
||||||
|
const url = process.env.ASSETS_API_URL + "/assets?pageSize=1";
|
||||||
|
const key = process.env.ASSETS_API_KEY || "";
|
||||||
|
http.get(url, {headers:{"Authorization":"Bearer "+key}}, (res) => {
|
||||||
|
if (res.statusCode === 200) { console.log("连通正常:", res.statusCode); process.exit(0); }
|
||||||
|
console.error("连通失败: HTTP", res.statusCode, "(请检查 ASSETS_API_KEY 是否在 assets-ai 的 API Keys 中注册)");
|
||||||
|
process.exit(1);
|
||||||
|
}).on("error", (e) => { console.error("连通失败:", e.message); process.exit(1); });
|
||||||
|
CHECKEOF
|
||||||
|
CONN_CHECK=$(ssh txjp "docker cp /tmp/check-conn.js issue-ai:/tmp/check-conn.js && docker exec issue-ai sh -c 'cd /app && NODE_PATH=/app/node_modules node /tmp/check-conn.js'" 2>&1)
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error "issue→assets API 连通性检查失败!请检查 ASSETS_API_KEY 是否在 assets-ai 中注册"
|
||||||
|
echo " $CONN_CHECK"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "issue→assets API 连通性正常" ;;
|
||||||
|
assets)
|
||||||
|
build_on_server || exit 1
|
||||||
|
info "部署 nginx 路由..."
|
||||||
|
ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/assets-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true
|
||||||
|
docker exec nginx-ai nginx -s reload 2>/dev/null || true" ;;
|
||||||
|
oa)
|
||||||
|
build_on_server || exit 1
|
||||||
|
info "部署 nginx 配置..."
|
||||||
|
ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/oa-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true
|
||||||
|
docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 验证
|
||||||
|
# ============================================================
|
||||||
|
echo ""
|
||||||
|
log "验证部署..."
|
||||||
|
BUILD_END=$(date +%s)
|
||||||
|
TOTAL_DUR=$((BUILD_END - BUILD_START))
|
||||||
|
|
||||||
|
URL=""
|
||||||
|
case "$SITE" in
|
||||||
|
www) URL="https://www.tlyq.ai" ;;
|
||||||
|
cloud) URL="https://cloud.tlyq.ai" ;;
|
||||||
|
token) URL="https://token.tlyq.ai" ;;
|
||||||
|
issue) URL="https://issue.tlyq.ai" ;;
|
||||||
|
assets) URL="https://assets.tlyq.ai" ;;
|
||||||
|
oa) URL="https://oa.tlyq.ai" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
STATUS=$(ssh txjp "curl -s -o /dev/null -w '%{http_code}' -k '$URL' 2>/dev/null" 2>/dev/null || echo "???")
|
||||||
|
if [[ "$STATUS" == "200" || "$STATUS" == "307" || "$STATUS" == "301" ]]; then
|
||||||
|
log "部署成功!总耗时: ${TOTAL_DUR}s | 访问 $URL"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
warn "返回状态码: $STATUS,请检查"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# 本地修改 → 上传服务器 → 构建 → 部署
|
||||||
|
# 兼容 macOS (Bash 3.2+) / Ubuntu / Rocky Linux
|
||||||
|
# 用法:bash deploy-cc.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ANSI 颜色(兼容所有平台的 printf)
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; }
|
||||||
|
warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; }
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 选择要部署的站点
|
||||||
|
# ============================================================
|
||||||
|
echo ""
|
||||||
|
printf "${CYAN}=========================================${NC}\n"
|
||||||
|
printf "${CYAN} 选择要部署的站点${NC}\n"
|
||||||
|
printf "${CYAN}=========================================${NC}\n"
|
||||||
|
echo ""
|
||||||
|
echo " 1) www.tlyq.cc (图灵引擎官网)"
|
||||||
|
echo " 2) cloud.tlyq.cc (图灵智算系统云平台)"
|
||||||
|
echo " 3) token.tlyq.cc (Token工厂)"
|
||||||
|
echo ""
|
||||||
|
printf "请输入编号 (1/2/3): "
|
||||||
|
read choice
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
1)
|
||||||
|
SITE="www"
|
||||||
|
LOCAL_DIR="/Users/niuniu/programs/docker/www-cc/src"
|
||||||
|
REMOTE_DIR="/tmp/project-extract-2/turing-engine"
|
||||||
|
DEPLOY_DIR="/root/docker/www-cc/html"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
SITE="cloud"
|
||||||
|
LOCAL_DIR="/Users/niuniu/programs/docker/cloud-cc/html"
|
||||||
|
REMOTE_DIR="/root/docker/cloud-cc/html"
|
||||||
|
DEPLOY_DIR="/root/docker/cloud-cc/html"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
SITE="token"
|
||||||
|
LOCAL_DIR="/Users/niuniu/programs/docker/token-cc/html"
|
||||||
|
REMOTE_DIR="/root/docker/token-cc/html"
|
||||||
|
DEPLOY_DIR="/root/docker/token-cc/html"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "无效选择,已取消"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "部署目标: $LOCAL_DIR → 服务器 $DEPLOY_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 部署前:将 localhost 链接替换为正式域名
|
||||||
|
# ============================================================
|
||||||
|
restore_files() {
|
||||||
|
# 恢复本地文件中的 localhost 链接(deploy 后 / 异常退出时)
|
||||||
|
case "$SITE" in
|
||||||
|
www)
|
||||||
|
cd "$LOCAL_DIR"
|
||||||
|
find src -type f -name '*.tsx' -exec sed -i '' \
|
||||||
|
-e 's|http://localhost:5174/login|https://cloud.tlyq.cc/login|g' \
|
||||||
|
-e 's|http://localhost:5175/login|https://token.tlyq.cc/login|g' \
|
||||||
|
{} +
|
||||||
|
;;
|
||||||
|
cloud)
|
||||||
|
sed -i '' 's|http://localhost:5173|https://www.tlyq.cc|g' "$LOCAL_DIR/index.html"
|
||||||
|
;;
|
||||||
|
token)
|
||||||
|
sed -i '' 's|http://localhost:5173|https://www.tlyq.cc|g' "$LOCAL_DIR/index.html"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
to_production() {
|
||||||
|
log "将 localhost 链接替换为正式域名..."
|
||||||
|
case "$SITE" in
|
||||||
|
www)
|
||||||
|
cd "$LOCAL_DIR"
|
||||||
|
find src -type f -name '*.tsx' -exec sed -i '' \
|
||||||
|
-e 's|http://localhost:5174/login|https://cloud.tlyq.cc/login|g' \
|
||||||
|
-e 's|http://localhost:5175/login|https://token.tlyq.cc/login|g' \
|
||||||
|
{} +
|
||||||
|
;;
|
||||||
|
cloud)
|
||||||
|
sed -i '' 's|http://localhost:5173|https://www.tlyq.cc|g' "$LOCAL_DIR/index.html"
|
||||||
|
;;
|
||||||
|
token)
|
||||||
|
sed -i '' 's|http://localhost:5173|https://www.tlyq.cc|g' "$LOCAL_DIR/index.html"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
log "域名替换完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
to_localhost() {
|
||||||
|
log "恢复 localhost 链接..."
|
||||||
|
case "$SITE" in
|
||||||
|
www)
|
||||||
|
cd "$LOCAL_DIR"
|
||||||
|
find src -type f -name '*.tsx' -exec sed -i '' \
|
||||||
|
-e 's|https://cloud.tlyq.cc/login|http://localhost:5174/login|g' \
|
||||||
|
-e 's|https://token.tlyq.cc/login|http://localhost:5175/login|g' \
|
||||||
|
{} +
|
||||||
|
;;
|
||||||
|
cloud)
|
||||||
|
sed -i '' 's|https://www.tlyq.cc|http://localhost:5173|g' "$LOCAL_DIR/index.html"
|
||||||
|
;;
|
||||||
|
token)
|
||||||
|
sed -i '' 's|https://www.tlyq.cc|http://localhost:5173|g' "$LOCAL_DIR/index.html"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
log "已恢复为 localhost 链接"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 确保无论成功或失败都恢复本地文件
|
||||||
|
trap 'to_localhost' EXIT
|
||||||
|
|
||||||
|
to_production
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# www 站点需要构建,cloud/token 是静态文件直接上传
|
||||||
|
# ============================================================
|
||||||
|
if [ "$SITE" = "www" ]; then
|
||||||
|
# 打包源码
|
||||||
|
log "打包源码..."
|
||||||
|
cd "$LOCAL_DIR"
|
||||||
|
COPYFILE_DISABLE=1 tar czf /tmp/www-src.tar.gz --exclude='node_modules' --exclude='.next' --exclude='out' .
|
||||||
|
|
||||||
|
# 上传
|
||||||
|
log "上传到服务器..."
|
||||||
|
scp /tmp/www-src.tar.gz tgz:/tmp/www-src.tar.gz
|
||||||
|
|
||||||
|
# 服务器上构建并部署
|
||||||
|
log "服务器上构建并部署..."
|
||||||
|
ssh tgz "cd $REMOTE_DIR && rm -rf .next out src && tar xzf /tmp/www-src.tar.gz && npm install && rm -rf .next out && npm run build && rm -rf ${DEPLOY_DIR:?}/* && cp -r out/* $DEPLOY_DIR/ && echo 'deployed'"
|
||||||
|
else
|
||||||
|
# cloud/token 直接上传静态文件
|
||||||
|
log "上传静态文件..."
|
||||||
|
scp "$LOCAL_DIR"/index.html tgz:$DEPLOY_DIR/index.html
|
||||||
|
scp "$LOCAL_DIR"/nginx.conf tgz:$DEPLOY_DIR/nginx.conf
|
||||||
|
|
||||||
|
# 重启容器
|
||||||
|
log "重启容器..."
|
||||||
|
ssh tgz "docker restart ${SITE}-cc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 验证
|
||||||
|
echo ""
|
||||||
|
log "验证部署..."
|
||||||
|
case "$SITE" in
|
||||||
|
www) URL="https://www.tlyq.cc" ;;
|
||||||
|
cloud) URL="https://cloud.tlyq.cc" ;;
|
||||||
|
token) URL="https://token.tlyq.cc" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
STATUS=$(ssh tgz "curl -s -o /dev/null -w '%{http_code}' -k '$URL'")
|
||||||
|
if [ "$STATUS" = "200" ]; then
|
||||||
|
log "部署成功!访问 $URL"
|
||||||
|
else
|
||||||
|
warn "返回状态码: $STATUS,请检查"
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,448 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# www.tlyq.cc 内容管理脚本
|
||||||
|
# 兼容 macOS (Bash 3.2+) / Ubuntu / Rocky Linux
|
||||||
|
# 用法:bash edit-site-cc.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 配置
|
||||||
|
# ============================================================
|
||||||
|
SERVER="tgz"
|
||||||
|
SRC_DIR="/tmp/project-extract-2/turing-engine"
|
||||||
|
TRANS="src/lib/translations.ts"
|
||||||
|
FOOTER="src/components/Footer.tsx"
|
||||||
|
CONTACT="src/app/contact/page.tsx"
|
||||||
|
NEWS="src/app/news/page.tsx"
|
||||||
|
|
||||||
|
# ANSI 颜色
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; }
|
||||||
|
warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; }
|
||||||
|
err() { printf "${RED}[✗]${NC} %s\n" "$1"; }
|
||||||
|
info() { printf "${CYAN}[i]${NC} %s\n" "$1"; }
|
||||||
|
|
||||||
|
# 在服务器上执行命令
|
||||||
|
rsh() { ssh "$SERVER" "$@"; }
|
||||||
|
|
||||||
|
# 获取 translations.ts 中指定 marker 的行号
|
||||||
|
# 用法: get_line "marker" [nth] nth=1 返回第1个匹配,nth=2 返回第2个
|
||||||
|
get_line() {
|
||||||
|
local marker="$1"
|
||||||
|
local nth="${2:-1}"
|
||||||
|
rsh "grep -n \"$marker\" $SRC_DIR/$TRANS | sed -n '${nth}p' | cut -d: -f1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 在服务器文件中批量插入多行
|
||||||
|
# 用法: insert_lines <line1> <content1> <line2> <content2> ...
|
||||||
|
# 行号从大到小排序后依次插入,保证行号准确性
|
||||||
|
insert_lines() {
|
||||||
|
local args=("$@")
|
||||||
|
local pairs=""
|
||||||
|
local i=0
|
||||||
|
while [ $i -lt $# ]; do
|
||||||
|
local ln="${args[$i]}"
|
||||||
|
local ct="${args[$((i+1))]}"
|
||||||
|
# 构造 python 元组列表: (line_num, content)
|
||||||
|
if [ -n "$pairs" ]; then
|
||||||
|
pairs="$pairs,"
|
||||||
|
fi
|
||||||
|
# 转义内容中的反斜杠和引号
|
||||||
|
pairs="$pairs($ln, \"$(printf '%s' "$ct" | sed 's/\\/\\\\/g; s/"/\\"/g')\")"
|
||||||
|
i=$((i+2))
|
||||||
|
done
|
||||||
|
ssh "$SERVER" python3 - <<PYEOF
|
||||||
|
pairs = [$pairs]
|
||||||
|
with open('$SRC_DIR/$TRANS') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
# 按行号从大到小排序,避免插入后行号偏移
|
||||||
|
pairs.sort(reverse=True)
|
||||||
|
for line_num, content in pairs:
|
||||||
|
lines.insert(line_num, content + '\n')
|
||||||
|
with open('$SRC_DIR/$TRANS', 'w') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
PYEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 主菜单
|
||||||
|
# ============================================================
|
||||||
|
show_menu() {
|
||||||
|
echo ""
|
||||||
|
printf "${CYAN}=========================================${NC}\n"
|
||||||
|
printf "${CYAN} www.tlyq.cc 内容管理${NC}\n"
|
||||||
|
printf "${CYAN}=========================================${NC}\n"
|
||||||
|
echo ""
|
||||||
|
echo " 1) 修改联系方式(邮箱/电话/地址)"
|
||||||
|
echo " 2) 添加企业动态(新闻文章)"
|
||||||
|
echo " 3) 管理发展历程(时间线)"
|
||||||
|
echo " 4) 部署到服务器"
|
||||||
|
echo " 0) 退出"
|
||||||
|
echo ""
|
||||||
|
printf "请输入编号 (0-4): "
|
||||||
|
read choice
|
||||||
|
echo ""
|
||||||
|
case "$choice" in
|
||||||
|
1) edit_contact_info ;;
|
||||||
|
2) add_news ;;
|
||||||
|
3) manage_milestones ;;
|
||||||
|
4) deploy ;;
|
||||||
|
0) exit 0 ;;
|
||||||
|
*) warn "无效选择"; show_menu ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 功能 1:修改联系方式
|
||||||
|
# ============================================================
|
||||||
|
edit_contact_info() {
|
||||||
|
printf "${CYAN}--- 修改联系方式 ---${NC}\n"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 获取当前值
|
||||||
|
CUR_EMAIL=$(rsh "grep -m1 'mailto:' $SRC_DIR/$FOOTER | sed 's/.*mailto:\\([^\\\"]*\\).*/\\1/'")
|
||||||
|
CUR_PHONE=$(rsh "grep -m1 'tel:' $SRC_DIR/$FOOTER | sed 's/.*tel:\\([^\\\"]*\\).*/\\1/'")
|
||||||
|
CUR_ADDR_ZH=$(rsh "grep 'addrText' $SRC_DIR/$TRANS | head -1 | sed \"s/.*addrText: '\\([^']*\\)'.*/\\1/\"")
|
||||||
|
CUR_ADDR_EN=$(rsh "grep 'addrText' $SRC_DIR/$TRANS | tail -1 | sed \"s/.*addrText: '\\([^']*\\)'.*/\\1/\"")
|
||||||
|
|
||||||
|
info "当前邮箱: $CUR_EMAIL"
|
||||||
|
info "当前电话: $CUR_PHONE"
|
||||||
|
info "当前地址(中文): $CUR_ADDR_ZH"
|
||||||
|
info "当前地址(英文): $CUR_ADDR_EN"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
printf "新邮箱 (回车跳过): "
|
||||||
|
read NEW_EMAIL
|
||||||
|
printf "新电话 (回车跳过): "
|
||||||
|
read NEW_PHONE
|
||||||
|
printf "新地址-中文 (回车跳过): "
|
||||||
|
read NEW_ADDR_ZH
|
||||||
|
printf "新地址-英文 (回车跳过): "
|
||||||
|
read NEW_ADDR_EN
|
||||||
|
|
||||||
|
if [ -n "$NEW_EMAIL" ]; then
|
||||||
|
info "替换邮箱: $CUR_EMAIL → $NEW_EMAIL"
|
||||||
|
rsh "sed -i 's/$CUR_EMAIL/$NEW_EMAIL/g' $SRC_DIR/$FOOTER $SRC_DIR/$CONTACT $SRC_DIR/$NEWS"
|
||||||
|
log "邮箱已更新(Footer、联系我们、企业动态 3个文件)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$NEW_PHONE" ]; then
|
||||||
|
info "替换电话: $CUR_PHONE → $NEW_PHONE"
|
||||||
|
rsh "sed -i 's/$CUR_PHONE/$NEW_PHONE/g' $SRC_DIR/$FOOTER $SRC_DIR/$CONTACT"
|
||||||
|
log "电话已更新(Footer、联系我们 2个文件)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$NEW_ADDR_ZH" ]; then
|
||||||
|
info "替换地址(中文): $CUR_ADDR_ZH → $NEW_ADDR_ZH"
|
||||||
|
rsh "sed -i \"s/addrText: '$CUR_ADDR_ZH'/addrText: '$NEW_ADDR_ZH'/\" $SRC_DIR/$TRANS"
|
||||||
|
log "中文地址已更新"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$NEW_ADDR_EN" ]; then
|
||||||
|
info "替换地址(英文): $CUR_ADDR_EN → $NEW_ADDR_EN"
|
||||||
|
rsh "sed -i \"s/addrText: '$CUR_ADDR_EN'/addrText: '$NEW_ADDR_EN'/\" $SRC_DIR/$TRANS"
|
||||||
|
log "英文地址已更新"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$NEW_EMAIL" ] && [ -z "$NEW_PHONE" ] && [ -z "$NEW_ADDR_ZH" ] && [ -z "$NEW_ADDR_EN" ]; then
|
||||||
|
warn "未修改任何内容"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ask_deploy
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 功能 2:添加企业动态
|
||||||
|
# ============================================================
|
||||||
|
add_news() {
|
||||||
|
printf "${CYAN}--- 添加企业动态 ---${NC}\n"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 选择分类
|
||||||
|
echo "选择分类:"
|
||||||
|
echo " 1) 公司新闻"
|
||||||
|
echo " 2) 行业资讯"
|
||||||
|
printf "请选择 (1/2): "
|
||||||
|
read cat_choice
|
||||||
|
case "$cat_choice" in
|
||||||
|
1) ZH_CAT="公司新闻"; EN_CAT="Company News" ;;
|
||||||
|
2) ZH_CAT="行业资讯"; EN_CAT="Industry News" ;;
|
||||||
|
*) err "无效选择"; return ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "${YELLOW}--- 中文信息 ---${NC}\n"
|
||||||
|
printf "日期 (如 2026年4月): "
|
||||||
|
read ZH_DATE
|
||||||
|
[ -z "$ZH_DATE" ] && { err "日期不能为空"; return; }
|
||||||
|
printf "标题: "
|
||||||
|
read ZH_TITLE
|
||||||
|
[ -z "$ZH_TITLE" ] && { err "标题不能为空"; return; }
|
||||||
|
printf "摘要: "
|
||||||
|
read ZH_DESC
|
||||||
|
[ -z "$ZH_DESC" ] && { err "摘要不能为空"; return; }
|
||||||
|
printf "标签 (逗号分隔, 如 AI,产品): "
|
||||||
|
read ZH_TAGS_INPUT
|
||||||
|
[ -z "$ZH_TAGS_INPUT" ] && { err "标签不能为空"; return; }
|
||||||
|
|
||||||
|
# 构造 tags 数组字符串
|
||||||
|
ZH_TAGS=$(printf '%s' "$ZH_TAGS_INPUT" | sed "s/ *, */', '/g" | sed "s/^/['/" | sed "s/$/']/")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "${YELLOW}--- English Info ---${NC}\n"
|
||||||
|
printf "Date (e.g. April 2026): "
|
||||||
|
read EN_DATE
|
||||||
|
[ -z "$EN_DATE" ] && { err "Date is required"; return; }
|
||||||
|
printf "Title: "
|
||||||
|
read EN_TITLE
|
||||||
|
[ -z "$EN_TITLE" ] && { err "Title is required"; return; }
|
||||||
|
printf "Description: "
|
||||||
|
read EN_DESC
|
||||||
|
[ -z "$EN_DESC" ] && { err "Description is required"; return; }
|
||||||
|
printf "Tags (comma separated, e.g. AI,Product): "
|
||||||
|
read EN_TAGS_INPUT
|
||||||
|
[ -z "$EN_TAGS_INPUT" ] && { err "Tags are required"; return; }
|
||||||
|
|
||||||
|
EN_TAGS=$(printf '%s' "$EN_TAGS_INPUT" | sed "s/ *, */', '/g" | sed "s/^/['/" | sed "s/$/']/")
|
||||||
|
|
||||||
|
# 预览
|
||||||
|
echo ""
|
||||||
|
printf "${CYAN}--- 确认信息 ---${NC}\n"
|
||||||
|
echo "分类: $ZH_CAT / $EN_CAT"
|
||||||
|
echo "日期: $ZH_DATE / $EN_DATE"
|
||||||
|
echo "标题: $ZH_TITLE"
|
||||||
|
echo " $EN_TITLE"
|
||||||
|
echo "摘要: $ZH_DESC"
|
||||||
|
echo " $EN_DESC"
|
||||||
|
echo "标签: $ZH_TAGS"
|
||||||
|
echo " $EN_TAGS"
|
||||||
|
echo ""
|
||||||
|
printf "确认添加? (y/n): "
|
||||||
|
read confirm
|
||||||
|
[ "$confirm" != "y" ] && { warn "已取消"; return; }
|
||||||
|
|
||||||
|
# 获取 zh 和 en 的 articles: [ 行号
|
||||||
|
ZH_ART_LINE=$(get_line "articles: \[" 1)
|
||||||
|
EN_ART_LINE=$(get_line "articles: \[" 2)
|
||||||
|
|
||||||
|
# 一次性插入中英文(按行号从大到小排序,保证准确性)
|
||||||
|
insert_lines \
|
||||||
|
"$ZH_ART_LINE" " { category: '$ZH_CAT', date: '$ZH_DATE', title: '$ZH_TITLE', desc: '$ZH_DESC', tags: $ZH_TAGS }," \
|
||||||
|
"$EN_ART_LINE" " { category: '$EN_CAT', date: '$EN_DATE', title: '$EN_TITLE', desc: '$EN_DESC', tags: $EN_TAGS },"
|
||||||
|
|
||||||
|
log "企业动态已添加(中英文)"
|
||||||
|
ask_deploy
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 功能 3:管理发展历程
|
||||||
|
# ============================================================
|
||||||
|
manage_milestones() {
|
||||||
|
printf "${CYAN}--- 管理发展历程 ---${NC}\n"
|
||||||
|
echo ""
|
||||||
|
echo " 1) 添加新条目"
|
||||||
|
echo " 2) 修改现有条目"
|
||||||
|
echo " 0) 返回"
|
||||||
|
echo ""
|
||||||
|
printf "请选择 (0-2): "
|
||||||
|
read ms_choice
|
||||||
|
case "$ms_choice" in
|
||||||
|
1) add_milestone ;;
|
||||||
|
2) edit_milestone ;;
|
||||||
|
0) show_menu ;;
|
||||||
|
*) warn "无效选择"; manage_milestones ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
add_milestone() {
|
||||||
|
echo ""
|
||||||
|
printf "${YELLOW}--- 中文信息 ---${NC}\n"
|
||||||
|
printf "日期 (如 2026年4月): "
|
||||||
|
read ZH_DATE
|
||||||
|
[ -z "$ZH_DATE" ] && { err "日期不能为空"; return; }
|
||||||
|
printf "标题: "
|
||||||
|
read ZH_TITLE
|
||||||
|
[ -z "$ZH_TITLE" ] && { err "标题不能为空"; return; }
|
||||||
|
printf "描述: "
|
||||||
|
read ZH_DESC
|
||||||
|
[ -z "$ZH_DESC" ] && { err "描述不能为空"; return; }
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "${YELLOW}--- English Info ---${NC}\n"
|
||||||
|
printf "Date (e.g. Apr 2026): "
|
||||||
|
read EN_DATE
|
||||||
|
[ -z "$EN_DATE" ] && { err "Date is required"; return; }
|
||||||
|
printf "Title: "
|
||||||
|
read EN_TITLE
|
||||||
|
[ -z "$EN_TITLE" ] && { err "Title is required"; return; }
|
||||||
|
printf "Description: "
|
||||||
|
read EN_DESC
|
||||||
|
[ -z "$EN_DESC" ] && { err "Description is required"; return; }
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "确认添加? (y/n): "
|
||||||
|
read confirm
|
||||||
|
[ "$confirm" != "y" ] && { warn "已取消"; return; }
|
||||||
|
|
||||||
|
# 获取 zh 和 en 的 milestones: [ 行号
|
||||||
|
ZH_MS_LINE=$(get_line "milestones: \[" 1)
|
||||||
|
EN_MS_LINE=$(get_line "milestones: \[" 2)
|
||||||
|
|
||||||
|
# 一次性插入中英文
|
||||||
|
insert_lines \
|
||||||
|
"$ZH_MS_LINE" " { date: '$ZH_DATE', title: '$ZH_TITLE', desc: '$ZH_DESC' }," \
|
||||||
|
"$EN_MS_LINE" " { date: '$EN_DATE', title: '$EN_TITLE', desc: '$EN_DESC' },"
|
||||||
|
|
||||||
|
log "发展历程已添加(中英文)"
|
||||||
|
ask_deploy
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_milestone() {
|
||||||
|
echo ""
|
||||||
|
info "当前发展历程 (中文):"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 获取 zh milestones 的起始行
|
||||||
|
ZH_MS_LINE=$(get_line "milestones: \[" 1)
|
||||||
|
EN_MS_LINE=$(get_line "milestones: \[" 2)
|
||||||
|
|
||||||
|
# 列出所有中文里程碑条目
|
||||||
|
rsh "awk 'NR>${ZH_MS_LINE} && /date:.*title:/{printf \" %d: %s\\n\", NR, \$0}' $SRC_DIR/$TRANS"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "请输入要修改的条目行号: "
|
||||||
|
read LINE_NUM
|
||||||
|
[ -z "$LINE_NUM" ] && { err "行号不能为空"; return; }
|
||||||
|
|
||||||
|
# 验证行号是否有效
|
||||||
|
VALID=$(rsh "sed -n '${LINE_NUM}p' $SRC_DIR/$TRANS | grep -c 'date:.*title:' || true")
|
||||||
|
if [ "$VALID" != "1" ]; then
|
||||||
|
err "该行不是有效的历程条目"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 计算对应的英文行号
|
||||||
|
OFFSET=$((LINE_NUM - ZH_MS_LINE))
|
||||||
|
EN_LINE_NUM=$((EN_MS_LINE + OFFSET))
|
||||||
|
|
||||||
|
# 显示当前值
|
||||||
|
info "当前中文内容:"
|
||||||
|
rsh "sed -n '${LINE_NUM}p' $SRC_DIR/$TRANS"
|
||||||
|
info "当前英文内容:"
|
||||||
|
rsh "sed -n '${EN_LINE_NUM}p' $SRC_DIR/$TRANS"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "${YELLOW}--- 新的中文信息 ---${NC}\n"
|
||||||
|
printf "日期: "
|
||||||
|
read ZH_DATE
|
||||||
|
[ -z "$ZH_DATE" ] && { err "日期不能为空"; return; }
|
||||||
|
printf "标题: "
|
||||||
|
read ZH_TITLE
|
||||||
|
[ -z "$ZH_TITLE" ] && { err "标题不能为空"; return; }
|
||||||
|
printf "描述: "
|
||||||
|
read ZH_DESC
|
||||||
|
[ -z "$ZH_DESC" ] && { err "描述不能为空"; return; }
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "${YELLOW}--- New English Info ---${NC}\n"
|
||||||
|
printf "Date: "
|
||||||
|
read EN_DATE
|
||||||
|
[ -z "$EN_DATE" ] && { err "Date is required"; return; }
|
||||||
|
printf "Title: "
|
||||||
|
read EN_TITLE
|
||||||
|
[ -z "$EN_TITLE" ] && { err "Title is required"; return; }
|
||||||
|
printf "Description: "
|
||||||
|
read EN_DESC
|
||||||
|
[ -z "$EN_DESC" ] && { err "Description is required"; return; }
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "确认修改? (y/n): "
|
||||||
|
read confirm
|
||||||
|
[ "$confirm" != "y" ] && { warn "已取消"; return; }
|
||||||
|
|
||||||
|
# 替换中文行
|
||||||
|
ZH_NEW=" { date: '$ZH_DATE', title: '$ZH_TITLE', desc: '$ZH_DESC' },"
|
||||||
|
# 转义替换内容中的特殊字符
|
||||||
|
ZH_NEW_ESC=$(printf '%s' "$ZH_NEW" | sed 's/[&/\\]/\\&/g')
|
||||||
|
rsh "sed -i '${LINE_NUM}s/.*/${ZH_NEW_ESC}/' $SRC_DIR/$TRANS"
|
||||||
|
|
||||||
|
# 替换英文行
|
||||||
|
EN_NEW=" { date: '$EN_DATE', title: '$EN_TITLE', desc: '$EN_DESC' },"
|
||||||
|
EN_NEW_ESC=$(printf '%s' "$EN_NEW" | sed 's/[&/\\]/\\&/g')
|
||||||
|
rsh "sed -i '${EN_LINE_NUM}s/.*/${EN_NEW_ESC}/' $SRC_DIR/$TRANS"
|
||||||
|
|
||||||
|
log "发展历程已更新(中英文)"
|
||||||
|
ask_deploy
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 功能 4:部署到服务器
|
||||||
|
# ============================================================
|
||||||
|
deploy() {
|
||||||
|
printf "${CYAN}--- 部署 www.tlyq.cc ---${NC}\n"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
LOCAL_DIR="/Users/niuniu/programs/docker/www-cc/src"
|
||||||
|
REMOTE_DIR="$SRC_DIR"
|
||||||
|
DEPLOY_DIR="/root/docker/www-cc/html"
|
||||||
|
|
||||||
|
# 从服务器同步最新源码到本地
|
||||||
|
info "从服务器同步最新源码..."
|
||||||
|
rsync -az --delete --exclude='node_modules' --exclude='.next' --exclude='out' \
|
||||||
|
"$SERVER:$REMOTE_DIR/" "$LOCAL_DIR/" 2>/dev/null || {
|
||||||
|
warn "rsync 不可用,使用 scp..."
|
||||||
|
rm -rf "$LOCAL_DIR"
|
||||||
|
mkdir -p "$LOCAL_DIR"
|
||||||
|
scp -r "$SERVER:$REMOTE_DIR/." "$LOCAL_DIR/" 2>/dev/null
|
||||||
|
rm -rf "$LOCAL_DIR/node_modules" "$LOCAL_DIR/.next" "$LOCAL_DIR/out"
|
||||||
|
}
|
||||||
|
|
||||||
|
log "打包源码..."
|
||||||
|
cd "$LOCAL_DIR"
|
||||||
|
COPYFILE_DISABLE=1 tar czf /tmp/www-src.tar.gz --exclude='node_modules' --exclude='.next' --exclude='out' .
|
||||||
|
|
||||||
|
log "上传到服务器..."
|
||||||
|
scp /tmp/www-src.tar.gz "$SERVER:/tmp/www-src.tar.gz"
|
||||||
|
|
||||||
|
log "服务器上构建并部署..."
|
||||||
|
ssh "$SERVER" "cd $REMOTE_DIR && rm -rf .next out src && tar xzf /tmp/www-src.tar.gz && npm install && rm -rf .next out && npm run build && rm -rf ${DEPLOY_DIR:?}/* && cp -r out/* $DEPLOY_DIR/"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "验证部署..."
|
||||||
|
URL="https://www.tlyq.cc"
|
||||||
|
STATUS=$(ssh "$SERVER" "curl -s -o /dev/null -w '%{http_code}' -k '$URL'")
|
||||||
|
if [ "$STATUS" = "200" ]; then
|
||||||
|
log "部署成功!访问 $URL"
|
||||||
|
else
|
||||||
|
warn "返回状态码: $STATUS,请检查"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
show_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 通用:询问是否部署
|
||||||
|
# ============================================================
|
||||||
|
ask_deploy() {
|
||||||
|
echo ""
|
||||||
|
printf "是否立即部署到服务器? (y/n): "
|
||||||
|
read d
|
||||||
|
if [ "$d" = "y" ]; then
|
||||||
|
deploy
|
||||||
|
else
|
||||||
|
warn "未部署,修改仅在服务器源码中生效"
|
||||||
|
info "稍后可选择菜单 4) 部署到服务器"
|
||||||
|
show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 启动
|
||||||
|
# ============================================================
|
||||||
|
show_menu
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# 本地站点管理:启动 / 停止 / 重启 / 状态
|
||||||
|
# 用法:bash local.sh [start|stop|restart|status] [站点名...]
|
||||||
|
# 不加参数则交互选择
|
||||||
|
#
|
||||||
|
# 站点名:
|
||||||
|
# cc: www-cc cloud-cc token-cc
|
||||||
|
# ai: www-ai cloud-ai token-ai issue assets ldap
|
||||||
|
# all: 全部站点
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BASE_DIR="/Users/niuniu/programs/docker"
|
||||||
|
ISSUE_DIR="$BASE_DIR/issue-ai"
|
||||||
|
ASSETS_DIR="$BASE_DIR/assets-ai"
|
||||||
|
LDAP_DIR="$BASE_DIR/ldap-ai"
|
||||||
|
OA_DIR="$BASE_DIR/oa-ai"
|
||||||
|
OA_PORT=6179
|
||||||
|
|
||||||
|
# 颜色
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; }
|
||||||
|
warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; }
|
||||||
|
err() { printf "${RED}[✗]${NC} %s\n" "$1"; }
|
||||||
|
|
||||||
|
# 端口
|
||||||
|
ISSUE_PORT=6176
|
||||||
|
ASSETS_PORT=6177
|
||||||
|
LDAP_WEB_PORT=6178
|
||||||
|
|
||||||
|
is_port() { lsof -i :"$1" >/dev/null 2>&1; }
|
||||||
|
is_running() { docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^$1$"; }
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 单个 Docker 站点操作
|
||||||
|
# ============================================================
|
||||||
|
docker_start() {
|
||||||
|
local name=$1 dir=$2
|
||||||
|
if is_running $name; then
|
||||||
|
warn "$name 已在运行"
|
||||||
|
else
|
||||||
|
log "启动 $name..."
|
||||||
|
cd "$dir" && docker compose up -d 2>/dev/null && log "$name 已启动" || err "$name 启动失败"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
docker_stop() {
|
||||||
|
local name=$1 dir=$2
|
||||||
|
if is_running $name; then
|
||||||
|
log "停止 $name..."
|
||||||
|
cd "$dir" && docker compose down 2>/dev/null && log "$name 已停止" || err "$name 停止失败"
|
||||||
|
else
|
||||||
|
warn "$name 未在运行"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 单个 Node.js 服务(npm run dev)
|
||||||
|
# ============================================================
|
||||||
|
node_start() {
|
||||||
|
local name=$1 dir=$2 port=$3 logfile=$4
|
||||||
|
if is_port $port; then
|
||||||
|
warn "$name 已在运行 (port $port)"
|
||||||
|
else
|
||||||
|
log "启动 $name (port $port)..."
|
||||||
|
cd "$dir" && npm run dev > "$logfile" 2>&1 &
|
||||||
|
sleep 2
|
||||||
|
is_port $port && log "$name 已启动" || err "$name 启动失败,请查看 $logfile"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
node_stop() {
|
||||||
|
local name=$1 port=$2
|
||||||
|
if is_port $port; then
|
||||||
|
kill $(lsof -ti :$port) 2>/dev/null && log "$name 已停止" || warn "$name 停止失败"
|
||||||
|
else
|
||||||
|
warn "$name 未在运行"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 批量操作
|
||||||
|
# ============================================================
|
||||||
|
start_all() {
|
||||||
|
for name in "$@"; do
|
||||||
|
case $name in
|
||||||
|
www-cc) docker_start www-local "$BASE_DIR/www-cc" ;;
|
||||||
|
cloud-cc) docker_start cloud-cc "$BASE_DIR/cloud-cc" ;;
|
||||||
|
token-cc) docker_start token-cc "$BASE_DIR/token-cc" ;;
|
||||||
|
www-ai) docker_start www-local-ai "$BASE_DIR/www-ai" ;;
|
||||||
|
cloud-ai) docker_start cloud-ai "$BASE_DIR/cloud-ai" ;;
|
||||||
|
token-ai) docker_start token-ai "$BASE_DIR/token-ai" ;;
|
||||||
|
issue) node_start issue-ai "$ISSUE_DIR" $ISSUE_PORT "/tmp/issue-ai-dev.log"
|
||||||
|
if ! is_port $ASSETS_PORT; then
|
||||||
|
node_start assets-ai "$ASSETS_DIR" $ASSETS_PORT "/tmp/assets-ai-dev.log"
|
||||||
|
warn "已自动启动 assets-ai(issue 依赖 assets API)"
|
||||||
|
fi ;;
|
||||||
|
assets) node_start assets-ai "$ASSETS_DIR" $ASSETS_PORT "/tmp/assets-ai-dev.log" ;;
|
||||||
|
ldap) docker_start lldap "$LDAP_DIR"
|
||||||
|
if is_port $LDAP_WEB_PORT; then
|
||||||
|
log "LLDAP Web UI: http://localhost:$LDAP_WEB_PORT"
|
||||||
|
fi ;;
|
||||||
|
oa) node_start oa-ai "$OA_DIR" $OA_PORT "/tmp/oa-ai-dev.log" ;;
|
||||||
|
cc) start_all www-cc cloud-cc token-cc ;;
|
||||||
|
ai) start_all ldap www-ai cloud-ai token-ai issue assets oa ;;
|
||||||
|
all) start_all cc ai ;;
|
||||||
|
*) err "未知站点: $name" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_all() {
|
||||||
|
for name in "$@"; do
|
||||||
|
case $name in
|
||||||
|
www-cc) docker_stop www-local "$BASE_DIR/www-cc" ;;
|
||||||
|
cloud-cc) docker_stop cloud-cc "$BASE_DIR/cloud-cc" ;;
|
||||||
|
token-cc) docker_stop token-cc "$BASE_DIR/token-cc" ;;
|
||||||
|
www-ai) docker_stop www-local-ai "$BASE_DIR/www-ai" ;;
|
||||||
|
cloud-ai) docker_stop cloud-ai "$BASE_DIR/cloud-ai" ;;
|
||||||
|
token-ai) docker_stop token-ai "$BASE_DIR/token-ai" ;;
|
||||||
|
issue) node_stop issue-ai $ISSUE_PORT ;;
|
||||||
|
assets) node_stop assets-ai $ASSETS_PORT ;;
|
||||||
|
ldap) docker_stop lldap "$LDAP_DIR" ;;
|
||||||
|
oa) node_stop oa-ai $OA_PORT ;;
|
||||||
|
cc) stop_all www-cc cloud-cc token-cc ;;
|
||||||
|
ai) stop_all ldap www-ai cloud-ai token-ai issue assets oa ;;
|
||||||
|
all) stop_all cc ai ;;
|
||||||
|
*) err "未知站点: $name" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_all() {
|
||||||
|
stop_all "$@"
|
||||||
|
echo ""
|
||||||
|
log "正在重启..."
|
||||||
|
start_all "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 状态
|
||||||
|
# ============================================================
|
||||||
|
show_status() {
|
||||||
|
local targets=("$@")
|
||||||
|
# 合并 all/cc/ai
|
||||||
|
local expanded=()
|
||||||
|
for t in "${targets[@]}"; do
|
||||||
|
case $t in
|
||||||
|
all) eval "expanded=(www-cc cloud-cc token-cc ldap www-ai cloud-ai token-ai issue assets oa)" ;;
|
||||||
|
cc) eval "expanded=(\${expanded[@]} www-cc cloud-cc token-cc)" ;;
|
||||||
|
ai) eval "expanded=(\${expanded[@]} ldap www-ai cloud-ai token-ai issue assets oa)" ;;
|
||||||
|
*) expanded+=($t) ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "${CYAN}=========================================${NC}\n"
|
||||||
|
printf "${CYAN} 当前状态${NC}\n"
|
||||||
|
printf "${CYAN}=========================================${NC}\n"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local running=0
|
||||||
|
for name in "${expanded[@]}"; do
|
||||||
|
case $name in
|
||||||
|
www-cc|cloud-cc|token-cc|cloud-ai|token-ai)
|
||||||
|
if is_running $name; then
|
||||||
|
printf " ${GREEN}●${NC} %-12s 运行中\n" "$name"
|
||||||
|
running=$((running + 1))
|
||||||
|
else
|
||||||
|
printf " ${RED}●${NC} %-12s 未运行\n" "$name"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
www-ai)
|
||||||
|
if is_running www-local-ai; then
|
||||||
|
printf " ${GREEN}●${NC} %-12s 运行中\n" "$name"
|
||||||
|
running=$((running + 1))
|
||||||
|
else
|
||||||
|
printf " ${RED}●${NC} %-12s 未运行\n" "$name"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
issue)
|
||||||
|
if is_port $ISSUE_PORT; then
|
||||||
|
printf " ${GREEN}●${NC} %-12s 运行中 (port %d)\n" "$name" $ISSUE_PORT
|
||||||
|
running=$((running + 1))
|
||||||
|
else
|
||||||
|
printf " ${RED}●${NC} %-12s 未运行\n" "$name"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
ldap)
|
||||||
|
if is_running lldap; then
|
||||||
|
printf " ${GREEN}●${NC} %-12s 运行中 (port %d)\n" "$name" $LDAP_WEB_PORT
|
||||||
|
running=$((running + 1))
|
||||||
|
else
|
||||||
|
printf " ${RED}●${NC} %-12s 未运行\n" "$name"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
assets)
|
||||||
|
if is_port $ASSETS_PORT; then
|
||||||
|
printf " ${GREEN}●${NC} %-12s 运行中 (port %d)\n" "$name" $ASSETS_PORT
|
||||||
|
running=$((running + 1))
|
||||||
|
else
|
||||||
|
printf " ${RED}●${NC} %-12s 未运行\n" "$name"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
oa)
|
||||||
|
if is_port $OA_PORT; then
|
||||||
|
printf " ${GREEN}●${NC} %-12s 运行中 (port %d)\n" "$name" $OA_PORT
|
||||||
|
running=$((running + 1))
|
||||||
|
else
|
||||||
|
printf " ${RED}●${NC} %-12s 未运行\n" "$name"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
printf "${CYAN}=========================================${NC}\n"
|
||||||
|
printf "${CYAN} 访问地址${NC}\n"
|
||||||
|
printf "${CYAN}=========================================${NC}\n"
|
||||||
|
echo ""
|
||||||
|
echo " cc 站点:"
|
||||||
|
echo " www-cc http://localhost:5173"
|
||||||
|
echo " cloud-cc http://localhost:5174"
|
||||||
|
echo " token-cc http://localhost:5175"
|
||||||
|
echo ""
|
||||||
|
echo " ai 站点:"
|
||||||
|
echo " www-ai http://localhost:6173"
|
||||||
|
echo " cloud-ai http://localhost:6174"
|
||||||
|
echo " token-ai http://localhost:6175"
|
||||||
|
echo " ldap http://localhost:$LDAP_WEB_PORT (LLDAP Web UI)"
|
||||||
|
echo " oa http://localhost:$OA_PORT"
|
||||||
|
echo " issue http://localhost:$ISSUE_PORT"
|
||||||
|
echo " assets http://localhost:$ASSETS_PORT"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 帮助
|
||||||
|
# ============================================================
|
||||||
|
show_help() {
|
||||||
|
echo ""
|
||||||
|
printf "${CYAN}用法:${NC} bash local.sh <命令> [站点名...]\n"
|
||||||
|
echo ""
|
||||||
|
echo " 命令:"
|
||||||
|
echo " start 启动站点"
|
||||||
|
echo " stop 停止站点"
|
||||||
|
echo " restart 重启站点"
|
||||||
|
echo " status 查看状态"
|
||||||
|
echo ""
|
||||||
|
echo " 站点名(可指定多个,空格分隔):"
|
||||||
|
echo " www-cc cloud-cc token-cc"
|
||||||
|
echo " ldap www-ai cloud-ai token-ai issue assets oa"
|
||||||
|
echo " cc — tlyq.cc 全部 (www-cc cloud-cc token-cc)"
|
||||||
|
echo " ai — tlyq.ai 全部 (ldap www-ai cloud-ai token-ai issue assets oa)"
|
||||||
|
echo " all — 全部站点"
|
||||||
|
echo ""
|
||||||
|
echo "示例:"
|
||||||
|
echo " bash local.sh start all # 启动全部"
|
||||||
|
echo " bash local.sh start ai # 启动 ai 全部"
|
||||||
|
echo " bash local.sh start issue assets # 只启 issue + assets"
|
||||||
|
echo " bash local.sh stop www-ai # 只停 www-ai"
|
||||||
|
echo " bash local.sh status # 查看所有状态"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 入口
|
||||||
|
# ============================================================
|
||||||
|
ACTION=${1:-}
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
if [ -z "$ACTION" ]; then
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 解析站点列表
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
case $ACTION in
|
||||||
|
start|stop|restart) TARGETS=(all) ;;
|
||||||
|
status) TARGETS=(all) ;;
|
||||||
|
help|--help|-h) show_help; exit 0 ;;
|
||||||
|
*) err "未知命令: $ACTION"; show_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
TARGETS=("$@")
|
||||||
|
fi
|
||||||
|
|
||||||
|
case $ACTION in
|
||||||
|
start) start_all "${TARGETS[@]}" ;;
|
||||||
|
stop) stop_all "${TARGETS[@]}" ;;
|
||||||
|
restart) restart_all "${TARGETS[@]}" ;;
|
||||||
|
status) show_status "${TARGETS[@]}" ;;
|
||||||
|
help|--help|-h) show_help ;;
|
||||||
|
*) err "未知命令: $ACTION"; show_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# ============================================
|
||||||
|
# 数据库恢复脚本
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# 恢复本地数据库: bash scripts/restore-db.sh local <服务名> <备份文件名>
|
||||||
|
# 恢复云端数据库: bash scripts/restore-db.sh cloud <服务名> <备份文件名>
|
||||||
|
#
|
||||||
|
# 服务名: issue / assets
|
||||||
|
#
|
||||||
|
# 示例:
|
||||||
|
# bash scripts/restore-db.sh local issue issue-2026-04-29_1753.db
|
||||||
|
# bash scripts/restore-db.sh cloud assets assets-2026-04-29_1753.db
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ---- 参数检查 ----
|
||||||
|
if [ $# -lt 3 ]; then
|
||||||
|
echo "用法: $0 <local|cloud> <issue|assets> <备份文件名>"
|
||||||
|
echo ""
|
||||||
|
echo "示例:"
|
||||||
|
echo " $0 local issue issue-2026-04-29_1753.db"
|
||||||
|
echo " $0 cloud assets assets-2026-04-29_1753.db"
|
||||||
|
echo ""
|
||||||
|
echo "可用备份文件:"
|
||||||
|
echo " 本地:"
|
||||||
|
ls -1 /Users/niuniu/programs/docker/db-backups/ 2>/dev/null || echo " (无)"
|
||||||
|
echo " 云端:"
|
||||||
|
ssh txjp "ls -1 /root/docker/db-backups/" 2>/dev/null || echo " (无)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TARGET=$1
|
||||||
|
SERVICE=$2
|
||||||
|
BACKUP_FILE=$3
|
||||||
|
|
||||||
|
# ---- 路径定义 ----
|
||||||
|
LOCAL_BACKUP_DIR="/Users/niuniu/programs/docker/db-backups"
|
||||||
|
CLOUD_BACKUP_DIR="/root/docker/db-backups"
|
||||||
|
|
||||||
|
case $SERVICE in
|
||||||
|
issue)
|
||||||
|
PROJECT_DIR="issue-ai"
|
||||||
|
CONTAINER="issue-ai"
|
||||||
|
DB_PATH_IN_CONTAINER="/app/data/issue.db"
|
||||||
|
LOCAL_DB_PATH="/Users/niuniu/programs/docker/issue-ai/data/issue.db"
|
||||||
|
;;
|
||||||
|
assets)
|
||||||
|
PROJECT_DIR="assets-ai"
|
||||||
|
CONTAINER="assets-ai"
|
||||||
|
DB_PATH_IN_CONTAINER="/app/data/assets.db"
|
||||||
|
LOCAL_DB_PATH="/Users/niuniu/programs/docker/assets-ai/data/assets.db"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "错误: 服务名必须是 issue 或 assets"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 本地恢复
|
||||||
|
# ============================================
|
||||||
|
restore_local() {
|
||||||
|
local BACKUP_PATH="${LOCAL_BACKUP_DIR}/${BACKUP_FILE}"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " 本地数据库恢复"
|
||||||
|
echo "========================================"
|
||||||
|
echo " 服务: ${SERVICE} (${CONTAINER})"
|
||||||
|
echo " 备份文件: ${BACKUP_PATH}"
|
||||||
|
echo " 目标数据库: ${LOCAL_DB_PATH}"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. 检查备份文件
|
||||||
|
if [ ! -f "$BACKUP_PATH" ]; then
|
||||||
|
echo "[错误] 备份文件不存在: ${BACKUP_PATH}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. 验证备份文件是有效的 SQLite 数据库
|
||||||
|
if ! sqlite3 "$BACKUP_PATH" "SELECT count(*) FROM sqlite_master" > /dev/null 2>&1; then
|
||||||
|
echo "[错误] 备份文件不是有效的 SQLite 数据库"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. 确认操作
|
||||||
|
echo "[警告] 这将覆盖本地 ${CONTAINER} 数据库!"
|
||||||
|
echo " 当前数据库: ${LOCAL_DB_PATH}"
|
||||||
|
echo " 将替换为: ${BACKUP_PATH}"
|
||||||
|
echo ""
|
||||||
|
read -p "确认恢复? (输入 yes 继续): " CONFIRM
|
||||||
|
if [ "$CONFIRM" != "yes" ]; then
|
||||||
|
echo "已取消"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. 备份当前数据库(安全起见)
|
||||||
|
local SAFE_BACKUP="${LOCAL_DB_PATH}.before-restore-$(date +%Y%m%d_%H%M%S)"
|
||||||
|
if [ -f "$LOCAL_DB_PATH" ]; then
|
||||||
|
cp "$LOCAL_DB_PATH" "$SAFE_BACKUP"
|
||||||
|
echo "[备份] 当前数据库已备份至: ${SAFE_BACKUP}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. WAL checkpoint on backup (确保数据完整)
|
||||||
|
sqlite3 "$BACKUP_PATH" "PRAGMA wal_checkpoint(TRUNCATE)" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 6. 停止本地容器(如果在运行)
|
||||||
|
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then
|
||||||
|
echo "[操作] 停止容器 ${CONTAINER}..."
|
||||||
|
docker stop "$CONTAINER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7. 替换数据库
|
||||||
|
cp "$BACKUP_PATH" "$LOCAL_DB_PATH"
|
||||||
|
rm -f "${LOCAL_DB_PATH}-shm" "${LOCAL_DB_PATH}-wal"
|
||||||
|
echo "[完成] 数据库已替换"
|
||||||
|
|
||||||
|
# 8. 启动容器
|
||||||
|
echo "[操作] 启动容器 ${CONTAINER}..."
|
||||||
|
docker start "$CONTAINER"
|
||||||
|
|
||||||
|
# 9. 验证
|
||||||
|
sleep 2
|
||||||
|
echo ""
|
||||||
|
echo "[验证] 恢复后数据:"
|
||||||
|
if [ "$SERVICE" = "issue" ]; then
|
||||||
|
echo " 工单数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM tickets")"
|
||||||
|
echo " 配件名称: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM tickets WHERE parts_name IS NOT NULL AND parts_name != ''") 条"
|
||||||
|
else
|
||||||
|
echo " 设备数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM assets")"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "恢复完成!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 云端恢复
|
||||||
|
# ============================================
|
||||||
|
restore_cloud() {
|
||||||
|
local BACKUP_PATH="${CLOUD_BACKUP_DIR}/${BACKUP_FILE}"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " 云端数据库恢复 (txjp)"
|
||||||
|
echo "========================================"
|
||||||
|
echo " 服务: ${SERVICE} (${CONTAINER})"
|
||||||
|
echo " 备份文件: ${BACKUP_PATH}"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. 检查备份文件
|
||||||
|
if ! ssh txjp "test -f ${BACKUP_PATH}"; then
|
||||||
|
echo "[错误] 云端备份文件不存在: ${BACKUP_PATH}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. 验证备份文件
|
||||||
|
if ! ssh txjp "sqlite3 ${BACKUP_PATH} 'SELECT count(*) FROM sqlite_master' > /dev/null 2>&1"; then
|
||||||
|
echo "[错误] 备份文件不是有效的 SQLite 数据库"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. 显示备份和当前状态
|
||||||
|
echo "[信息] 备份文件信息:"
|
||||||
|
ssh txjp "ls -lh ${BACKUP_PATH}"
|
||||||
|
echo ""
|
||||||
|
echo "[信息] 当前云端数据库状态:"
|
||||||
|
if [ "$SERVICE" = "issue" ]; then
|
||||||
|
ssh txjp "docker exec ${CONTAINER} node -e \"
|
||||||
|
const D=require('better-sqlite3');
|
||||||
|
const db=new D('/app/data/issue.db',{readonly:true});
|
||||||
|
console.log(' 工单数:', db.prepare('SELECT COUNT(*) as c FROM tickets').get().c);
|
||||||
|
db.close();
|
||||||
|
\""
|
||||||
|
else
|
||||||
|
ssh txjp "docker exec ${CONTAINER} node -e \"
|
||||||
|
const D=require('better-sqlite3');
|
||||||
|
const db=new D('/app/data/assets.db',{readonly:true});
|
||||||
|
console.log(' 设备数:', db.prepare('SELECT COUNT(*) as c FROM assets').get().c);
|
||||||
|
db.close();
|
||||||
|
\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. 确认
|
||||||
|
echo ""
|
||||||
|
echo "[警告] 这将覆盖云端 ${CONTAINER} 数据库!"
|
||||||
|
read -p "确认恢复? (输入 yes 继续): " CONFIRM
|
||||||
|
if [ "$CONFIRM" != "yes" ]; then
|
||||||
|
echo "已取消"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. 云端安全备份当前数据库
|
||||||
|
local SAFE_NAME="before-restore-$(date +%Y%m%d_%H%M%S).db"
|
||||||
|
echo "[操作] 备份云端当前数据库..."
|
||||||
|
ssh txjp "docker cp ${CONTAINER}:${DB_PATH_IN_CONTAINER} /root/docker/db-backups/${SAFE_NAME}"
|
||||||
|
echo "[备份] 当前数据库已保存至云端: /root/docker/db-backups/${SAFE_NAME}"
|
||||||
|
|
||||||
|
# 6. 复制备份文件到容器
|
||||||
|
echo "[操作] 复制备份文件到容器..."
|
||||||
|
ssh txjp "docker cp ${BACKUP_PATH} ${CONTAINER}:${DB_PATH_IN_CONTAINER}"
|
||||||
|
|
||||||
|
# 7. 重启容器
|
||||||
|
echo "[操作] 重启容器 ${CONTAINER}..."
|
||||||
|
ssh txjp "cd /root/docker/${PROJECT_DIR} && docker compose restart ${CONTAINER}"
|
||||||
|
|
||||||
|
# 8. 验证
|
||||||
|
sleep 3
|
||||||
|
echo ""
|
||||||
|
echo "[验证] 恢复后数据:"
|
||||||
|
if [ "$SERVICE" = "issue" ]; then
|
||||||
|
ssh txjp "docker exec ${CONTAINER} node -e \"
|
||||||
|
const D=require('better-sqlite3');
|
||||||
|
const db=new D('/app/data/issue.db',{readonly:true});
|
||||||
|
console.log(' 工单数:', db.prepare('SELECT COUNT(*) as c FROM tickets').get().c);
|
||||||
|
db.close();
|
||||||
|
\""
|
||||||
|
else
|
||||||
|
ssh txjp "docker exec ${CONTAINER} node -e \"
|
||||||
|
const D=require('better-sqlite3');
|
||||||
|
const db=new D('/app/data/assets.db',{readonly:true});
|
||||||
|
console.log(' 设备数:', db.prepare('SELECT COUNT(*) as c FROM assets').get().c);
|
||||||
|
db.close();
|
||||||
|
\""
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "恢复完成!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 执行 ----
|
||||||
|
case $TARGET in
|
||||||
|
local) restore_local ;;
|
||||||
|
cloud) restore_cloud ;;
|
||||||
|
*)
|
||||||
|
echo "错误: 目标必须是 local 或 cloud"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Loading…
Reference in New Issue