Add TKMind platform extensions, H5/MindSpace stack, and deployment tooling.
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

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>
This commit is contained in:
john
2026-06-14 21:30:20 +08:00
parent e5fd568e01
commit 4e21ca937a
359 changed files with 70658 additions and 56 deletions
+6
View File
@@ -0,0 +1,6 @@
node_modules
dist
.env
*.log
MindSpace
.DS_Store
+23
View File
@@ -0,0 +1,23 @@
.git
.target
target/
node_modules/
.hermit/
deploy/.cargo-linux-amd64/
deploy/.target-linux-amd64/
deploy/artifacts/
ui/h5/MindSpace/
ui/h5/users/
ui/h5/node_modules/
ui/h5/dist/
ui/desktop/out/
*.log
.DS_Store
.env.local
ui/h5/.env
.goosed.pid
.h5.pid
.h5-dev.pid
.plaza.pid
.cloudflared.pid
build-goosed-no-local.exit
+25
View File
@@ -0,0 +1,25 @@
# 与 105Alibaba Cloud Linux 3 / glibc 2.32)兼容的 goosed 构建镜像。
# 勿用 debian bookworm:其 glibc/libstdc++ 过新,产物无法在 105 上运行。
FROM almalinux:8
RUN dnf install -y epel-release && \
/usr/bin/crb enable && \
dnf install -y \
gcc gcc-c++ make cmake \
pkgconf-pkg-config \
openssl-devel \
dbus-devel \
clang llvm \
protobuf protobuf-compiler \
git curl ca-certificates \
&& dnf clean all
ARG RUST_VERSION=1.92.0
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain "${RUST_VERSION}" --profile minimal
ENV PATH="/root/.cargo/bin:${PATH}"
RUN rustc --version
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
WORKDIR /build
+14
View File
@@ -0,0 +1,14 @@
# Cloudflare Tunnel 配置示例
# 1. cloudflared tunnel login
# 2. cloudflared tunnel create goose-local
# 3. 复制本文件到 ~/.cloudflared/config.yml 并替换 <TUNNEL-ID>
# 4. cloudflared tunnel route dns goose-local goo.tkmind.cn
# 5. cloudflared tunnel run goose-local
tunnel: 17325277-5bf8-4a9e-a4be-2aee39e53e45
credentials-file: /Users/john/.cloudflared/17325277-5bf8-4a9e-a4be-2aee39e53e45.json
ingress:
- hostname: goo.tkmind.cn
service: http://127.0.0.1:8080
- service: http_status:404
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
ENGINE="${1:-}"
PROJECT_DIR="${2:-}"
TASK="${3:-}"
DRY_RUN="${4:-}"
AIDER_BIN="${AIDER_BIN:-${GOOSE_AIDER_BIN:-/root/aider/.venv/bin/aider}}"
if [[ -z "$ENGINE" || -z "$PROJECT_DIR" || -z "$TASK" ]]; then
echo "Usage: coding_router.sh <aider|goose|auto> <project_dir> <task> [--dry-run]"
exit 1
fi
if [[ ! -d "$PROJECT_DIR" ]]; then
echo "Project dir not found: $PROJECT_DIR"
exit 1
fi
choose_engine() {
case "$ENGINE" in
aider) echo "aider" ;;
goose) echo "goose" ;;
auto)
if echo "$TASK" | grep -qE '修复|bug|Bug|BUG|重构|跨文件|实现|新增|改造|兼容|报错|测试失败|commit|提交|代码修改|开发|feature|refactor|fix'; then
echo "aider"
else
echo "goose"
fi
;;
*)
echo "Unknown engine: $ENGINE"
exit 1
;;
esac
}
SELECTED_ENGINE="$(choose_engine)"
echo "Selected engine: $SELECTED_ENGINE"
if [[ "$DRY_RUN" == "--dry-run" ]]; then
exit 0
fi
cd "$PROJECT_DIR"
echo "===== git status before ====="
git status --short || true
case "$SELECTED_ENGINE" in
aider)
if [[ ! -x "$AIDER_BIN" ]]; then
echo "Aider not executable: $AIDER_BIN"
exit 1
fi
"$AIDER_BIN" --yes-always --message "$TASK"
echo "===== git status after ====="
git status --short || true
;;
goose)
echo "Goose developer tools handle this task in-session: $TASK"
exit 2
;;
esac
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env bash
# 本地编译 goosedlinux/amd64)并上传到 105,不在生产机 cargo build。
#
# Mac / 非 x86_64 LinuxDocker --platform linux/amd64
# x86_64 Linux:本机 cargo,产物写入 deploy/.target-linux-amd64/
#
# 用法:
# ./deploy/deploy-goosed-105.sh
# ./deploy/deploy-goosed-105.sh --skip-build # 只上传已有二进制
# ./deploy/deploy-goosed-105.sh --skip-source # 不上传 crates 源码
# ./deploy/deploy-goosed-105.sh --no-restart
# ./rsync_to_server.sh goosed
# ./rsync_to_server.sh all
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DEPLOY_ENV="${ROOT}/deploy/h5-105.env"
BUILDER_DOCKERFILE="${ROOT}/deploy/Dockerfile.goosed-build"
BUILDER_IMAGE="${GOOSED_BUILDER_IMAGE:-goose-goosed-builder:1.92-al8}"
CARGO_HOME="${ROOT}/deploy/.cargo-linux-amd64"
CARGO_HOME_CONTAINER="/cache/cargo"
TARGET_DIR="${ROOT}/deploy/.target-linux-amd64"
LOCAL_BIN="${TARGET_DIR}/release/goosed"
H5_DEPLOY_HOST="${H5_DEPLOY_HOST:-root@120.26.184.105}"
GOOSED_REMOTE_DIR="${GOOSED_REMOTE_DIR:-/root/tkmind_go}"
GOOSED_REMOTE_BIN="${GOOSED_REMOTE_BIN:-${GOOSED_REMOTE_DIR}/target/release/goosed}"
GOOSED_SYSTEMD_SERVICE="${GOOSED_SYSTEMD_SERVICE:-goosed-tkmind-go}"
SKIP_BUILD=0
SKIP_SOURCE=0
NO_RESTART=0
for arg in "$@"; do
case "$arg" in
--skip-build) SKIP_BUILD=1 ;;
--skip-source) SKIP_SOURCE=1 ;;
--no-restart) NO_RESTART=1 ;;
-h|--help)
sed -n '2,13p' "$0"
exit 0
;;
*)
echo "未知参数: $arg(可用 --skip-build / --skip-source / --no-restart" >&2
exit 1
;;
esac
done
if [[ -f "${DEPLOY_ENV}" ]]; then
set -a
# shellcheck disable=SC1090
source "${DEPLOY_ENV}"
set +a
fi
ssh_cmd() {
ssh -o ConnectTimeout=15 -o BatchMode=yes "${H5_DEPLOY_HOST}" "$@"
}
use_docker_build() {
[[ "$(uname -s)" != "Linux" || "$(uname -m)" != "x86_64" ]]
}
ensure_builder_image() {
if docker image inspect "${BUILDER_IMAGE}" >/dev/null 2>&1; then
return
fi
echo "==> 构建 Docker 编译镜像 ${BUILDER_IMAGE}AlmaLinux 8,兼容 105 glibc 2.32..."
docker build --platform linux/amd64 -f "${BUILDER_DOCKERFILE}" -t "${BUILDER_IMAGE}" "${ROOT}/deploy"
}
build_with_docker() {
ensure_builder_image
mkdir -p "${CARGO_HOME}" "${TARGET_DIR}"
echo "==> Docker 编译 goosed (linux/amd64) ..."
docker run --rm \
--platform linux/amd64 \
-v "${ROOT}:/build:ro" \
-v "${CARGO_HOME}:${CARGO_HOME_CONTAINER}" \
-v "${TARGET_DIR}:/build/target" \
-e CARGO_HOME="${CARGO_HOME_CONTAINER}" \
-e CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
-w /build \
"${BUILDER_IMAGE}" \
cargo build --release -p goose-server --bin goosed
}
build_native() {
mkdir -p "${TARGET_DIR}"
echo "==> 本机编译 goosed (linux/amd64) ..."
(
cd "${ROOT}"
CARGO_TARGET_DIR="${TARGET_DIR}" \
cargo build --release -p goose-server --bin goosed
)
}
build_goosed() {
if use_docker_build; then
if ! command -v docker >/dev/null 2>&1; then
echo "错误: 需要 Docker 才能在 $(uname -s)/$(uname -m) 上编译 linux/amd64 goosed" >&2
exit 1
fi
build_with_docker
else
if ! command -v cargo >/dev/null 2>&1; then
echo "错误: 未找到 cargo" >&2
exit 1
fi
build_native
fi
}
sync_source() {
echo "==> rsync Rust 源码(不含 target/node_modules..."
rsync -az \
--exclude target \
--exclude node_modules \
--exclude ui/h5/MindSpace \
--exclude ui/h5/node_modules \
--exclude ui/h5/dist \
--exclude .git \
--exclude deploy/.cargo-linux-amd64 \
--exclude deploy/.target-linux-amd64 \
"${ROOT}/crates" "${ROOT}/Cargo.toml" "${ROOT}/Cargo.lock" \
"${H5_DEPLOY_HOST}:${GOOSED_REMOTE_DIR}/"
}
upload_binary() {
if [[ ! -x "${LOCAL_BIN}" ]]; then
echo "错误: 未找到可执行文件 ${LOCAL_BIN},请先编译或去掉 --skip-build" >&2
exit 1
fi
echo "==> 上传 goosed → ${H5_DEPLOY_HOST}:${GOOSED_REMOTE_BIN}"
ssh_cmd "mkdir -p '$(dirname "${GOOSED_REMOTE_BIN}")'"
rsync -az "${LOCAL_BIN}" "${H5_DEPLOY_HOST}:${GOOSED_REMOTE_BIN}.new"
ssh_cmd "install -m 755 '${GOOSED_REMOTE_BIN}.new' '${GOOSED_REMOTE_BIN}' && rm -f '${GOOSED_REMOTE_BIN}.new'"
}
echo "======================================"
echo "goosed → 105 部署(本地编译上传)"
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "本地二进制: ${LOCAL_BIN}"
echo "远端: ${H5_DEPLOY_HOST}:${GOOSED_REMOTE_BIN}"
echo "编译方式: $(use_docker_build && echo 'docker linux/amd64' || echo 'native cargo')"
echo "======================================"
if [[ "${SKIP_BUILD}" -eq 0 ]]; then
build_goosed
else
echo "==> 跳过编译 (--skip-build)"
fi
if [[ "${SKIP_SOURCE}" -eq 0 ]]; then
sync_source
else
echo "==> 跳过源码同步 (--skip-source)"
fi
upload_binary
if [[ "${NO_RESTART}" -eq 0 ]]; then
echo "==> 重启 ${GOOSED_SYSTEMD_SERVICE} ..."
ssh_cmd "systemctl restart '${GOOSED_SYSTEMD_SERVICE}'"
sleep 2
fi
echo ""
echo "=== 健康检查 ==="
ssh_cmd "systemctl is-active '${GOOSED_SYSTEMD_SERVICE}' && echo '${GOOSED_SYSTEMD_SERVICE}: active' || echo '${GOOSED_SYSTEMD_SERVICE}: inactive'"
ssh_cmd "curl -sk 'https://127.0.0.1:3000/status' && echo ' ← goosed'" || true
ssh_cmd "'${GOOSED_REMOTE_BIN}' --version 2>/dev/null || file '${GOOSED_REMOTE_BIN}'" || true
file "${LOCAL_BIN}" 2>/dev/null || true
echo ""
echo "✅ goosed 已发布"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# 兼容入口,实际逻辑在根目录 rsync_to_server.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
exec "${ROOT}/rsync_to_server.sh" h5 "$@"
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
# 生产发布脚本 — 仅当用户明确要求「发布生产」时由人工/Agent 执行
# 本地 H5 测试通过后,一键构建并发布到本机 Cloudflare 隧道(goo.tkmind.cn
# 105 服务器生产环境请用: ./deploy/deploy-h5-105.sh → https://go.tkmind.cn
#
# 典型流程:
# 1. cd ui/h5 && pnpm dev # 开发热更新 (5173)
# 2. cd ui/h5 && pnpm run test:local # 本地验证生产构建 (8080)
# 3. ./deploy/deploy-h5-prod.sh # 发布到本机隧道 goo.tkmind.cn
#
# 选项:
# --skip-test 跳过 auth 单元测试
# --skip-tunnel 不检查/启动 cloudflared
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
H5_DIR="${ROOT}/ui/h5"
H5_PORT="${H5_PORT:-8080}"
LOG_DIR="${ROOT}"
PROD_URL="${H5_PROD_URL:-https://goo.tkmind.cn}"
TUNNEL_NAME="${CLOUDFLARED_TUNNEL:-goose-local}"
SKIP_TEST=0
SKIP_TUNNEL=0
for arg in "$@"; do
case "$arg" in
--skip-test) SKIP_TEST=1 ;;
--skip-tunnel) SKIP_TUNNEL=1 ;;
-h|--help)
sed -n '2,12p' "$0"
exit 0
;;
*)
echo "未知参数: $arg(可用 --skip-test / --skip-tunnel" >&2
exit 1
;;
esac
done
load_env() {
if [[ -f "${ROOT}/.env.local" ]]; then
set -a
# shellcheck disable=SC1091
source "${ROOT}/.env.local"
set +a
fi
if [[ -f "${H5_DIR}/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "${H5_DIR}/.env"
set +a
fi
}
kill_port() {
local port="$1"
if command -v lsof >/dev/null 2>&1; then
local pids
pids="$(lsof -ti TCP:"${port}" -s TCP:LISTEN 2>/dev/null || true)"
if [[ -n "${pids}" ]]; then
echo "==> 释放 :${port} (PID ${pids})"
kill ${pids} 2>/dev/null || true
sleep 1
kill -9 ${pids} 2>/dev/null || true
fi
fi
}
pkg_run() {
cd "${H5_DIR}"
if command -v pnpm >/dev/null 2>&1; then
pnpm "$@"
elif command -v npm >/dev/null 2>&1; then
npm "$@"
else
echo "错误: 需要 pnpm 或 npm" >&2
exit 1
fi
}
load_env
export TKMIND_API_TARGET="${TKMIND_API_TARGET:-${GOOSE_API_TARGET:-https://127.0.0.1:${GOOSE_PORT:-18006}}}"
export TKMIND_SERVER__SECRET_KEY="${TKMIND_SERVER__SECRET_KEY:-${GOOSE_SERVER__SECRET_KEY:-local-dev-secret}}"
export H5_PORT
export VITE_TKMIND_WORKING_DIR="${VITE_TKMIND_WORKING_DIR:-${ROOT}}"
echo "==> H5 生产发布"
echo " 工作目录: ${VITE_TKMIND_WORKING_DIR}"
echo " API 代理: ${TKMIND_API_TARGET}"
echo " 本地端口: ${H5_PORT}"
echo " 公网地址: ${PROD_URL}"
if ! curl -sk "${TKMIND_API_TARGET}/status" >/dev/null 2>&1; then
echo "⚠️ goosed 未运行,正在启动..."
"${ROOT}/local_restart.sh"
fi
cd "${H5_DIR}"
if [[ ! -d node_modules ]]; then
echo "==> 安装依赖..."
pkg_run install
fi
if [[ "${SKIP_TEST}" -eq 0 ]]; then
echo "==> 运行测试..."
pkg_run test
fi
echo "==> 构建生产包..."
pkg_run run build
kill_port "${H5_PORT}"
if [[ -f "${ROOT}/.h5.pid" ]]; then
old_pid="$(<"${ROOT}/.h5.pid")"
if kill -0 "${old_pid}" 2>/dev/null; then
kill "${old_pid}" 2>/dev/null || true
fi
rm -f "${ROOT}/.h5.pid"
fi
echo "==> 重启 H5 服务 @ http://127.0.0.1:${H5_PORT}"
nohup node server.mjs >"${LOG_DIR}/h5.log" 2>&1 &
echo $! >"${ROOT}/.h5.pid"
sleep 1
if [[ "${SKIP_TUNNEL}" -eq 0 ]]; then
if pgrep -f "cloudflared tunnel run ${TUNNEL_NAME}" >/dev/null 2>&1; then
echo "✅ cloudflared 已在运行"
elif command -v cloudflared >/dev/null 2>&1 && [[ -f "${HOME}/.cloudflared/config.yml" ]]; then
kill_port 20241
echo "==> 启动 Cloudflare Tunnel (${PROD_URL})"
nohup cloudflared tunnel run "${TUNNEL_NAME}" >"${LOG_DIR}/cloudflared.log" 2>&1 &
echo $! >"${ROOT}/.cloudflared.pid"
sleep 3
else
echo "⚠️ 未检测到 cloudflared,仅本地可用"
fi
fi
echo ""
echo "=== 健康检查 ==="
curl -sk "${TKMIND_API_TARGET}/status" && echo " ← goosed"
curl -s "http://127.0.0.1:${H5_PORT}/api/status" && echo " ← h5 本地"
if [[ "${SKIP_TUNNEL}" -eq 0 ]] && command -v cloudflared >/dev/null 2>&1; then
curl -s --max-time 15 -o /dev/null -w "${PROD_URL} → %{http_code}\n" "${PROD_URL}/" || true
fi
echo ""
echo "✅ H5 已发布"
echo " 本地: http://127.0.0.1:${H5_PORT}"
echo " 公网: ${PROD_URL}"
echo " 日志: ${LOG_DIR}/h5.log"
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# 局域网 Mac 服务器部署:rsync 项目 → 192.168.71.100:/Users/john/Project/tkmind_go
#
# 前置:本机可免密 ssh 到目标机
# ssh-copy-id john@192.168.71.100
#
# 用法:
# ./deploy/deploy-lan-100.sh
# ./deploy/deploy-lan-100.sh --skip-build
# ./deploy/deploy-lan-100.sh --skip-goosed
# ./deploy/deploy-lan-100.sh --skip-h5
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DEPLOY_ENV="${ROOT}/deploy/lan-100.env"
EXCLUDE_FILE="${ROOT}/deploy/.rsync-exclude-project"
LAN_DEPLOY_HOST="${LAN_DEPLOY_HOST:-john@192.168.71.100}"
LAN_REMOTE_DIR="${LAN_REMOTE_DIR:-/Users/john/Project/tkmind_go}"
GOOSED_PORT="${GOOSED_PORT:-18006}"
H5_PORT="${H5_PORT:-8080}"
SKIP_BUILD=0
SKIP_GOOSED=0
SKIP_H5=0
for arg in "$@"; do
case "$arg" in
--skip-build) SKIP_BUILD=1 ;;
--skip-goosed) SKIP_GOOSED=1 ;;
--skip-h5) SKIP_H5=1 ;;
-h|--help)
sed -n '2,12p' "$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
ssh_cmd() {
ssh -o ConnectTimeout=15 -o BatchMode=yes "${LAN_DEPLOY_HOST}" "$@"
}
require_ssh() {
if ! ssh_cmd 'echo ok' >/dev/null 2>&1; then
echo "❌ 无法 SSH 到 ${LAN_DEPLOY_HOST}" >&2
echo "请在本机终端执行一次(输入服务器密码):" >&2
echo " ssh-copy-id ${LAN_DEPLOY_HOST}" >&2
exit 1
fi
}
echo "======================================"
echo "TKMind → 局域网部署"
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "本地: ${ROOT}"
echo "远端: ${LAN_DEPLOY_HOST}:${LAN_REMOTE_DIR}"
echo "======================================"
require_ssh
echo "==> 创建远端目录..."
ssh_cmd "mkdir -p '${LAN_REMOTE_DIR}'"
echo "==> rsync 同步项目..."
rsync -az --delete --exclude-from="${EXCLUDE_FILE}" \
"${ROOT}/" \
"${LAN_DEPLOY_HOST}:${LAN_REMOTE_DIR}/"
echo "==> 写入远端环境模板(不覆盖已有 .env)..."
ssh_cmd "test -f '${LAN_REMOTE_DIR}/.env.local' || cat > '${LAN_REMOTE_DIR}/.env.local' <<'EOF'
GOOSE_PORT=${GOOSED_PORT}
GOOSE_HOST=127.0.0.1
GOOSE_SERVER__SECRET_KEY=local-dev-secret
GOOSE_CODING_ROUTER=${LAN_REMOTE_DIR}/deploy/coding_router.sh
GOOSE_AIDER_BIN=/Users/john/PycharmProjects/aider/.venv/bin/aider
EOF"
ssh_cmd "test -f '${LAN_REMOTE_DIR}/ui/h5/.env' || cp '${LAN_REMOTE_DIR}/ui/h5/.env.example' '${LAN_REMOTE_DIR}/ui/h5/.env'"
if [[ "${SKIP_BUILD}" -eq 0 ]]; then
if [[ "${SKIP_GOOSED}" -eq 0 ]]; then
echo "==> 远端编译 goosed..."
ssh_cmd "cd '${LAN_REMOTE_DIR}' && (command -v cargo >/dev/null 2>&1 || source bin/activate-hermit 2>/dev/null || true) && cargo build -p goose-server --bin goosed"
fi
if [[ "${SKIP_H5}" -eq 0 ]]; then
echo "==> 远端构建 H5..."
ssh_cmd "cd '${LAN_REMOTE_DIR}/ui/h5' && (command -v pnpm >/dev/null 2>&1 && pnpm install && pnpm run build || (npm install && npm run build))"
fi
fi
if [[ "${SKIP_GOOSED}" -eq 0 ]]; then
echo "==> 重启远端 goosed..."
ssh_cmd "cd '${LAN_REMOTE_DIR}' && ./local_restart.sh" || true
fi
if [[ "${SKIP_H5}" -eq 0 ]]; then
echo "==> 重启远端 H5..."
ssh_cmd "cd '${LAN_REMOTE_DIR}/ui/h5' && (lsof -ti TCP:${H5_PORT} -s TCP:LISTEN | xargs kill 2>/dev/null || true); sleep 1; nohup node server.mjs >> '${LAN_REMOTE_DIR}/ui/h5/h5.log' 2>&1 & echo \$! > '${LAN_REMOTE_DIR}/ui/h5/.h5.pid'"
fi
echo ""
echo "=== 健康检查 ==="
ssh_cmd "curl -sk 'https://127.0.0.1:${GOOSED_PORT}/status' && echo ' ← goosed'" || true
ssh_cmd "curl -s 'http://127.0.0.1:${H5_PORT}/api/status' && echo ' ← h5'" || true
echo ""
echo "✅ 已同步到 ${LAN_DEPLOY_HOST}:${LAN_REMOTE_DIR}"
echo " H5: http://192.168.71.100:${H5_PORT}/"
echo " goosed: https://127.0.0.1:${GOOSED_PORT}/ (仅服务器本机)"
+66
View File
@@ -0,0 +1,66 @@
upstream goose_h5 {
server 127.0.0.1:8080;
keepalive 16;
}
upstream legacy_tkmind_h5 {
server 127.0.0.1:18800;
keepalive 16;
}
server {
server_name go.tkmind.cn;
# 所有请求(含 /MindSpace/<username>/;旧 /temp/ 由 server.mjs 301 重定向)走 H5
location / {
proxy_pass http://goose_h5;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
proxy_send_timeout 86400;
proxy_buffering off;
}
listen [::]:443 ssl ipv6only=on;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/go.tkmind.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/go.tkmind.cn/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
listen 80;
listen [::]:80;
server_name goo.tkmind.cn;
location / {
proxy_pass http://legacy_tkmind_h5;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
proxy_send_timeout 86400;
proxy_buffering off;
}
}
server {
if ($host = go.tkmind.cn) {
return 301 https://$host$request_uri;
}
listen 80;
listen [::]:80;
server_name go.tkmind.cn;
return 404;
}
+37
View File
@@ -0,0 +1,37 @@
# 105 服务器 H5 部署配置(复制为 deploy/h5-105.env 后按需修改)
H5_DEPLOY_HOST=root@120.26.184.105
H5_REMOTE_DIR=/root/tkmind_go/ui/h5
H5_PROD_URL=https://go.tkmind.cn
# 若服务器尚未重命名 systemd unit,可设为实际服务名
H5_SYSTEMD_SERVICE=goose-h5
GOOSED_SYSTEMD_SERVICE=goosed-tkmind-go
TKMIND_API_SYSTEMD_SERVICE=goosed-tkmind-go
# goosed 本地编译上传(Mac 用 Docker linux/amd64,不在 105 上 cargo build):
# ./deploy/deploy-goosed-105.sh
# ./rsync_to_server.sh goosed
# ./rsync_to_server.sh all # H5 + goosed 一起发
# GOOSED_REMOTE_DIR=/root/tkmind_go
# GOOSED_REMOTE_BIN=/root/tkmind_go/target/release/goosed
# H5 MySQL 使用与 tkmind 相同的阿里云 RDS(参照 /root/tkmind/shared/config/local.json
# 一键建表 + 导数据 + 更新 .env:
# ./deploy/sync-h5-db-105.sh
# H5_RDS_DATABASE=goose
# 多用户系统(由 sync-h5-db-105.sh 写入服务器 ui/h5/.env
# H5_USERS_ROOT=/root/tkmind_go/ui/h5/users
# H5_SIGNUP_BALANCE_CENTS=1000
# H5_ADMIN_USERNAME=admin
# H5_ADMIN_PASSWORD=your-admin-password
# 微信支付自助充值(配置后写入服务器 ui/h5/.env)
# H5_WECHAT_PAY_ENABLED=1
# H5_WECHAT_APP_ID=wx...
# H5_WECHAT_MCH_ID=...
# H5_WECHAT_API_V3_KEY=...
# H5_WECHAT_SERIAL_NO=...
# H5_WECHAT_PRIVATE_KEY_PATH=/root/tkmind_go/secrets/wechat_apiclient_key.pem
# H5_WECHAT_PLATFORM_CERT_PATH=/root/tkmind_go/secrets/wechat_platform.pem
# H5_WECHAT_NOTIFY_URL=https://go.tkmind.cn/webhooks/wechat-pay/notify
# H5_PUBLIC_BASE_URL=https://go.tkmind.cn
+12
View File
@@ -0,0 +1,12 @@
# 局域网 192.168.71.100 部署配置(复制为 deploy/lan-100.env 后按需修改)
LAN_DEPLOY_HOST=john@192.168.71.100
LAN_REMOTE_DIR=/Users/john/Project/tkmind_go
# 远端 goosed / H5 端口(与本地 port.conf 保持一致即可)
GOOSED_PORT=18006
H5_PORT=8080
# 首次部署后可在远端自行维护 ui/h5/.env 与 .env.local,同步脚本不会覆盖已有 .env
# LAN_SKIP_BUILD=1
# LAN_SKIP_GOOSED=1
# LAN_SKIP_H5=1
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
H5_DIR="${ROOT}/ui/h5"
H5_PORT="${H5_PORT:-8080}"
if [[ -f "${ROOT}/.env.local" ]]; then
set -a
# shellcheck disable=SC1091
source "${ROOT}/.env.local"
set +a
fi
export GOOSE_API_TARGET="${GOOSE_API_TARGET:-https://127.0.0.1:${GOOSE_PORT:-18006}}"
export GOOSE_SERVER__SECRET_KEY="${GOOSE_SERVER__SECRET_KEY:-local-dev-secret}"
export H5_PORT
export VITE_GOOSE_WORKING_DIR="${VITE_GOOSE_WORKING_DIR:-${ROOT}}"
echo "==> 检查 goosed..."
if ! curl -sk "${GOOSE_API_TARGET}/status" >/dev/null 2>&1; then
echo "goosed 未运行,正在启动..."
"${ROOT}/local_restart.sh"
fi
echo "==> 构建 H5..."
cd "${H5_DIR}"
if [[ ! -d node_modules ]]; then
pnpm install
fi
pnpm run build
echo "==> 启动 H5 服务 @ http://127.0.0.1:${H5_PORT}"
node server.mjs &
H5_PID=$!
echo "${H5_PID}" > "${ROOT}/.h5.pid"
if command -v cloudflared >/dev/null 2>&1 && [[ -f "${HOME}/.cloudflared/config.yml" ]]; then
echo "==> 启动 Cloudflare Tunnel (goo.tkmind.cn)"
cloudflared tunnel run goose-local &
CF_PID=$!
echo "${CF_PID}" > "${ROOT}/.cloudflared.pid"
echo ""
echo "✅ 已启动"
echo " 本地: http://127.0.0.1:${H5_PORT}"
echo " 公网: https://goo.tkmind.cn"
else
echo ""
echo "✅ H5 已启动: http://127.0.0.1:${H5_PORT}"
echo "️ 未检测到 cloudflared 配置,请参考 deploy/cloudflared/config.yml.example"
fi
+334
View File
@@ -0,0 +1,334 @@
#!/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 并完成数据同步"