Merge remote-tracking branch 'origin/master' into codex/ddd
# Conflicts: # docs/technical/README.md # docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md # docs/technical/SPACETIMEDB_TABLE_CATALOG.md # scripts/generate-spacetime-bindings.mjs # server-rs/crates/api-server/src/app.rs # server-rs/crates/api-server/src/assets.rs # server-rs/crates/api-server/src/big_fish.rs # server-rs/crates/api-server/src/custom_world_ai.rs # server-rs/crates/api-server/src/llm.rs # server-rs/crates/api-server/src/main.rs # server-rs/crates/api-server/src/puzzle.rs # server-rs/crates/api-server/src/runtime_profile.rs # server-rs/crates/api-server/src/runtime_story/compat/ai.rs # server-rs/crates/api-server/src/runtime_story/compat/npc_actions.rs # server-rs/crates/api-server/src/runtime_story/compat/presentation.rs # server-rs/crates/api-server/src/runtime_story/compat/tests.rs # server-rs/crates/api-server/src/state.rs # server-rs/crates/module-auth/src/lib.rs # server-rs/crates/module-big-fish/src/lib.rs # server-rs/crates/module-custom-world/src/lib.rs # server-rs/crates/module-puzzle/src/lib.rs # server-rs/crates/module-runtime/src/lib.rs # server-rs/crates/spacetime-client/src/big_fish.rs # server-rs/crates/spacetime-client/src/lib.rs # server-rs/crates/spacetime-client/src/mapper.rs # server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_redeem_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_redeem_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/append_ai_text_chunk_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/attach_ai_result_reference_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/authorize_database_migration_operator_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_type.rs # server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/cancel_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/clear_platform_browse_history_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/compile_custom_world_published_profile_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/compile_puzzle_agent_draft_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/complete_ai_stage_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/complete_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/confirm_asset_object_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/consume_profile_wallet_points_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/continue_story_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_battle_state_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_big_fish_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_custom_world_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_profile_recharge_order_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_puzzle_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_big_fish_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_custom_world_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_puzzle_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_runtime_snapshot_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/drag_puzzle_piece_or_group_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/execute_custom_world_agent_action_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/export_auth_store_snapshot_from_tables_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/export_database_migration_to_file_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/fail_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/finalize_big_fish_agent_message_turn_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/finalize_custom_world_agent_message_turn_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/finalize_puzzle_agent_message_turn_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/generate_big_fish_asset_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_auth_store_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_battle_state_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_chapter_progression_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_card_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_operation_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_gallery_detail_by_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_gallery_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_library_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_player_progression_or_default_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_dashboard_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_play_stats_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_recharge_center_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_referral_invite_center_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_run_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_runtime_inventory_state_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_runtime_setting_or_default_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_runtime_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_story_session_state_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/grant_player_progression_experience_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/import_auth_store_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/import_database_migration_from_file_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/import_database_migration_incremental_from_file_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_asset_history_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_big_fish_works_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_gallery_entries_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_profiles_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_works_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_platform_browse_history_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_profile_save_archives_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_profile_wallet_ledger_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_gallery_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_works_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/mod.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_big_fish_game_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_custom_world_world_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_puzzle_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/record_big_fish_play_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_referral_invite_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_reward_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/refund_profile_wallet_points_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_combat_action_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_battle_interaction_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_interaction_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_social_action_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_treasure_interaction_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resume_profile_save_archive_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/revoke_database_migration_operator_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/save_puzzle_generated_images_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/select_puzzle_cover_image_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/start_puzzle_run_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_message_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_custom_world_agent_message_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_agent_message_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_leaderboard_entry_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/swap_puzzle_pieces_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/unpublish_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/update_puzzle_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_auth_store_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_chapter_progression_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_custom_world_agent_operation_progress_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_npc_state_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_platform_browse_history_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_runtime_setting_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_runtime_snapshot_and_return_procedure.rs # server-rs/crates/spacetime-module/src/auth/procedures.rs # server-rs/crates/spacetime-module/src/custom_world/mod.rs # server-rs/crates/spacetime-module/src/lib.rs # server-rs/crates/spacetime-module/src/migration.rs # server-rs/crates/spacetime-module/src/puzzle.rs # server-rs/crates/spacetime-module/src/runtime/profile.rs # src/components/platform-entry/PlatformEntryFlowShellImpl.tsx # src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx # src/services/aiService.ts # src/services/puzzle-runtime/puzzleRuntimeClient.ts
This commit is contained in:
@@ -71,6 +71,31 @@ normalize_env_file() {
|
||||
cp "${temp_file}" "${env_file}"
|
||||
}
|
||||
|
||||
write_env_override() {
|
||||
local env_file="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local temp_file="${env_file}.tmp.$$"
|
||||
|
||||
mkdir -p "$(dirname "${env_file}")"
|
||||
if [[ -f "${env_file}" ]]; then
|
||||
# 发布包参数是本次构建的权威值,必须覆盖从 Jenkins 工作区复制进来的旧 .env.local。
|
||||
awk -v target_key="${key}" '
|
||||
BEGIN {
|
||||
pattern = "^[[:space:]]*(export[[:space:]]+)?" target_key "="
|
||||
}
|
||||
$0 !~ pattern {
|
||||
print
|
||||
}
|
||||
' "${env_file}" >"${temp_file}"
|
||||
else
|
||||
: >"${temp_file}"
|
||||
fi
|
||||
|
||||
printf "%s=%s\n" "${key}" "${value}" >>"${temp_file}"
|
||||
cp "${temp_file}" "${env_file}"
|
||||
}
|
||||
|
||||
copy_optional_file() {
|
||||
local source_path="$1"
|
||||
local target_path_a="$2"
|
||||
@@ -166,7 +191,7 @@ BUILD_NAME="$(date +%Y%m%d-%H%M%S)"
|
||||
DATABASE="xushi-p4wfr"
|
||||
API_HOST="127.0.0.1"
|
||||
API_PORT="8082"
|
||||
WEB_HOST="0.0.0.0"
|
||||
WEB_HOST="127.0.0.1"
|
||||
WEB_PORT="25001"
|
||||
SPACETIME_HOST="127.0.0.1"
|
||||
SPACETIME_PORT="3101"
|
||||
@@ -270,6 +295,13 @@ if [[ ! "${BUILD_NAME}" =~ ^[0-9A-Za-z._-]+$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "${DATABASE}" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
|
||||
echo "[deploy:rust] --database 必须匹配 SpacetimeDB 数据库名规则 ^[a-z0-9]+(-[a-z0-9]+)*$,只能使用小写字母、数字,并用单个短横线分隔: ${DATABASE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[deploy:rust] SpacetimeDB 发布数据库: ${DATABASE}"
|
||||
|
||||
if [[ "${SKIP_SPACETIME_BUILD}" -eq 1 && "${MIGRATION_BOOTSTRAP_SECRET_MODE}" != "disabled" ]]; then
|
||||
echo "[deploy:rust] --skip-spacetime-build 无法把迁移引导密钥注入 wasm。" >&2
|
||||
echo "[deploy:rust] 请移除 --skip-spacetime-build,或同时传 --no-migration-bootstrap-secret。" >&2
|
||||
@@ -278,6 +310,7 @@ fi
|
||||
|
||||
TARGET_DIR="${BUILD_ROOT}/${BUILD_NAME}"
|
||||
WEB_DIR="${TARGET_DIR}/web"
|
||||
ADMIN_WEB_DIR="${WEB_DIR}/admin"
|
||||
API_BINARY_SOURCE="${SERVER_RS_DIR}/target/x86_64-unknown-linux-gnu/release/api-server"
|
||||
WASM_SOURCE="${SERVER_RS_DIR}/target/wasm32-unknown-unknown/release/spacetime_module.wasm"
|
||||
|
||||
@@ -323,6 +356,8 @@ echo "[deploy:rust] 发布包目录: ${TARGET_DIR}"
|
||||
|
||||
copy_optional_file "${REPO_ROOT}/.env" "${TARGET_DIR}/.env" "${WEB_DIR}/.env" ".env"
|
||||
copy_optional_file "${REPO_ROOT}/.env.local" "${TARGET_DIR}/.env.local" "${WEB_DIR}/.env.local" ".env.local"
|
||||
write_env_override "${TARGET_DIR}/.env.local" "GENARRATIVE_SPACETIME_DATABASE" "${DATABASE}"
|
||||
write_env_override "${WEB_DIR}/.env.local" "GENARRATIVE_SPACETIME_DATABASE" "${DATABASE}"
|
||||
|
||||
if [[ "${SKIP_WEB_BUILD}" -ne 1 ]]; then
|
||||
echo "[deploy:rust] 构建 Vite release -> ${WEB_DIR}"
|
||||
@@ -330,6 +365,12 @@ if [[ "${SKIP_WEB_BUILD}" -ne 1 ]]; then
|
||||
cd "${REPO_ROOT}"
|
||||
node scripts/vite-cli.mjs build --outDir "${WEB_DIR}" --emptyOutDir
|
||||
)
|
||||
|
||||
echo "[deploy:rust] 构建后台 Vite release -> ${ADMIN_WEB_DIR}"
|
||||
(
|
||||
cd "${REPO_ROOT}"
|
||||
MSYS2_ARG_CONV_EXCL="--base=" node scripts/admin-web-build.mjs build --base=/admin/ --outDir "${ADMIN_WEB_DIR}" --emptyOutDir
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_API_BUILD}" -ne 1 ]]; then
|
||||
@@ -366,6 +407,19 @@ if [[ "${MIGRATION_BOOTSTRAP_SECRET_MODE}" != "disabled" ]]; then
|
||||
chmod 600 "${TARGET_DIR}/migration-bootstrap-secret.txt"
|
||||
fi
|
||||
|
||||
mkdir -p "${TARGET_DIR}/scripts"
|
||||
for migration_script in \
|
||||
spacetime-migration-common.mjs \
|
||||
spacetime-export-migration-json.mjs \
|
||||
spacetime-import-migration-json.mjs \
|
||||
spacetime-authorize-migration-operator.mjs \
|
||||
spacetime-revoke-migration-operator.mjs; do
|
||||
copy_required_file \
|
||||
"${SCRIPT_DIR}/${migration_script}" \
|
||||
"${TARGET_DIR}/scripts/${migration_script}" \
|
||||
"SpacetimeDB 迁移脚本 ${migration_script}"
|
||||
done
|
||||
|
||||
cat >"${TARGET_DIR}/web-server.mjs" <<'WEB_SERVER'
|
||||
import http from 'node:http';
|
||||
import fs from 'node:fs';
|
||||
@@ -374,11 +428,14 @@ import {fileURLToPath} from 'node:url';
|
||||
|
||||
const releaseDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const webRoot = path.join(releaseDir, 'web');
|
||||
const webHost = process.env.GENARRATIVE_WEB_HOST || '0.0.0.0';
|
||||
const adminWebRoot = path.join(webRoot, 'admin');
|
||||
const webHost = process.env.GENARRATIVE_WEB_HOST || '127.0.0.1';
|
||||
const webPort = Number(process.env.GENARRATIVE_WEB_PORT || '3000');
|
||||
const apiTarget = new URL(process.env.GENARRATIVE_API_TARGET || 'http://127.0.0.1:8082');
|
||||
const indexPath = path.join(webRoot, 'index.html');
|
||||
const adminIndexPath = path.join(adminWebRoot, 'index.html');
|
||||
const proxyPrefixes = [
|
||||
'/admin/api',
|
||||
'/api/',
|
||||
'/api',
|
||||
'/healthz',
|
||||
@@ -413,11 +470,11 @@ function sendFile(response, filePath) {
|
||||
.pipe(response);
|
||||
}
|
||||
|
||||
function serveStatic(request, response, pathname) {
|
||||
function serveStaticFromRoot(response, pathname, rootDir, fallbackIndexPath) {
|
||||
const decodedPath = decodeURIComponent(pathname);
|
||||
const relativePath = decodedPath === '/' ? '/index.html' : decodedPath;
|
||||
const filePath = path.normalize(path.join(webRoot, relativePath));
|
||||
const safeRelativePath = path.relative(webRoot, filePath);
|
||||
const filePath = path.normalize(path.join(rootDir, relativePath));
|
||||
const safeRelativePath = path.relative(rootDir, filePath);
|
||||
|
||||
if (safeRelativePath.startsWith('..') || path.isAbsolute(safeRelativePath)) {
|
||||
response.writeHead(403, {'content-type': 'text/plain; charset=utf-8'});
|
||||
@@ -427,12 +484,21 @@ function serveStatic(request, response, pathname) {
|
||||
|
||||
const resolvedFilePath = fs.existsSync(filePath) && fs.statSync(filePath).isFile()
|
||||
? filePath
|
||||
: indexPath;
|
||||
: fallbackIndexPath;
|
||||
|
||||
response.writeHead(200, {'content-type': contentTypeFor(resolvedFilePath)});
|
||||
sendFile(response, resolvedFilePath);
|
||||
}
|
||||
|
||||
function serveStatic(request, response, pathname) {
|
||||
serveStaticFromRoot(response, pathname, webRoot, indexPath);
|
||||
}
|
||||
|
||||
function serveAdminStatic(response, pathname) {
|
||||
const adminPath = pathname === '/admin/' ? '/' : pathname.replace(/^\/admin/u, '');
|
||||
serveStaticFromRoot(response, adminPath, adminWebRoot, adminIndexPath);
|
||||
}
|
||||
|
||||
function proxyToApi(request, response) {
|
||||
const targetUrl = new URL(request.url || '/', apiTarget);
|
||||
const proxyRequest = http.request(
|
||||
@@ -469,6 +535,17 @@ const server = http.createServer((request, response) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/admin') {
|
||||
response.writeHead(301, {location: '/admin/'});
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/admin/' || url.pathname.startsWith('/admin/')) {
|
||||
serveAdminStatic(response, url.pathname);
|
||||
return;
|
||||
}
|
||||
|
||||
serveStatic(request, response, url.pathname);
|
||||
});
|
||||
|
||||
@@ -477,6 +554,15 @@ server.listen(webPort, webHost, () => {
|
||||
});
|
||||
WEB_SERVER
|
||||
|
||||
touch "${TARGET_DIR}/.env"
|
||||
for env_file in "${TARGET_DIR}/.env" "${TARGET_DIR}/.env.local"; do
|
||||
if [[ -f "${env_file}" ]]; then
|
||||
grep -v '^GENARRATIVE_SPACETIME_ROOT_DIR=' "${env_file}" >"${env_file}.tmp" || true
|
||||
mv "${env_file}.tmp" "${env_file}"
|
||||
printf '\nGENARRATIVE_SPACETIME_ROOT_DIR=__GENARRATIVE_RUNTIME_SPACETIME_ROOT_DIR__\n' >>"${env_file}"
|
||||
fi
|
||||
done
|
||||
|
||||
cat >"${TARGET_DIR}/start.sh" <<'START_SCRIPT'
|
||||
#!/usr/bin/env bash
|
||||
|
||||
@@ -543,7 +629,8 @@ usage() {
|
||||
说明:
|
||||
1. 启动当前发布包内的静态网站、SpacetimeDB 与 api-server。
|
||||
2. 默认发布 spacetime_module.wasm 到 GENARRATIVE_SPACETIME_DATABASE,但不清库。
|
||||
3. 只有显式传入 --clear-database 时才会在 schema 冲突时清理旧模块数据后重发。
|
||||
3. 默认遇到 schema 冲突时自动导出旧库、清库发布新模块并导入回灌。
|
||||
4. 显式传入 --clear-database 时代表人工确认清库,不执行自动回灌。
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -569,17 +656,28 @@ load_env_file "${SCRIPT_DIR}/.env"
|
||||
load_env_file "${SCRIPT_DIR}/.env.local"
|
||||
|
||||
SPACETIME_ROOT_DIR="${GENARRATIVE_SPACETIME_ROOT_DIR:-${SCRIPT_DIR}/.spacetimedb}"
|
||||
if [[ "${SPACETIME_ROOT_DIR}" == "__GENARRATIVE_RUNTIME_SPACETIME_ROOT_DIR__" ]]; then
|
||||
SPACETIME_ROOT_DIR="${SCRIPT_DIR}/.spacetimedb"
|
||||
fi
|
||||
SPACETIME_HOST="${GENARRATIVE_SPACETIME_HOST:-__GENARRATIVE_DEFAULT_SPACETIME_HOST__}"
|
||||
SPACETIME_PORT="${GENARRATIVE_SPACETIME_PORT:-__GENARRATIVE_DEFAULT_SPACETIME_PORT__}"
|
||||
SPACETIME_SERVER_URL="${GENARRATIVE_SPACETIME_SERVER_URL:-http://${SPACETIME_HOST}:${SPACETIME_PORT}}"
|
||||
SPACETIME_DATABASE="${GENARRATIVE_SPACETIME_DATABASE:-__GENARRATIVE_DEFAULT_SPACETIME_DATABASE__}"
|
||||
SPACETIME_TIMEOUT_SECONDS="${GENARRATIVE_SPACETIME_TIMEOUT_SECONDS:-60}"
|
||||
SPACETIME_MIGRATE_ON_CONFLICT="${GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT:-true}"
|
||||
SPACETIME_MIGRATION_DIR="${GENARRATIVE_SPACETIME_MIGRATION_DIR:-}"
|
||||
SPACETIME_MIGRATION_EXPORT_TOKEN="${GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN:-}"
|
||||
SPACETIME_MIGRATION_IMPORT_TOKEN="${GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN:-}"
|
||||
API_HOST="${GENARRATIVE_API_HOST:-__GENARRATIVE_DEFAULT_API_HOST__}"
|
||||
API_PORT="${GENARRATIVE_API_PORT:-__GENARRATIVE_DEFAULT_API_PORT__}"
|
||||
API_LOG="${GENARRATIVE_API_LOG:-info,tower_http=info}"
|
||||
WEB_HOST="${GENARRATIVE_WEB_HOST:-__GENARRATIVE_DEFAULT_WEB_HOST__}"
|
||||
WEB_PORT="${GENARRATIVE_WEB_PORT:-__GENARRATIVE_DEFAULT_WEB_PORT__}"
|
||||
MIGRATION_BOOTSTRAP_SECRET_FILE="${SCRIPT_DIR}/migration-bootstrap-secret.txt"
|
||||
PREVIOUS_MIGRATION_BOOTSTRAP_SECRET_FILE="${SCRIPT_DIR}/deploy-state/migration-bootstrap-secret.previous.txt"
|
||||
MIGRATION_SCRIPT_DIR="${SCRIPT_DIR}/scripts"
|
||||
MIGRATION_EXPORT_SCRIPT="${MIGRATION_SCRIPT_DIR}/spacetime-export-migration-json.mjs"
|
||||
MIGRATION_IMPORT_SCRIPT="${MIGRATION_SCRIPT_DIR}/spacetime-import-migration-json.mjs"
|
||||
|
||||
# 日志默认落文件,显式关闭 ANSI 颜色码,避免控制字符写入 *.log。
|
||||
export NO_COLOR="${NO_COLOR:-1}"
|
||||
@@ -594,6 +692,178 @@ require_command() {
|
||||
fi
|
||||
}
|
||||
|
||||
is_truthy() {
|
||||
local normalized
|
||||
|
||||
normalized="$(printf "%s" "${1:-}" | tr '[:upper:]' '[:lower:]')"
|
||||
case "${normalized}" in
|
||||
1|true|yes|y|on)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
timestamp_slug() {
|
||||
date -u +%Y-%m-%dT%H-%M-%SZ
|
||||
}
|
||||
|
||||
sanitize_path_segment() {
|
||||
printf "%s" "$1" | tr -c 'A-Za-z0-9._-' '_'
|
||||
}
|
||||
|
||||
validate_spacetime_database_name() {
|
||||
local database="$1"
|
||||
|
||||
if [[ ! "${database}" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
|
||||
echo "[start] GENARRATIVE_SPACETIME_DATABASE 必须匹配 SpacetimeDB 数据库名规则 ^[a-z0-9]+(-[a-z0-9]+)*$,只能使用小写字母、数字,并用单个短横线分隔: ${database}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_publish_conflict_output() {
|
||||
local output="$1"
|
||||
local normalized
|
||||
|
||||
normalized="$(printf "%s" "${output}" | tr '[:upper:]' '[:lower:]')"
|
||||
[[ "${normalized}" == *"requires a manual migration"* ]] \
|
||||
|| [[ "${normalized}" == *"manual migration"* ]] \
|
||||
|| [[ "${normalized}" == *"schema"* && "${normalized}" == *"conflict"* ]] \
|
||||
|| [[ "${normalized}" == *"clear-database"* ]] \
|
||||
|| [[ "${normalized}" == *"clear database"* && "${normalized}" == *"publish"* ]]
|
||||
}
|
||||
|
||||
read_migration_bootstrap_secret() {
|
||||
local secret_file="$1"
|
||||
local label="$2"
|
||||
local secret=""
|
||||
|
||||
if [[ ! -f "${secret_file}" ]]; then
|
||||
echo "[start] schema 冲突自动迁移需要${label}: ${secret_file}" >&2
|
||||
echo "[start] 请使用默认带迁移引导密钥的发布包,或设置 GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT=false 后人工处理。" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
secret="$(tr -d '\r\n' <"${secret_file}")"
|
||||
if [[ -z "${secret}" ]]; then
|
||||
echo "[start] 迁移引导密钥为空${label}: ${secret_file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "%s" "${secret}"
|
||||
}
|
||||
|
||||
read_export_migration_bootstrap_secret() {
|
||||
if [[ -f "${PREVIOUS_MIGRATION_BOOTSTRAP_SECRET_FILE}" ]]; then
|
||||
read_migration_bootstrap_secret "${PREVIOUS_MIGRATION_BOOTSTRAP_SECRET_FILE}" "(旧模块导出)"
|
||||
return
|
||||
fi
|
||||
|
||||
read_migration_bootstrap_secret "${MIGRATION_BOOTSTRAP_SECRET_FILE}" "(当前模块导出兜底)"
|
||||
}
|
||||
|
||||
read_import_migration_bootstrap_secret() {
|
||||
read_migration_bootstrap_secret "${MIGRATION_BOOTSTRAP_SECRET_FILE}" "(新模块导入)"
|
||||
}
|
||||
|
||||
require_migration_script() {
|
||||
local script_path="$1"
|
||||
|
||||
if [[ ! -f "${script_path}" ]]; then
|
||||
echo "[start] 发布包缺少 SpacetimeDB 迁移脚本: ${script_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_publish() {
|
||||
local output_file="$1"
|
||||
shift
|
||||
|
||||
set +e
|
||||
spacetime --root-dir="${SPACETIME_ROOT_DIR}" "$@" >"${output_file}" 2>&1
|
||||
local status=$?
|
||||
set -e
|
||||
cat "${output_file}"
|
||||
return "${status}"
|
||||
}
|
||||
|
||||
run_conflict_migration_publish() {
|
||||
local export_bootstrap_secret=""
|
||||
local import_bootstrap_secret=""
|
||||
local export_auth_args=()
|
||||
local import_auth_args=()
|
||||
local migration_database_slug=""
|
||||
local migration_root=""
|
||||
local migration_file=""
|
||||
local publish_log=""
|
||||
|
||||
if [[ -n "${SPACETIME_MIGRATION_EXPORT_TOKEN}" ]]; then
|
||||
echo "[start] 使用 GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN 导出旧库"
|
||||
export_auth_args=(--token "${SPACETIME_MIGRATION_EXPORT_TOKEN}")
|
||||
else
|
||||
export_bootstrap_secret="$(read_export_migration_bootstrap_secret)"
|
||||
export_auth_args=(--bootstrap-secret "${export_bootstrap_secret}")
|
||||
fi
|
||||
|
||||
if [[ -n "${SPACETIME_MIGRATION_IMPORT_TOKEN}" ]]; then
|
||||
echo "[start] 使用 GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN 导入新库"
|
||||
import_auth_args=(--token "${SPACETIME_MIGRATION_IMPORT_TOKEN}")
|
||||
else
|
||||
import_bootstrap_secret="$(read_import_migration_bootstrap_secret)"
|
||||
import_auth_args=(--bootstrap-secret "${import_bootstrap_secret}")
|
||||
fi
|
||||
require_migration_script "${MIGRATION_EXPORT_SCRIPT}"
|
||||
require_migration_script "${MIGRATION_IMPORT_SCRIPT}"
|
||||
|
||||
migration_database_slug="$(sanitize_path_segment "${SPACETIME_DATABASE}")"
|
||||
migration_root="${SPACETIME_MIGRATION_DIR:-${SCRIPT_DIR}/database-migrations/${migration_database_slug}}"
|
||||
mkdir -p "${migration_root}"
|
||||
migration_file="${migration_root}/$(timestamp_slug).json"
|
||||
|
||||
echo "[start] 检测到 SpacetimeDB schema 冲突,开始导出旧库迁移 JSON: ${migration_file}"
|
||||
node "${MIGRATION_EXPORT_SCRIPT}" \
|
||||
--server "${SPACETIME_SERVER_URL}" \
|
||||
--server-url "${SPACETIME_SERVER_URL}" \
|
||||
--root-dir "${SPACETIME_ROOT_DIR}" \
|
||||
--database "${SPACETIME_DATABASE}" \
|
||||
"${export_auth_args[@]}" \
|
||||
--out "${migration_file}" \
|
||||
--note "deploy conflict export $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
echo "[start] 清库发布新 SpacetimeDB wasm"
|
||||
publish_log="$(mktemp)"
|
||||
if ! run_publish "${publish_log}" \
|
||||
publish \
|
||||
"${SPACETIME_DATABASE}" \
|
||||
--server "${SPACETIME_SERVER_URL}" \
|
||||
--bin-path "${SCRIPT_DIR}/spacetime_module.wasm" \
|
||||
--clear-database \
|
||||
--yes; then
|
||||
echo "[start] 清库发布失败,迁移 JSON 已保留: ${migration_file}" >&2
|
||||
rm -f "${publish_log}"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "${publish_log}"
|
||||
|
||||
echo "[start] 导入迁移 JSON 回灌数据"
|
||||
if ! node "${MIGRATION_IMPORT_SCRIPT}" \
|
||||
--server "${SPACETIME_SERVER_URL}" \
|
||||
--server-url "${SPACETIME_SERVER_URL}" \
|
||||
--root-dir "${SPACETIME_ROOT_DIR}" \
|
||||
--database "${SPACETIME_DATABASE}" \
|
||||
"${import_auth_args[@]}" \
|
||||
--in "${migration_file}" \
|
||||
--replace-existing \
|
||||
--note "deploy conflict import $(date -u +%Y-%m-%dT%H:%M:%SZ)"; then
|
||||
echo "[start] 导入失败,迁移 JSON 已保留: ${migration_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[start] schema 冲突自动迁移完成,迁移 JSON: ${migration_file}"
|
||||
}
|
||||
|
||||
wait_for_spacetime() {
|
||||
local process_pid="${1:-}"
|
||||
local deadline=$((SECONDS + SPACETIME_TIMEOUT_SECONDS))
|
||||
@@ -781,10 +1051,17 @@ start_process() {
|
||||
fi
|
||||
|
||||
echo "[start] 启动 ${name}"
|
||||
nohup "$@" >"${log_file}" 2>&1 &
|
||||
JENKINS_NODE_COOKIE=dontKillMe BUILD_ID=dontKillMe nohup "$@" >"${log_file}" 2>&1 &
|
||||
echo "$!" >"${pid_file}"
|
||||
}
|
||||
|
||||
validate_spacetime_database_name "${SPACETIME_DATABASE}"
|
||||
|
||||
echo "[start] SpacetimeDB 发布配置:"
|
||||
echo "[start] - database: ${SPACETIME_DATABASE}"
|
||||
echo "[start] - server: ${SPACETIME_SERVER_URL}"
|
||||
echo "[start] - root-dir: ${SPACETIME_ROOT_DIR}"
|
||||
|
||||
require_command node
|
||||
require_command spacetime
|
||||
|
||||
@@ -834,14 +1111,28 @@ if [[ -f "${MIGRATION_BOOTSTRAP_SECRET_FILE}" ]]; then
|
||||
else
|
||||
echo "[start] 未启用迁移引导密钥。"
|
||||
fi
|
||||
if ! spacetime --root-dir="${SPACETIME_ROOT_DIR}" "${PUBLISH_ARGS[@]}"; then
|
||||
echo "[start] SpacetimeDB 发布失败。" >&2
|
||||
echo "[start] 如果错误包含 403 Forbidden 或 is not authorized,通常是当前 CLI 身份无权更新目标数据库。" >&2
|
||||
echo "[start] 当前 start.sh 使用的 CLI root: ${SPACETIME_ROOT_DIR}" >&2
|
||||
spacetime --root-dir="${SPACETIME_ROOT_DIR}" login show >&2 || true
|
||||
echo "[start] 如果目标是本地库且可以清空数据:先执行 ./stop.sh,备份或删除 ${SPACETIME_ROOT_DIR},再重新执行 ./start.sh。" >&2
|
||||
echo "[start] 如果目标是 Maincloud 或必须保留数据:请切换到创建该数据库的 SpacetimeDB 身份,或把 GENARRATIVE_SPACETIME_DATABASE 改为当前身份有权限的库。" >&2
|
||||
exit 1
|
||||
PUBLISH_LOG="$(mktemp)"
|
||||
if ! run_publish "${PUBLISH_LOG}" "${PUBLISH_ARGS[@]}"; then
|
||||
PUBLISH_OUTPUT="$(cat "${PUBLISH_LOG}")"
|
||||
rm -f "${PUBLISH_LOG}"
|
||||
if [[ "${CLEAR_DATABASE}" -eq 0 ]] \
|
||||
&& is_truthy "${SPACETIME_MIGRATE_ON_CONFLICT}" \
|
||||
&& is_publish_conflict_output "${PUBLISH_OUTPUT}"; then
|
||||
run_conflict_migration_publish
|
||||
else
|
||||
if [[ "${CLEAR_DATABASE}" -eq 0 ]] && ! is_truthy "${SPACETIME_MIGRATE_ON_CONFLICT}"; then
|
||||
echo "[start] 已禁用 schema 冲突自动迁移: GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT=${SPACETIME_MIGRATE_ON_CONFLICT}" >&2
|
||||
fi
|
||||
echo "[start] SpacetimeDB 发布失败。" >&2
|
||||
echo "[start] 如果错误包含 403 Forbidden 或 is not authorized,通常是当前 CLI 身份无权更新目标数据库。" >&2
|
||||
echo "[start] 当前 start.sh 使用的 CLI root: ${SPACETIME_ROOT_DIR}" >&2
|
||||
spacetime --root-dir="${SPACETIME_ROOT_DIR}" login show >&2 || true
|
||||
echo "[start] 如果目标是本地库且可以清空数据:先执行 ./stop.sh,备份或删除 ${SPACETIME_ROOT_DIR},再重新执行 ./start.sh --clear-database。" >&2
|
||||
echo "[start] 如果目标是 Maincloud 或必须保留数据:请切换到创建该数据库的 SpacetimeDB 身份,或把 GENARRATIVE_SPACETIME_DATABASE 改为当前身份有权限的库。" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
rm -f "${PUBLISH_LOG}"
|
||||
fi
|
||||
|
||||
export GENARRATIVE_API_HOST="${API_HOST}"
|
||||
@@ -860,6 +1151,7 @@ echo "[start] 完成"
|
||||
echo "[start] Web: http://${WEB_HOST}:${WEB_PORT}"
|
||||
echo "[start] API: http://${API_HOST}:${API_PORT}"
|
||||
echo "[start] SpacetimeDB: ${SPACETIME_SERVER_URL}"
|
||||
echo "[start] SpacetimeDB database: ${SPACETIME_DATABASE}"
|
||||
START_SCRIPT
|
||||
|
||||
replace_placeholder_in_file "${TARGET_DIR}/start.sh" "__GENARRATIVE_DEFAULT_SPACETIME_HOST__" "${SPACETIME_HOST}"
|
||||
@@ -913,18 +1205,19 @@ STOP_SCRIPT
|
||||
|
||||
chmod +x "${TARGET_DIR}/start.sh" "${TARGET_DIR}/stop.sh"
|
||||
|
||||
cat >"${TARGET_DIR}/README.md" <<EOF
|
||||
cat >"${TARGET_DIR}/README.md" <<'EOF'
|
||||
# Genarrative Ubuntu Release
|
||||
|
||||
构建时间:\`${BUILD_NAME}\`
|
||||
构建时间:`__GENARRATIVE_BUILD_NAME__`
|
||||
|
||||
## 内容
|
||||
|
||||
- \`.env\` / \`.env.local\`:从仓库根目录复制的环境文件,同时各保留一份到 \`web/\`
|
||||
- \`web/\`:Vite release 静态资源
|
||||
- \`web/\`:主前端 Vite release 静态资源,\`web/admin/\` 为后台管理前端静态资源
|
||||
- \`api-server\`:x86_64-unknown-linux-gnu release 可执行文件
|
||||
- \`spacetime_module.wasm\`:wasm32-unknown-unknown release 模块
|
||||
- \`migration-bootstrap-secret.txt\`:本发布包 wasm 编译时注入的迁移引导密钥;服务器 \`start.sh\` 发布时会显示,迁移授权完成后可删除
|
||||
- \`scripts/spacetime-*.mjs\`:部署时 schema 冲突自动导出、导入回灌使用的 SpacetimeDB 迁移脚本
|
||||
- \`web-server.mjs\`:静态网站与 API 反代入口
|
||||
- \`start.sh\` / \`stop.sh\`:目标服务器启动与停止脚本
|
||||
|
||||
@@ -940,21 +1233,31 @@ cat >"${TARGET_DIR}/README.md" <<EOF
|
||||
./start.sh --clear-database
|
||||
\`\`\`
|
||||
|
||||
默认启动会先尝试无清库发布;如果 SpacetimeDB 返回 schema 冲突,\`start.sh\` 会把旧库导出到 \`database-migrations/<database>/\`,随后清库发布新 wasm,并用 \`--replace-existing\` 导入回灌。
|
||||
|
||||
## 入口
|
||||
|
||||
- 主站:\`http://<web-host>:<web-port>/\`
|
||||
- 后台:\`http://<web-host>:<web-port>/admin/\`
|
||||
|
||||
## 环境变量
|
||||
|
||||
- 启动时会先加载发布目录根部的 \`.env\` 与 \`.env.local\`,再回退到脚本内默认值。
|
||||
- 环境文件复制进发布包时会移除 UTF-8 BOM 与 CRLF;启动时也会按 \`KEY=value\` 子集解析,跳过不合法行。
|
||||
- 脚本内默认值来自构建时的 `--database`、`--api-port`、`--web-port`、`--spacetime-host`、`--spacetime-port` 参数。
|
||||
- 脚本内默认值来自构建时的 `--database`、`--api-port`、`--web-host`、`--web-port`、`--spacetime-host`、`--spacetime-port` 参数;Web 默认只监听 `127.0.0.1`。
|
||||
- 默认导出 \`NO_COLOR=1\` 与 \`CARGO_TERM_COLOR=never\`,避免 ANSI 颜色控制码写入日志文件;如确有需要可在启动前显式覆盖。
|
||||
- \`GENARRATIVE_WEB_HOST\` / \`GENARRATIVE_WEB_PORT\`
|
||||
- \`GENARRATIVE_API_HOST\` / \`GENARRATIVE_API_PORT\` / \`GENARRATIVE_API_LOG\`
|
||||
- \`GENARRATIVE_SPACETIME_HOST\` / \`GENARRATIVE_SPACETIME_PORT\`
|
||||
- \`GENARRATIVE_SPACETIME_SERVER_URL\` / \`GENARRATIVE_SPACETIME_DATABASE\`
|
||||
- \`GENARRATIVE_SPACETIME_SERVER_URL\` / \`GENARRATIVE_SPACETIME_DATABASE\` / \`GENARRATIVE_SPACETIME_TOKEN\`
|
||||
- \`GENARRATIVE_SPACETIME_ROOT_DIR\`:默认使用发布目录下的 \`.spacetimedb/\`,同时承载本地 SpacetimeDB 运行数据与 CLI 身份。
|
||||
- \`GENARRATIVE_SPACETIME_TIMEOUT_SECONDS\`:等待 SpacetimeDB 就绪的秒数,默认 \`60\`。
|
||||
- OSS、LLM、短信、微信等业务密钥仍通过目标服务器环境变量或同目录 \`.env.local\` 管理。
|
||||
- \`GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT\`:默认 \`true\`,普通发布遇到 schema 冲突时自动导出、清库发布、导入回灌;设为 \`false\` 时保留原始发布失败。
|
||||
- \`GENARRATIVE_SPACETIME_MIGRATION_DIR\`:自动迁移 JSON 输出目录,默认 \`database-migrations/<database>/\`。
|
||||
- OSS、LLM、短信、微信、SpacetimeDB owner token 等业务密钥仍通过目标服务器环境变量或同目录 \`.env.local\` 管理;后台表统计读取 private 表时需要 \`GENARRATIVE_SPACETIME_TOKEN\` 对目标库有 owner 权限。
|
||||
- 迁移引导密钥由构建发布包时随机生成,构建日志和服务器 \`start.sh\` 发布日志都会显示同一份密钥。
|
||||
EOF
|
||||
replace_placeholder_in_file "${TARGET_DIR}/README.md" "__GENARRATIVE_BUILD_NAME__" "${BUILD_NAME}"
|
||||
|
||||
BUILD_COMPLETED=1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user