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