Files
tkmind_go/deploy/sync-h5-db-105.sh
john 4e21ca937a
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Add TKMind platform extensions, H5/MindSpace stack, and deployment tooling.
Fork goose with custom MCP widgets, platform extensions (aider, git, web, search),
MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 21:30:20 +08:00

335 lines
12 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 将本地 H5 用户数据同步到 105 RDS(数据库连接参照 tkmind shared/config/local.json
#
# 用法:
# ./deploy/sync-h5-db-105.sh # 完整:建表 + 导数据 + 更新 .env + 同步用户目录
# ./deploy/sync-h5-db-105.sh --schema-only
# ./deploy/sync-h5-db-105.sh --data-only
# ./deploy/sync-h5-db-105.sh --no-restart
# ./deploy/sync-h5-db-105.sh --baseline-only # 仅重置生产权限基线
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
H5_DIR="${ROOT}/ui/h5"
DEPLOY_ENV="${ROOT}/deploy/h5-105.env"
LOCAL_DB_ENV="${ROOT}/.env.local"
H5_DEPLOY_HOST="${H5_DEPLOY_HOST:-root@120.26.184.105}"
H5_REMOTE_DIR="${H5_REMOTE_DIR:-/root/tkmind_go/ui/h5}"
H5_SYSTEMD_SERVICE="${H5_SYSTEMD_SERVICE:-goose-h5}"
TKMIND_SHARED_CONFIG="/root/tkmind/shared/config/local.json"
H5_RDS_DATABASE="${H5_RDS_DATABASE:-goose}"
LOCAL_MYSQL_DATABASE="${LOCAL_MYSQL_DATABASE:-goose}"
SCHEMA_ONLY=0
DATA_ONLY=0
BASELINE_ONLY=0
NO_RESTART=0
for arg in "$@"; do
case "$arg" in
--schema-only) SCHEMA_ONLY=1 ;;
--data-only) DATA_ONLY=1 ;;
--baseline-only) BASELINE_ONLY=1 ;;
--no-restart) NO_RESTART=1 ;;
-h|--help)
sed -n '2,10p' "$0"
exit 0
;;
*)
echo "未知参数: $arg" >&2
exit 1
;;
esac
done
if [[ -f "${DEPLOY_ENV}" ]]; then
set -a
# shellcheck disable=SC1090
source "${DEPLOY_ENV}"
set +a
fi
if [[ -f "${LOCAL_DB_ENV}" ]]; then
set -a
# shellcheck disable=SC1090
source "${LOCAL_DB_ENV}"
set +a
fi
LOCAL_MYSQL_USER="${MYSQL_USER:-boot}"
LOCAL_MYSQL_PASSWORD="${MYSQL_PASSWORD:-888888}"
LOCAL_MYSQL_HOST="${MYSQL_HOST:-localhost}"
LOCAL_MYSQL_PORT="${MYSQL_PORT:-3306}"
# 仅同步用户/计费数据;权限表不同步,导入后强制应用生产安全基线
H5_DATA_TABLES=(
h5_users
h5_user_wallets
h5_user_path_grants
h5_user_sessions
h5_session_billing_state
h5_usage_records
h5_billing_ledger
)
ssh_cmd() {
ssh -o ConnectTimeout=15 -o BatchMode=yes "${H5_DEPLOY_HOST}" "$@"
}
b64() {
printf '%s' "$1" | base64 | tr -d '\n'
}
read_rds_config() {
ssh_cmd "node -e \"
const fs=require('fs');
const cfg=JSON.parse(fs.readFileSync('${TKMIND_SHARED_CONFIG}','utf8'));
const db=cfg.database||{};
const out={
host:db.host||'',
port:db.port||3306,
user:db.account||db.user||'boot',
password:db.password||'',
database:'${H5_RDS_DATABASE}'
};
process.stdout.write(JSON.stringify(out));
\""
}
transform_paths() {
local file="$1"
sed -i.bak \
-e 's|/Users/john/PycharmProjects/goose/ui/h5/temp/|/root/tkmind_go/ui/h5/MindSpace/|g' \
-e 's|/Users/john/PycharmProjects/goose/ui/h5/MindSpace/|/root/tkmind_go/ui/h5/MindSpace/|g' \
-e 's|/Users/john/PycharmProjects/goose/ui/h5/users/|/root/tkmind_go/ui/h5/users/|g' \
"${file}"
rm -f "${file}.bak"
}
dump_local_data() {
local dump_file="$1"
echo "==> 导出本地库 ${LOCAL_MYSQL_DATABASE} 的 H5 表..."
MYSQL_PWD="${LOCAL_MYSQL_PASSWORD}" mysqldump \
-h "${LOCAL_MYSQL_HOST}" -P "${LOCAL_MYSQL_PORT}" -u "${LOCAL_MYSQL_USER}" \
--single-transaction --complete-insert --no-create-info --set-gtid-purged=OFF \
"${LOCAL_MYSQL_DATABASE}" "${H5_DATA_TABLES[@]}" >"${dump_file}"
transform_paths "${dump_file}"
echo " 导出 $(wc -l <"${dump_file}" | tr -d ' ') 行 SQL"
}
init_remote_schema() {
local rds_b64="$1"
echo "==> 在 RDS 初始化 H5 表结构..."
ssh_cmd "cd '${H5_REMOTE_DIR}' && H5_RDS_B64='${rds_b64}' node --input-type=module -e \"
import { createDbPool, initSchema } from './db.mjs';
const cfg = JSON.parse(Buffer.from(process.env.H5_RDS_B64, 'base64').toString('utf8'));
process.env.MYSQL_HOST = cfg.host;
process.env.MYSQL_PORT = String(cfg.port);
process.env.MYSQL_USER = cfg.user;
process.env.MYSQL_PASSWORD = cfg.password;
process.env.MYSQL_DATABASE = cfg.database;
const pool = createDbPool();
await initSchema(pool);
await pool.end();
console.log('schema ok');
\""
}
import_remote_data() {
local rds_b64="$1"
local remote_dump="/tmp/h5_data_sync.sql"
echo "==> 导入数据到 RDS..."
scp -q "${DUMP_FILE}" "${H5_DEPLOY_HOST}:${remote_dump}"
local tables_json tables_b64
tables_json="$(printf '%s\n' "${H5_DATA_TABLES[@]}" | node -e "const fs=require('fs'); process.stdout.write(JSON.stringify(fs.readFileSync(0,'utf8').trim().split('\\n')))")"
tables_b64="$(b64 "${tables_json}")"
ssh_cmd "cd '${H5_REMOTE_DIR}' && H5_RDS_B64='${rds_b64}' H5_DUMP_FILE='${remote_dump}' H5_TABLES_B64='${tables_b64}' node --input-type=module -e \"
import fs from 'node:fs';
import { createDbPool } from './db.mjs';
const cfg = JSON.parse(Buffer.from(process.env.H5_RDS_B64, 'base64').toString('utf8'));
const tables = JSON.parse(Buffer.from(process.env.H5_TABLES_B64, 'base64').toString('utf8'));
process.env.MYSQL_HOST = cfg.host;
process.env.MYSQL_PORT = String(cfg.port);
process.env.MYSQL_USER = cfg.user;
process.env.MYSQL_PASSWORD = cfg.password;
process.env.MYSQL_DATABASE = cfg.database;
const pool = createDbPool();
const sql = fs.readFileSync(process.env.H5_DUMP_FILE, 'utf8');
await pool.query('SET FOREIGN_KEY_CHECKS=0').catch(() => {});
for (const table of tables) {
await pool.query('TRUNCATE TABLE ??', [table]).catch(() => {});
}
const statements = sql
.split(/;\\s*\\n/)
.map(s => s.trim())
.filter(s => s && !/^\\/\\*!|^SET @|^SET @@/i.test(s));
for (const stmt of statements) {
await pool.query(stmt);
}
await pool.query('SET FOREIGN_KEY_CHECKS=1').catch(() => {});
await pool.end();
fs.unlinkSync(process.env.H5_DUMP_FILE);
console.log('import ok');
\""
}
update_remote_env() {
local rds_b64="$1"
echo "==> 更新 105 H5 .envRDS 连接)..."
local admin_user_b64 admin_pass_b64
admin_user_b64="$(b64 "${H5_ADMIN_USERNAME:-admin}")"
admin_pass_b64="$(b64 "${H5_ADMIN_PASSWORD:-}")"
ssh_cmd "H5_RDS_B64='${rds_b64}' H5_ADMIN_USER_B64='${admin_user_b64}' H5_ADMIN_PASS_B64='${admin_pass_b64}' node -e \"
const fs=require('fs');
const cfg=JSON.parse(Buffer.from(process.env.H5_RDS_B64,'base64').toString('utf8'));
const adminUser=Buffer.from(process.env.H5_ADMIN_USER_B64,'base64').toString('utf8');
const adminPass=Buffer.from(process.env.H5_ADMIN_PASS_B64,'base64').toString('utf8');
const envPath='${H5_REMOTE_DIR}/.env';
const lines=fs.existsSync(envPath)?fs.readFileSync(envPath,'utf8').split('\\n'):[];
const updates={
MYSQL_HOST:cfg.host,
MYSQL_PORT:String(cfg.port),
MYSQL_USER:cfg.user,
MYSQL_PASSWORD:cfg.password,
MYSQL_DATABASE:cfg.database,
DATABASE_URL:'mysql://'+encodeURIComponent(cfg.user)+':'+encodeURIComponent(cfg.password)+'@'+cfg.host+':'+cfg.port+'/'+cfg.database,
H5_USERS_ROOT:'/root/tkmind_go/ui/h5/users',
H5_SIGNUP_BALANCE_CENTS:'1000',
H5_PUBLIC_BASE_URL:'https://go.tkmind.cn',
};
if(adminUser) updates.H5_ADMIN_USERNAME=adminUser;
if(adminPass) updates.H5_ADMIN_PASSWORD=adminPass;
const keys=new Set(Object.keys(updates));
const kept=[];
for(const line of lines){
const m=line.match(/^([A-Z0-9_]+)=/);
if(m && keys.has(m[1])) continue;
kept.push(line);
}
while(kept.length && kept[kept.length-1]==='') kept.pop();
for(const [k,v] of Object.entries(updates)) kept.push(k+'='+v);
fs.writeFileSync(envPath, kept.join('\\n')+'\\n');
console.log('env updated');
\""
}
migrate_remote_publish_dir() {
echo "==> 迁移远端 temp → MindSpace(若存在)..."
ssh_cmd "if [[ -d '${H5_REMOTE_DIR}/temp' && ! -d '${H5_REMOTE_DIR}/MindSpace' ]]; then mv '${H5_REMOTE_DIR}/temp' '${H5_REMOTE_DIR}/MindSpace'; fi"
}
sync_user_dirs() {
migrate_remote_publish_dir
echo "==> 同步用户 workspace 目录..."
for user_dir in "${H5_DIR}/MindSpace"/* "${H5_DIR}/users"/*; do
[[ -d "${user_dir}" ]] || continue
base="$(basename "${user_dir}")"
if [[ "${user_dir}" == *"/MindSpace/"* ]]; then
rsync -az "${user_dir}/" "${H5_DEPLOY_HOST}:${H5_REMOTE_DIR}/MindSpace/${base}/"
else
rsync -az "${user_dir}/" "${H5_DEPLOY_HOST}:${H5_REMOTE_DIR}/users/${base}/"
fi
done
}
apply_production_security_baseline() {
local rds_b64="$1"
echo "==> 应用生产安全基线(role=user 默认权限,清除用户级权限覆盖)..."
ssh_cmd "cd '${H5_REMOTE_DIR}' && H5_RDS_B64='${rds_b64}' node --input-type=module -e \"
import { createDbPool } from './db.mjs';
import { applyProductionSecurityBaseline } from './security-baseline.mjs';
const cfg = JSON.parse(Buffer.from(process.env.H5_RDS_B64, 'base64').toString('utf8'));
process.env.MYSQL_HOST = cfg.host;
process.env.MYSQL_PORT = String(cfg.port);
process.env.MYSQL_USER = cfg.user;
process.env.MYSQL_PASSWORD = cfg.password;
process.env.MYSQL_DATABASE = cfg.database;
const pool = createDbPool();
await applyProductionSecurityBaseline(pool);
await pool.end();
console.log('security baseline ok');
\""
}
verify_remote() {
local rds_b64="$1"
echo "==> 验证 RDS 数据与 john 会话策略..."
ssh_cmd "cd '${H5_REMOTE_DIR}' && H5_RDS_B64='${rds_b64}' node --input-type=module -e \"
import { createDbPool } from './db.mjs';
import { createUserAuth } from './user-auth.mjs';
import fs from 'node:fs';
const cfg = JSON.parse(Buffer.from(process.env.H5_RDS_B64, 'base64').toString('utf8'));
process.env.MYSQL_HOST = cfg.host;
process.env.MYSQL_PORT = String(cfg.port);
process.env.MYSQL_USER = cfg.user;
process.env.MYSQL_PASSWORD = cfg.password;
process.env.MYSQL_DATABASE = cfg.database;
for (const line of fs.readFileSync('.env','utf8').split('\\n')) {
const t=line.trim(); if(!t||t.startsWith('#')) continue;
const eq=t.indexOf('='); if(eq<0) continue;
if(!process.env[t.slice(0,eq)]) process.env[t.slice(0,eq)]=t.slice(eq+1);
}
const pool = createDbPool();
const [users] = await pool.query('SELECT username, role FROM h5_users ORDER BY username');
const [usage] = await pool.query('SELECT COUNT(*) AS cnt FROM h5_usage_records');
const [roleShell] = await pool.query(
\\\"SELECT allowed FROM h5_capability_grants WHERE subject_type='role' AND subject_id='user' AND capability_key='shell'\\\"
);
console.log('users:', users.map(u => u.username + '(' + u.role + ')').join(', '));
console.log('usage_records:', usage[0].cnt);
console.log('role shell:', roleShell[0]?.allowed ?? 'default');
const auth = createUserAuth(pool, { usersRoot: process.env.H5_USERS_ROOT, h5Root: process.cwd() });
const [john] = await pool.query(\\\"SELECT id FROM h5_users WHERE username='john'\\\");
if (john[0]) {
const policy = await auth.getAgentSessionPolicy(john[0].id);
const dev = policy.extensionOverrides?.find(e => e.name === 'developer');
console.log('john developer tools:', dev?.available_tools?.join(',') ?? 'none');
console.log('john goose_mode:', policy.gooseMode ?? 'null');
}
await pool.end();
\""
}
echo "======================================"
echo "H5 数据库 → 105 RDS 同步"
echo "参照: ${TKMIND_SHARED_CONFIG}"
echo "目标库: ${H5_RDS_DATABASE}"
echo "======================================"
RDS_JSON="$(read_rds_config)"
RDS_B64="$(b64 "${RDS_JSON}")"
echo "RDS: $(echo "${RDS_JSON}" | node -e "const c=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log(c.host+'/'+c.database)")"
if [[ "${BASELINE_ONLY}" -eq 1 ]]; then
apply_production_security_baseline "${RDS_B64}"
else
DUMP_FILE="$(mktemp /tmp/h5_data_XXXXXX.sql)"
trap 'rm -f "${DUMP_FILE}"' EXIT
if [[ "${DATA_ONLY}" -eq 0 ]]; then
init_remote_schema "${RDS_B64}"
fi
if [[ "${SCHEMA_ONLY}" -eq 0 ]]; then
dump_local_data "${DUMP_FILE}"
import_remote_data "${RDS_B64}"
sync_user_dirs
apply_production_security_baseline "${RDS_B64}"
fi
fi
update_remote_env "${RDS_B64}"
if [[ "${NO_RESTART}" -eq 0 ]]; then
echo "==> 重启 ${H5_SYSTEMD_SERVICE}..."
ssh_cmd "systemctl restart '${H5_SYSTEMD_SERVICE}'"
sleep 2
fi
verify_remote "${RDS_B64}"
ssh_cmd "curl -s http://127.0.0.1:8080/api/status && echo ' ← h5'" || true
echo ""
echo "✅ H5 已连接 RDS 并完成数据同步"