release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
This commit is contained in:
+11
-6
@@ -309,7 +309,8 @@ ctms_require_docker_compose() {
|
||||
fi
|
||||
}
|
||||
|
||||
# 安装/更新所需依赖。openssl、curl 仅在非 dev 环境要求(dev 用默认密钥、不做 RSA 生成)。
|
||||
# 安装/更新所需依赖。所有环境都需要 openssl 生成独立的 ONLYOFFICE JWT 密钥;
|
||||
# main/release 还会用它生成登录 RSA 密钥。
|
||||
ctms_require_dependencies() {
|
||||
local env="$1"
|
||||
ctms_require_env "$env"
|
||||
@@ -317,10 +318,7 @@ ctms_require_dependencies() {
|
||||
|
||||
ctms_require_command docker "安装 Docker Engine 或 Docker Desktop,并确认 docker 命令可用" || missing=1
|
||||
ctms_require_command curl "Linux: sudo apt-get install -y curl | macOS: brew install curl" || missing=1
|
||||
# openssl 仅非 dev 需要:dev 用 dev-secret 与空 RSA key,不生成密钥
|
||||
if [[ "$env" != "dev" ]]; then
|
||||
ctms_require_command openssl "Linux: sudo apt-get install -y openssl | macOS: brew install openssl" || missing=1
|
||||
fi
|
||||
ctms_require_command openssl "Linux: sudo apt-get install -y openssl | macOS: brew install openssl" || missing=1
|
||||
[[ "$missing" -eq 0 ]] || ctms_fail "系统依赖不完整,请按上方提示安装后重试"
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
@@ -525,7 +523,7 @@ ctms_upsert_env_value() {
|
||||
local key="$1" value="$2"
|
||||
local temp_file
|
||||
temp_file="$(mktemp "$CTMS_ROOT_DIR/.env.tmp.XXXXXX")"
|
||||
if [[ -f "$CTMS_ENV_FILE" && -n "$(ctms_read_env_value "$key" || true)" ]]; then
|
||||
if ctms_env_has_key "$key"; then
|
||||
awk -v key="$key" -v value="$value" '
|
||||
BEGIN { prefix = key "=" }
|
||||
index($0, prefix) == 1 { print key "=" value; next }
|
||||
@@ -541,6 +539,7 @@ ctms_upsert_env_value() {
|
||||
ctms_write_env_file() {
|
||||
local project_name="$1" runtime_env="$2" login_key_id="$3"
|
||||
local jwt_secret="$4" rsa_private_key="$5"
|
||||
local onlyoffice_jwt_secret="$6" onlyoffice_instance_id="$7"
|
||||
local allow_insecure_dev_login="false"
|
||||
[[ "$runtime_env" == "development" ]] && allow_insecure_dev_login="true"
|
||||
local temp_file
|
||||
@@ -554,6 +553,12 @@ ctms_write_env_file() {
|
||||
printf 'JWT_SECRET_KEY=%s\n' "$jwt_secret"
|
||||
printf 'LOGIN_RSA_KEY_ID=%s\n' "$login_key_id"
|
||||
printf 'LOGIN_RSA_PRIVATE_KEY=%s\n' "$rsa_private_key"
|
||||
printf 'ONLYOFFICE_ENABLED=true\n'
|
||||
printf 'ONLYOFFICE_JWT_SECRET=%s\n' "$onlyoffice_jwt_secret"
|
||||
printf 'ONLYOFFICE_INTERNAL_URL=http://onlyoffice\n'
|
||||
printf 'ONLYOFFICE_STORAGE_BASE_URL=http://backend:8000\n'
|
||||
printf 'ONLYOFFICE_INSTANCE_ID=%s\n' "$onlyoffice_instance_id"
|
||||
printf 'ONLYOFFICE_CONFIG_TTL_SECONDS=300\n'
|
||||
} >> "$temp_file"
|
||||
|
||||
mv "$temp_file" "$CTMS_ENV_FILE"
|
||||
|
||||
+60
-6
@@ -173,6 +173,10 @@ generate_jwt_secret() {
|
||||
openssl rand -hex 32
|
||||
}
|
||||
|
||||
generate_onlyoffice_instance_id() {
|
||||
printf 'ctms-%s-%s' "$TARGET_ENV" "$(openssl rand -hex 8)"
|
||||
}
|
||||
|
||||
# ── 地址解析 ─────────────────────────────────
|
||||
resolve_base_url() {
|
||||
[[ -z "$BASE_URL" ]] && BASE_URL="$(default_base_url)"
|
||||
@@ -221,6 +225,7 @@ confirm_install() {
|
||||
row "健康检查地址" "$BASE_URL" "${CC_INFO}"
|
||||
row ".env 文件" "$env_status"
|
||||
row "pg_data 目录" "$pg_status"
|
||||
row "ONLYOFFICE" "标准组件(自动安装)" "${CC_INFO}"
|
||||
row "镜像构建" "$build_status"
|
||||
row "数据库迁移" "$migrate_status"
|
||||
ctms_box_bottom "${CC_PRIMARY}${C_BOLD}" "$w"
|
||||
@@ -252,6 +257,7 @@ prepare_env_file() {
|
||||
step "准备环境配置文件"
|
||||
local project_name="$1" runtime_env="$2" login_key_id="$3"
|
||||
local jwt_secret="" rsa_private_key=""
|
||||
local onlyoffice_jwt_secret="" onlyoffice_instance_id=""
|
||||
|
||||
# 首次安装:无 .env,写全量。
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
@@ -265,7 +271,12 @@ prepare_env_file() {
|
||||
rsa_private_key="$(generate_escaped_private_key)"
|
||||
ok "已生成新的 LOGIN_RSA_PRIVATE_KEY"
|
||||
fi
|
||||
ctms_write_env_file "$project_name" "$runtime_env" "$login_key_id" "$jwt_secret" "$rsa_private_key"
|
||||
onlyoffice_jwt_secret="$(generate_jwt_secret)"
|
||||
onlyoffice_instance_id="$(generate_onlyoffice_instance_id)"
|
||||
ok "已生成独立的 ONLYOFFICE JWT 密钥与实例标识"
|
||||
ctms_write_env_file \
|
||||
"$project_name" "$runtime_env" "$login_key_id" "$jwt_secret" "$rsa_private_key" \
|
||||
"$onlyoffice_jwt_secret" "$onlyoffice_instance_id"
|
||||
ok ".env 写入完成"
|
||||
return
|
||||
fi
|
||||
@@ -310,6 +321,36 @@ prepare_env_file() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# ONLYOFFICE 是标准组件:旧环境升级时自动启用并补齐独立安全配置。
|
||||
if [[ "$(read_env_value ONLYOFFICE_ENABLED || true)" != "true" ]]; then
|
||||
ctms_upsert_env_value ONLYOFFICE_ENABLED true; repaired=1
|
||||
warn ".env 中 ONLYOFFICE 未启用,已作为标准组件启用"
|
||||
fi
|
||||
|
||||
onlyoffice_jwt_secret="$(read_env_value ONLYOFFICE_JWT_SECRET || true)"
|
||||
jwt_secret="$(read_env_value JWT_SECRET_KEY || true)"
|
||||
if [[ ${#onlyoffice_jwt_secret} -lt 32 || "$onlyoffice_jwt_secret" == "$jwt_secret" ]]; then
|
||||
onlyoffice_jwt_secret="$(generate_jwt_secret)"
|
||||
ctms_upsert_env_value ONLYOFFICE_JWT_SECRET "$onlyoffice_jwt_secret"; repaired=1
|
||||
warn ".env 缺少有效的独立 ONLYOFFICE_JWT_SECRET,已生成并补全"
|
||||
fi
|
||||
|
||||
if [[ -z "$(read_env_value ONLYOFFICE_INSTANCE_ID || true)" ]]; then
|
||||
onlyoffice_instance_id="$(generate_onlyoffice_instance_id)"
|
||||
ctms_upsert_env_value ONLYOFFICE_INSTANCE_ID "$onlyoffice_instance_id"; repaired=1
|
||||
warn ".env 缺少 ONLYOFFICE_INSTANCE_ID,已生成稳定实例标识"
|
||||
fi
|
||||
|
||||
if [[ -z "$(read_env_value ONLYOFFICE_INTERNAL_URL || true)" ]]; then
|
||||
ctms_upsert_env_value ONLYOFFICE_INTERNAL_URL http://onlyoffice; repaired=1
|
||||
fi
|
||||
if [[ -z "$(read_env_value ONLYOFFICE_STORAGE_BASE_URL || true)" ]]; then
|
||||
ctms_upsert_env_value ONLYOFFICE_STORAGE_BASE_URL http://backend:8000; repaired=1
|
||||
fi
|
||||
if [[ -z "$(read_env_value ONLYOFFICE_CONFIG_TTL_SECONDS || true)" ]]; then
|
||||
ctms_upsert_env_value ONLYOFFICE_CONFIG_TTL_SECONDS 300; repaired=1
|
||||
fi
|
||||
|
||||
if [[ "$repaired" -eq 1 ]]; then
|
||||
ok ".env 已补全缺失项"
|
||||
else
|
||||
@@ -320,7 +361,8 @@ prepare_env_file() {
|
||||
# ── Docker 流程 ───────────────────────────────
|
||||
run_compose_config() {
|
||||
step "校验 Docker Compose 配置"
|
||||
ctms_run_quiet "Docker Compose 配置语法校验" -- compose_cmd config
|
||||
# config 会展开并输出 JWT/RSA 等敏感环境变量;即使启用 --verbose 也只做静默校验。
|
||||
ctms_run_quiet "Docker Compose 配置语法校验" -- compose_cmd config --quiet
|
||||
}
|
||||
|
||||
run_backend_init() {
|
||||
@@ -338,14 +380,14 @@ run_build_and_start() {
|
||||
if [[ "$SKIP_BUILD" -eq 1 ]]; then
|
||||
step "启动服务(跳过镜像构建)"
|
||||
if [[ "$TARGET_ENV" == "dev" ]]; then
|
||||
ctms_run_quiet "启动开发容器并刷新后端/Nginx/Vite" -- compose_cmd up -d --force-recreate backend nginx frontend-dev
|
||||
ctms_run_quiet "启动开发容器并刷新后端/ONLYOFFICE/Nginx/Vite" -- compose_cmd up -d --force-recreate backend onlyoffice nginx frontend-dev
|
||||
else
|
||||
ctms_run_quiet "启动容器" -- compose_cmd up -d
|
||||
fi
|
||||
else
|
||||
step "构建镜像并启动服务"
|
||||
if [[ "$TARGET_ENV" == "dev" ]]; then
|
||||
CTMS_LIVE_OUTPUT=1 ctms_run_quiet "构建并刷新开发容器(首次较慢)" -- compose_cmd up -d --build --force-recreate backend nginx frontend-dev
|
||||
CTMS_LIVE_OUTPUT=1 ctms_run_quiet "构建并刷新开发容器(首次较慢)" -- compose_cmd up -d --build --force-recreate backend onlyoffice nginx frontend-dev
|
||||
else
|
||||
CTMS_LIVE_OUTPUT=1 ctms_run_quiet "构建镜像并启动容器(首次较慢)" -- compose_cmd up -d --build
|
||||
fi
|
||||
@@ -366,7 +408,7 @@ check_container_status() {
|
||||
step "检查容器运行状态"
|
||||
local running
|
||||
running="$(compose_cmd ps --services --filter status=running 2>/dev/null || true)"
|
||||
local services=(db backend nginx)
|
||||
local services=(db backend onlyoffice nginx)
|
||||
[[ "$TARGET_ENV" != "dev" ]] || services+=(frontend-dev)
|
||||
local svc
|
||||
for svc in "${services[@]}"; do
|
||||
@@ -398,6 +440,14 @@ if settings.LOGIN_RSA_KEY_ID != expected_key_id:
|
||||
sys.exit("LOGIN_RSA_KEY_ID 不匹配")
|
||||
if expected_env == "production" and settings.JWT_SECRET_KEY == "dev-secret":
|
||||
sys.exit("生产环境不允许使用默认 JWT_SECRET_KEY")
|
||||
if not settings.ONLYOFFICE_ENABLED:
|
||||
sys.exit("ONLYOFFICE 必须作为标准组件启用")
|
||||
if len(settings.ONLYOFFICE_JWT_SECRET or "") < 32:
|
||||
sys.exit("ONLYOFFICE_JWT_SECRET 未正确配置")
|
||||
if settings.ONLYOFFICE_JWT_SECRET == settings.JWT_SECRET_KEY:
|
||||
sys.exit("ONLYOFFICE_JWT_SECRET 不得复用登录 JWT 密钥")
|
||||
if not settings.ONLYOFFICE_INSTANCE_ID:
|
||||
sys.exit("ONLYOFFICE_INSTANCE_ID 未配置")
|
||||
if expect_rsa:
|
||||
if not settings.LOGIN_RSA_PRIVATE_KEY:
|
||||
sys.exit("LOGIN_RSA_PRIVATE_KEY 未配置")
|
||||
@@ -461,7 +511,9 @@ run_health_checks() {
|
||||
check_backend_environment "$runtime_env" "$login_key_id" "$expect_rsa"
|
||||
step "探测 HTTP 接口可用性"
|
||||
check_http_endpoint "/health"
|
||||
check_http_endpoint "/readyz"
|
||||
check_http_endpoint "/api/v1/auth/login-key"
|
||||
check_http_endpoint "/onlyoffice/healthcheck" 120
|
||||
if [[ "$TARGET_ENV" == "dev" ]]; then
|
||||
check_http_endpoint "/" 60
|
||||
fi
|
||||
@@ -514,4 +566,6 @@ main() {
|
||||
show_success
|
||||
}
|
||||
|
||||
main "$@"
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
ROTATE_SECRET=0
|
||||
if [[ "${1:-}" == "--rotate-secret" ]]; then
|
||||
ROTATE_SECRET=1
|
||||
shift
|
||||
fi
|
||||
if [[ $# -gt 0 ]]; then
|
||||
ctms_fail "未知参数: $1(支持的参数仅有 --rotate-secret)"
|
||||
fi
|
||||
|
||||
generate_random_hex() {
|
||||
local bytes="${1:-32}"
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl rand -hex "$bytes"
|
||||
return
|
||||
fi
|
||||
od -An -N "$bytes" -tx1 /dev/urandom | tr -d ' \n'
|
||||
}
|
||||
|
||||
prepare_onlyoffice_environment() {
|
||||
local secret current_login_secret instance_id generated=0
|
||||
secret="$(ctms_read_env_value ONLYOFFICE_JWT_SECRET || true)"
|
||||
current_login_secret="$(ctms_read_env_value JWT_SECRET_KEY || true)"
|
||||
instance_id="$(ctms_read_env_value ONLYOFFICE_INSTANCE_ID || true)"
|
||||
|
||||
if [[ "$ROTATE_SECRET" -eq 1 || ${#secret} -lt 32 || "$secret" == "$current_login_secret" ]]; then
|
||||
secret="$(generate_random_hex 32)"
|
||||
ctms_upsert_env_value ONLYOFFICE_JWT_SECRET "$secret"
|
||||
generated=1
|
||||
fi
|
||||
|
||||
if [[ -z "$instance_id" ]]; then
|
||||
instance_id="ctms-dev-$(generate_random_hex 8)"
|
||||
ctms_upsert_env_value ONLYOFFICE_INSTANCE_ID "$instance_id"
|
||||
fi
|
||||
|
||||
ctms_upsert_env_value ONLYOFFICE_ENABLED true
|
||||
ctms_upsert_env_value ONLYOFFICE_INTERNAL_URL http://onlyoffice
|
||||
ctms_upsert_env_value ONLYOFFICE_STORAGE_BASE_URL http://backend:8000
|
||||
ctms_upsert_env_value ONLYOFFICE_CONFIG_TTL_SECONDS 300
|
||||
chmod 600 "$CTMS_ENV_FILE"
|
||||
|
||||
if [[ "$generated" -eq 1 ]]; then
|
||||
ctms_ok "已生成新的 ONLYOFFICE 开发密钥并写入 .env(内容不会回显)"
|
||||
else
|
||||
ctms_ok "已复用 .env 中现有的 ONLYOFFICE 开发密钥"
|
||||
fi
|
||||
ctms_ok "ONLYOFFICE 开发实例标识已就绪: $instance_id"
|
||||
}
|
||||
|
||||
wait_for_url() {
|
||||
local label="$1" url="$2" attempts="${3:-90}" attempt=1
|
||||
while [[ "$attempt" -le "$attempts" ]]; do
|
||||
if curl -fsS "$url" >/dev/null 2>&1; then
|
||||
ctms_ok "$label 已就绪"
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
attempt=$(( attempt + 1 ))
|
||||
done
|
||||
ctms_fail "$label 未在预期时间内就绪: $url"
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$CTMS_ROOT_DIR"
|
||||
ctms_require_docker_compose
|
||||
ctms_require_command curl "macOS 通常已预装;Linux 可执行 apt-get install curl" \
|
||||
|| ctms_fail "缺少 curl,无法执行启动健康检查"
|
||||
|
||||
ctms_step "准备 ONLYOFFICE 开发环境"
|
||||
prepare_onlyoffice_environment
|
||||
|
||||
local -a compose=(
|
||||
docker compose
|
||||
-f "$CTMS_ROOT_DIR/docker-compose.dev.yaml"
|
||||
-p ctms_dev
|
||||
)
|
||||
|
||||
ctms_step "校验开发 Compose 配置"
|
||||
"${compose[@]}" config --quiet
|
||||
ctms_ok "Compose 配置有效"
|
||||
|
||||
ctms_step "重新构建并启动标准 ONLYOFFICE 服务"
|
||||
"${compose[@]}" up -d --build --force-recreate backend onlyoffice nginx
|
||||
|
||||
ctms_step "验证运行状态"
|
||||
wait_for_url "CTMS 后端" "http://127.0.0.1:8888/readyz" 60
|
||||
wait_for_url "ONLYOFFICE Document Server" "http://127.0.0.1:8888/onlyoffice/healthcheck" 120
|
||||
"${compose[@]}" exec -T backend python -c '
|
||||
from app.core.config import settings
|
||||
from app.main import app
|
||||
|
||||
paths = {route.path for route in app.routes}
|
||||
required = {
|
||||
"/api/v1/onlyoffice/attachments/{attachment_id}/config",
|
||||
"/api/v1/onlyoffice/versions/{version_id}/config",
|
||||
"/api/v1/studies/{study_id}/collaboration/files/{file_id}/editor-config",
|
||||
"/api/v1/studies/{study_id}/collaboration/files/{file_id}/share-link",
|
||||
"/api/v1/collaboration/shares/metadata",
|
||||
"/api/v1/collaboration/shares/access",
|
||||
"/api/v1/collaboration/shares/editor-config",
|
||||
"/internal/onlyoffice/collaboration/sessions/{session_id}/content",
|
||||
"/internal/onlyoffice/collaboration/sessions/{session_id}/callback",
|
||||
}
|
||||
if not settings.ONLYOFFICE_ENABLED:
|
||||
raise SystemExit("ONLYOFFICE_ENABLED 未生效")
|
||||
if len(settings.ONLYOFFICE_JWT_SECRET or "") < 32:
|
||||
raise SystemExit("ONLYOFFICE_JWT_SECRET 未生效")
|
||||
if not settings.ONLYOFFICE_INSTANCE_ID:
|
||||
raise SystemExit("ONLYOFFICE_INSTANCE_ID 未生效")
|
||||
missing = required - paths
|
||||
if missing:
|
||||
raise SystemExit(f"后端缺少 ONLYOFFICE 路由: {sorted(missing)}")
|
||||
print("ONLYOFFICE backend configuration and routes are ready")
|
||||
'
|
||||
ctms_ok "ONLYOFFICE 开发预览环境启动完成"
|
||||
}
|
||||
|
||||
main
|
||||
@@ -15,7 +15,7 @@ ${C_BOLD}环境:${C_RESET}
|
||||
|
||||
${C_BOLD}说明:${C_RESET}
|
||||
默认仅停止并移除对应 Compose 项目的容器和网络。
|
||||
不删除 .env、pg_data、Docker volume 或镜像。
|
||||
不删除 .env、pg_data、backend/app/uploads、Docker volume 或镜像。
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ main() {
|
||||
ctms_require_docker_compose
|
||||
ctms_confirm_danger \
|
||||
"停止并移除 CTMS 容器" \
|
||||
"Compose 项目 ${project};不会删除 .env、pg_data、volume 或镜像。" \
|
||||
"Compose 项目 ${project};不会删除 .env、pg_data、backend/app/uploads、volume 或镜像。" \
|
||||
"服务会停止,对外访问中断;数据默认保留。"
|
||||
|
||||
cd "$CTMS_ROOT_DIR"
|
||||
|
||||
Reference in New Issue
Block a user