#!/usr/bin/env bash set -euo pipefail target=/mnt/PoolOne/apps/youyidian-notification-center archive=/tmp/youyidian-notification-center-release.tar.gz source_env=/mnt/PoolOne/apps/youyidian-3d/.env backup_root=/mnt/PoolOne/apps/youyidian-notification-center-backups test -f "$archive" test -f "$source_env" if tar -tzf "$archive" | grep -Eq '(^/|(^|/)\.\.(/|$))'; then echo '安装包包含不安全路径' >&2 exit 1 fi mkdir -p "$target" "$backup_root" if test -f "$target/package.json"; then stamp="$(date +%Y%m%d%H%M%S)" tar -czf "$backup_root/source-$stamp.tar.gz" --exclude=.env --exclude=data -C "$target" . fi tar -xzf "$archive" -C "$target" python3 - <<'PY' from pathlib import Path import json import os import secrets import shutil import time def parse_env(path: Path): result = {} if not path.exists(): return result for raw in path.read_text(encoding='utf-8').splitlines(): line = raw.strip() if not line or line.startswith('#') or '=' not in line: continue key, value = line.split('=', 1) key = key.strip() value = value.strip() if value.startswith('"') and value.endswith('"'): try: value = json.loads(value) except Exception: value = value[1:-1] elif value.startswith("'") and value.endswith("'"): value = value[1:-1] result[key] = value return result def quoted(value): return json.dumps(str(value), ensure_ascii=False) source_path = Path('/mnt/PoolOne/apps/youyidian-3d/.env') target_path = Path('/mnt/PoolOne/apps/youyidian-notification-center/.env') source = parse_env(source_path) existing = parse_env(target_path) app_key = ( source.get('DINGTALK_WORK_NOTICE_CLIENT_ID') or source.get('DINGTALK_WORK_NOTICE_APP_KEY') or source.get('DINGTALK_CLIENT_ID') or source.get('DINGTALK_APP_KEY') ) app_secret = ( source.get('DINGTALK_WORK_NOTICE_CLIENT_SECRET') or source.get('DINGTALK_WORK_NOTICE_APP_SECRET') or source.get('DINGTALK_CLIENT_SECRET') or source.get('DINGTALK_APP_SECRET') ) robot_code = source.get('DINGTALK_ROBOT_CODE') if not app_key or not app_secret or not robot_code: raise SystemExit('素材库生产环境缺少统一机器人配置') try: old_projects = json.loads(existing.get('YYD_NOTIFICATION_PROJECTS_JSON', '[]')) except Exception: old_projects = [] by_code = { str(item.get('code')): item for item in old_projects if isinstance(item, dict) } def secret_for(code): old = str(by_code.get(code, {}).get('secret') or '') return old if len(old) >= 24 else secrets.token_hex(32) projects = [ { 'code': 'material_library', 'name': '素材库', 'secret': secret_for('material_library'), 'allowedHosts': ['sc.jiansun.vip'], }, { 'code': 'fixed_asset', 'name': '固定资产', 'secret': secret_for('fixed_asset'), 'allowedHosts': ['www.jsgdzc.cn', 'jsgdzc.cn'], }, ] center_lines = [ 'PORT=3088', 'DINGTALK_APP_KEY=' + quoted(app_key), 'DINGTALK_APP_SECRET=' + quoted(app_secret), 'DINGTALK_ROBOT_CODE=' + quoted(robot_code), 'YYD_NOTIFICATION_PROJECTS_JSON=' + quoted( json.dumps(projects, ensure_ascii=False, separators=(',', ':')) ), 'WORKER_ENABLED=true', 'WORKER_INTERVAL_MS=1000', 'READ_STATUS_INTERVAL_MS=5000', ] target_path.write_text('\n'.join(center_lines) + '\n', encoding='utf-8') os.chmod(target_path, 0o600) material_secret = next( item['secret'] for item in projects if item['code'] == 'material_library' ) updates = { 'YYD_NOTIFICATION_CENTER_URL': 'https://sc.jiansun.vip/notification-center', 'YYD_NOTIFICATION_CENTER_PROJECT_CODE': 'material_library', 'YYD_NOTIFICATION_CENTER_PROJECT_SECRET': material_secret, 'YYD_NOTIFICATION_CENTER_TEST_USER_IDS': '', } backup = source_path.with_name( '.env.before-yyd-notification-' + time.strftime('%Y%m%d%H%M%S') ) shutil.copy2(source_path, backup) os.chmod(backup, 0o600) lines = source_path.read_text(encoding='utf-8').splitlines() seen = set() output = [] for line in lines: if '=' in line and not line.lstrip().startswith('#'): key = line.split('=', 1)[0].strip() if key in updates: output.append(key + '=' + quoted(updates[key])) seen.add(key) continue output.append(line) for key, value in updates.items(): if key not in seen: output.append(key + '=' + quoted(value)) source_path.write_text('\n'.join(output) + '\n', encoding='utf-8') os.chmod(source_path, 0o600) print('通知中心与素材库适配配置已写入') PY cd "$target" image="youyidian-notification-center:${YYD_NOTIFICATION_IMAGE_TAG:-latest}" if docker image inspect "$image" >/dev/null 2>&1; then docker compose up -d --no-build else docker compose up -d --build fi healthy=0 for _ in $(seq 1 30); do if curl -fsS --max-time 3 http://127.0.0.1:3088/health >/tmp/yyd-notification-health.json; then healthy=1 break fi sleep 2 done if test "$healthy" != 1; then docker compose logs --tail 120 exit 1 fi python3 - <<'PY' from pathlib import Path source = Path('/etc/frp/frpc-sc-entry.toml').read_text(encoding='utf-8') header = source.split('[[proxies]]', 1)[0] header = header.replace( '/root/frp/frpc-sc-entry.log', '/root/frp/frpc-notify-entry.log', ) proxy = '''[[proxies]] name = "youyidian-notification-center" type = "tcp" localIP = "127.0.0.1" localPort = 3088 remotePort = 2326 healthCheck.type = "http" healthCheck.path = "/health" healthCheck.intervalSeconds = 15 healthCheck.maxFailed = 3 healthCheck.timeoutSeconds = 5 ''' Path('/etc/frp/frpc-notify-entry.toml').write_text( header.rstrip() + '\n\n' + proxy, encoding='utf-8', ) PY chmod 600 /etc/frp/frpc-notify-entry.toml cat > /etc/systemd/system/frpc-notify-entry.service <<'EOF' [Unit] Description=FRP Client for Youyidian notification center After=network-online.target docker.service Wants=network-online.target Requires=docker.service [Service] Type=simple Restart=always RestartSec=10s LimitNOFILE=65535 ExecStart=/root/frp-upgrade/frp_0.67.0_linux_amd64/frpc -c /etc/frp/frpc-notify-entry.toml StandardOutput=append:/root/frp/frpc-notify-entry.service.log StandardError=append:/root/frp/frpc-notify-entry.service.log [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now frpc-notify-entry.service sleep 3 systemctl is-active --quiet frpc-notify-entry.service rm -f "$archive" curl -fsS http://127.0.0.1:3088/health echo printf 'CENTER_CONTAINER=' docker inspect -f '{{.State.Status}}' youyidian-notification-center printf 'FRP_SERVICE=' systemctl is-active frpc-notify-entry.service