Initial commit: 浼佷笟寰俊 AI 鏈哄櫒浜哄姪鐞?MVP

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
bujie9527
2026-02-05 16:36:32 +08:00
commit 59275ed4dc
126 changed files with 9120 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# 最小闭环验收脚本:健康检查、登录、可选回调验签
# 用法: BASE_URL=http://localhost ./acceptance.sh 或 BASE_URL=https://your-domain.com ./acceptance.sh
set -e
BASE_URL="${BASE_URL:-http://localhost}"
echo "=== 1. Health ==="
r=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/health")
if [ "$r" != "200" ]; then
echo "FAIL health: got $r"
exit 1
fi
echo "OK"
echo "=== 2. Login ==="
login=$(curl -s -X POST "$BASE_URL/api/auth/login" -H "Content-Type: application/json" -d '{"username":"admin","password":"admin"}')
code=$(echo "$login" | grep -o '"code":[0-9]*' | cut -d: -f2)
if [ "$code" != "0" ]; then
echo "FAIL login: $login"
exit 1
fi
echo "OK"
echo "=== 3. WeCom callback GET (验签需正确 Token/Key此处仅检查 200 或 400) ==="
wecom_get=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/wecom/callback?msg_signature=xxx&timestamp=1&nonce=1&echostr=xxx")
if [ "$wecom_get" != "200" ] && [ "$wecom_get" != "400" ]; then
echo "WARN wecom GET: got $wecom_get (expected 200 or 400)"
fi
echo "OK (status $wecom_get)"
echo "=== All checks passed ==="

View File

@@ -0,0 +1,88 @@
#!/bin/bash
# 云端最小回调壳部署脚本
# 用途:在备案域名服务器上部署最小可用回调壳
set -e
echo "=== 企业微信 AI 助手 - 最小回调壳部署 ==="
echo ""
# 检查环境变量
if [ -z "$DOMAIN" ]; then
echo "错误: 未设置 DOMAIN 环境变量"
echo "请设置: export DOMAIN=your-domain.com"
exit 1
fi
if [ ! -f ".env" ]; then
echo "错误: 未找到 .env 文件"
echo "请复制 .env.example 并填写必需变量"
exit 1
fi
# 检查必需的环境变量
source .env
required_vars=("WECOM_TOKEN" "WECOM_ENCODING_AES_KEY" "WECOM_CORP_ID" "WECOM_AGENT_ID")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
echo "错误: 未设置 $var 环境变量"
exit 1
fi
done
echo "[1/5] 检查 Docker 环境..."
if ! command -v docker &> /dev/null; then
echo "错误: Docker 未安装"
exit 1
fi
if ! command -v docker-compose &> /dev/null; then
echo "错误: docker-compose 未安装"
exit 1
fi
echo "✓ Docker 环境正常"
echo ""
echo "[2/5] 构建后端镜像..."
docker-compose build backend
echo "✓ 构建完成"
echo ""
echo "[3/5] 启动服务最小回调壳backend + nginx..."
docker-compose up -d backend nginx
echo "✓ 服务已启动"
echo ""
echo "[4/5] 等待服务就绪..."
sleep 5
# 检查健康检查
max_retries=30
retry_count=0
while [ $retry_count -lt $max_retries ]; do
if curl -f -s http://localhost:8000/health > /dev/null 2>&1; then
echo "✓ 后端服务健康检查通过"
break
fi
retry_count=$((retry_count + 1))
echo "等待后端服务启动... ($retry_count/$max_retries)"
sleep 2
done
if [ $retry_count -eq $max_retries ]; then
echo "警告: 后端服务健康检查超时"
echo "请检查日志: docker-compose logs backend"
fi
echo ""
echo "[5/5] 部署完成!"
echo ""
echo "=== 下一步 ==="
echo "1. 配置企业微信回调 URL: https://$DOMAIN/api/wecom/callback"
echo "2. Token: $WECOM_TOKEN"
echo "3. EncodingAESKey: $WECOM_ENCODING_AES_KEY"
echo ""
echo "查看日志: docker-compose logs -f backend"
echo "检查服务: docker-compose ps"
echo ""

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
修复 alembic_version 表:如果数据库里记录了不存在的版本号(如 002
将其重置为当前最新的迁移版本(如 001
用法(在项目根目录):
python deploy/scripts/fix_alembic_version.py
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "backend"))
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), "..", "..", ".env"))
from sqlalchemy import create_engine, text
DATABASE_URL_SYNC = os.getenv(
"DATABASE_URL_SYNC",
"postgresql://wecom:wecom_secret@localhost:5432/wecom_ai",
)
def main():
engine = create_engine(DATABASE_URL_SYNC)
with engine.connect() as conn:
# 检查 alembic_version 表
try:
r = conn.execute(text("SELECT version_num FROM alembic_version"))
current = r.scalar_one_or_none()
if current:
print(f"当前数据库版本: {current}")
if current == "002":
print("检测到版本 002但本地只有 001。重置为 001...")
conn.execute(text("UPDATE alembic_version SET version_num = '001'"))
conn.commit()
print("已重置为 001。")
else:
print(f"版本 {current} 正常,无需修复。")
else:
print("alembic_version 表为空,无需修复。")
except Exception as e:
if "does not exist" in str(e) or "relation" in str(e).lower():
print("alembic_version 表不存在,这是首次迁移前的状态,正常。")
else:
print(f"错误: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""
修复 users 表添加缺失的列role、is_active、created_at
用法(在项目根目录):
python deploy/scripts/fix_users_table.py
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "backend"))
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), "..", "..", ".env"))
from sqlalchemy import create_engine, text, inspect
DATABASE_URL_SYNC = os.getenv(
"DATABASE_URL_SYNC",
"postgresql://wecom:wecom_secret@localhost:5432/wecom_ai",
)
def main():
engine = create_engine(DATABASE_URL_SYNC)
inspector = inspect(engine)
if not inspector.has_table("users"):
print("users 表不存在,请先执行迁移。")
sys.exit(1)
columns = [c["name"] for c in inspector.get_columns("users")]
print(f"当前 users 表的列: {columns}")
with engine.connect() as conn:
if "role" not in columns:
print("添加 role 列...")
conn.execute(text("ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(32) NOT NULL DEFAULT 'admin'"))
conn.commit()
print("✓ role 已添加")
if "is_active" not in columns:
print("添加 is_active 列...")
conn.execute(text("ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true"))
conn.commit()
print("✓ is_active 已添加")
if "created_at" not in columns:
print("添加 created_at 列...")
conn.execute(text("ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()"))
conn.commit()
print("✓ created_at 已添加")
print("修复完成。")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,31 @@
# One-click migrate (Docker). Run from project root: .\deploy\scripts\migrate.ps1
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
Set-Location $ProjectRoot
Write-Host "[1/3] Starting database..." -ForegroundColor Cyan
docker compose up -d db
Write-Host "[2/3] Waiting for DB ready..." -ForegroundColor Cyan
$max = 30
for ($i = 0; $i -lt $max; $i++) {
$null = docker compose exec -T db pg_isready -U wecom -d wecom_ai 2>$null
if ($LASTEXITCODE -eq 0) { break }
Start-Sleep -Seconds 1
}
if ($i -ge $max) {
Write-Host "DB not ready in ${max}s. Check: docker compose logs db" -ForegroundColor Red
exit 1
}
Write-Host "[3/3] Running Alembic upgrade head..." -ForegroundColor Cyan
docker compose run --rm backend sh -c "alembic upgrade head"
$code = $LASTEXITCODE
if ($code -eq 0) {
Write-Host "Migrate done." -ForegroundColor Green
} else {
Write-Host "Migrate failed. See errors above." -ForegroundColor Red
exit 1
}

72
deploy/scripts/migrate.py Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
一键迁移:支持 Docker 或本机执行,需在项目根目录运行。
python deploy/scripts/migrate.py # 默认用 Docker
python deploy/scripts/migrate.py --local # 本机执行(需已安装依赖且数据库可连)
"""
import os
import subprocess
import sys
import time
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
BACKEND_DIR = os.path.join(PROJECT_ROOT, "backend")
def run(cmd, cwd=None, env=None):
r = subprocess.run(cmd, cwd=cwd or PROJECT_ROOT, env=env or os.environ.copy(), shell=True)
if r.returncode != 0:
sys.exit(r.returncode)
def migrate_docker():
os.chdir(PROJECT_ROOT)
print("[1/3] 启动数据库...")
run("docker compose up -d db")
print("[2/3] 等待数据库就绪...")
for i in range(30):
r = subprocess.run(
"docker compose exec -T db pg_isready -U wecom -d wecom_ai",
shell=True,
cwd=PROJECT_ROOT,
capture_output=True,
)
if r.returncode == 0:
break
time.sleep(1)
else:
print("数据库未在 30s 内就绪,请检查 docker compose logs db")
sys.exit(1)
print("[3/3] 执行 Alembic 迁移...")
run('docker compose run --rm backend sh -c "alembic upgrade head"')
print("迁移完成。")
def migrate_local():
# 加载 .env
env_path = os.path.join(PROJECT_ROOT, ".env")
if os.path.isfile(env_path):
with open(env_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, _, v = line.partition("=")
os.environ[k.strip()] = v.strip()
if "DATABASE_URL_SYNC" not in os.environ:
os.environ.setdefault("DATABASE_URL_SYNC", "postgresql://wecom:wecom_secret@localhost:5432/wecom_ai")
os.chdir(BACKEND_DIR)
os.environ["PYTHONPATH"] = BACKEND_DIR
print("本机执行迁移backend 目录)...")
run("alembic upgrade head", cwd=BACKEND_DIR)
print("迁移完成。")
def main():
if "--local" in sys.argv:
migrate_local()
else:
migrate_docker()
if __name__ == "__main__":
main()

27
deploy/scripts/migrate.sh Normal file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# 一键迁移Docker在项目根目录执行
# 用法: bash deploy/scripts/migrate.sh 或 ./deploy/scripts/migrate.sh
set -e
cd "$(dirname "$0")/../.."
echo "[1/3] 启动数据库..."
docker compose up -d db
echo "[2/3] 等待数据库就绪..."
max=30
for i in $(seq 1 $max); do
if docker compose exec -T db pg_isready -U wecom -d wecom_ai 2>/dev/null; then
break
fi
if [ "$i" -eq "$max" ]; then
echo "数据库未在 ${max}s 内就绪,请检查 docker compose logs db"
exit 1
fi
sleep 1
done
echo "[3/3] 执行 Alembic 迁移..."
docker compose run --rm backend sh -c "alembic upgrade head"
echo "迁移完成。"

69
deploy/scripts/seed.py Normal file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
初始化管理员账号。从项目根目录的 .env 读取 ADMIN_USERNAME、ADMIN_PASSWORD可选
未设置则使用默认 admin / admin。需先执行 Alembic 迁移。
用法(在项目根目录):
python deploy/scripts/seed.py
cd backend && PYTHONPATH=. python ../deploy/scripts/seed.py
"""
import os
import sys
import uuid
# 允许从项目根或 backend 运行
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "backend"))
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), "..", "..", ".env"))
from sqlalchemy import create_engine, select, text
from sqlalchemy.orm import sessionmaker
# 同步连接,供脚本使用
DATABASE_URL_SYNC = os.getenv(
"DATABASE_URL_SYNC",
"postgresql://wecom:wecom_secret@localhost:5432/wecom_ai",
)
def main():
import bcrypt
username = os.getenv("ADMIN_USERNAME", "admin")
password = os.getenv("ADMIN_PASSWORD", "admin")
password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
engine = create_engine(DATABASE_URL_SYNC)
Session = sessionmaker(bind=engine)
with Session() as session:
# 检查 users 表是否存在
try:
session.execute(text("SELECT 1 FROM users LIMIT 1"))
except Exception:
print("表 users 不存在,请先执行: cd backend && alembic upgrade head")
sys.exit(1)
# 是否已存在该用户名
r = session.execute(
text("SELECT id FROM users WHERE username = :u"),
{"u": username},
)
if r.one_or_none():
print(f"用户 {username} 已存在,跳过创建。")
return
user_id = uuid.uuid4()
session.execute(
text(
"INSERT INTO users (id, username, password_hash, role, is_active, created_at) "
"VALUES (:id, :u, :p, 'admin', true, NOW())"
),
{"id": user_id, "u": username, "p": password_hash},
)
session.commit()
print(f"已创建管理员: username={username}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,67 @@
#!/bin/bash
# SSL 证书配置脚本Let's Encrypt
# 用途:为备案域名配置 HTTPS 证书
set -e
if [ -z "$DOMAIN" ]; then
echo "错误: 未设置 DOMAIN 环境变量"
echo "请设置: export DOMAIN=your-domain.com"
exit 1
fi
if [ -z "$SSL_EMAIL" ]; then
echo "错误: 未设置 SSL_EMAIL 环境变量"
echo "请设置: export SSL_EMAIL=your-email@example.com"
exit 1
fi
echo "=== SSL 证书配置Let's Encrypt==="
echo "域名: $DOMAIN"
echo "邮箱: $SSL_EMAIL"
echo ""
# 检查 Certbot
if ! command -v certbot &> /dev/null; then
echo "安装 Certbot..."
if [ -f /etc/debian_version ]; then
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
elif [ -f /etc/redhat-release ]; then
sudo yum install -y certbot python3-certbot-nginx
else
echo "错误: 未检测到支持的 Linux 发行版"
exit 1
fi
fi
echo "[1/3] 确保 HTTP 服务运行(用于验证)..."
docker-compose up -d backend nginx
sleep 3
echo "[2/3] 获取 SSL 证书..."
sudo certbot certonly --nginx \
-d "$DOMAIN" \
-d "www.$DOMAIN" \
--email "$SSL_EMAIL" \
--agree-tos \
--non-interactive \
--preferred-challenges http
echo "[3/3] 更新 Nginx 配置..."
# 更新 nginx-ssl.conf使用实际证书路径
sed -i "s|ssl_certificate.*|ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;|" deploy/nginx-ssl.conf
sed -i "s|ssl_certificate_key.*|ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;|" deploy/nginx-ssl.conf
# 更新 docker-compose.yml挂载证书目录
# 注意:需要手动更新 docker-compose.yml 的 volumes
echo "✓ SSL 证书配置完成"
echo ""
echo "证书路径: /etc/letsencrypt/live/$DOMAIN/"
echo ""
echo "请更新 docker-compose.yml添加证书挂载"
echo " volumes:"
echo " - /etc/letsencrypt:/etc/letsencrypt:ro"
echo ""
echo "然后重启 Nginx: docker-compose restart nginx"

55
deploy/scripts/start.sh Normal file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
# 生产环境启动脚本
# 用途:启动生产服务
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
echo "=== 启动生产服务 ==="
echo ""
# 检查 .env.prod 文件
if [ ! -f ".env.prod" ]; then
echo "错误: 未找到 .env.prod 文件"
echo "请复制 .env.example 为 .env.prod 并填写生产环境变量"
exit 1
fi
# 检查必需的环境变量
source .env.prod
required_vars=("WECOM_TOKEN" "WECOM_ENCODING_AES_KEY" "WECOM_CORP_ID" "WECOM_AGENT_ID")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
echo "错误: .env.prod 中未设置 $var"
exit 1
fi
done
# 设置镜像标签(默认 latest
IMAGE_TAG=${IMAGE_TAG:-latest}
export IMAGE_TAG
# 启动服务
echo "使用镜像标签: $IMAGE_TAG"
echo ""
docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d
echo ""
echo "等待服务启动..."
sleep 5
# 检查服务状态
echo ""
echo "服务状态:"
docker-compose -f docker-compose.prod.yml ps
echo ""
echo "=== 启动完成 ==="
echo ""
echo "查看日志: docker-compose -f docker-compose.prod.yml logs -f"
echo "检查健康: curl http://localhost/api/health"

18
deploy/scripts/stop.sh Normal file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# 生产环境停止脚本
# 用途:停止生产服务
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
echo "=== 停止生产服务 ==="
echo ""
docker-compose -f docker-compose.prod.yml --env-file .env.prod down
echo ""
echo "=== 停止完成 ==="

64
deploy/scripts/update.sh Normal file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
# 生产环境更新脚本
# 用途:拉取最新镜像并重启服务
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
# 检查参数
IMAGE_TAG=${1:-latest}
if [ -z "$IMAGE_TAG" ]; then
echo "用法: $0 [IMAGE_TAG]"
echo "示例: $0 latest"
echo "示例: $0 v1.0.0"
exit 1
fi
echo "=== 更新生产服务 ==="
echo "镜像标签: $IMAGE_TAG"
echo ""
# 检查 .env.prod 文件
if [ ! -f ".env.prod" ]; then
echo "错误: 未找到 .env.prod 文件"
exit 1
fi
# 设置镜像标签
export IMAGE_TAG
# 登录到容器镜像仓库(如果需要)
# docker login ghcr.io -u $GITHUB_USERNAME -p $GITHUB_TOKEN
# 拉取最新镜像
echo "[1/3] 拉取最新镜像..."
docker-compose -f docker-compose.prod.yml --env-file .env.prod pull
# 停止旧服务
echo ""
echo "[2/3] 停止旧服务..."
docker-compose -f docker-compose.prod.yml --env-file .env.prod down
# 启动新服务
echo ""
echo "[3/3] 启动新服务..."
docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d
echo ""
echo "等待服务启动..."
sleep 5
# 检查服务状态
echo ""
echo "服务状态:"
docker-compose -f docker-compose.prod.yml ps
echo ""
echo "=== 更新完成 ==="
echo ""
echo "查看日志: docker-compose -f docker-compose.prod.yml logs -f"
echo "检查健康: curl http://localhost/api/health"