Merge remote-tracking branch 'origin/hermes/wechat'

This commit is contained in:
2026-05-15 01:36:24 +08:00
33 changed files with 1864 additions and 188 deletions

View File

@@ -1,6 +1,11 @@
import { execFileSync, spawn } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
createWriteStream,
existsSync,
mkdirSync,
readFileSync,
} from 'node:fs';
import { dirname, isAbsolute, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = process.cwd();
@@ -67,7 +72,69 @@ export function mergeApiServerEnv(repoRootPath, baseEnv = process.env) {
return mergedEnv;
}
function stopExistingWindowsApiServer() {
export function formatApiServerLogTimestamp(date = new Date()) {
const pad = (value) => String(value).padStart(2, '0');
return [
date.getFullYear(),
pad(date.getMonth() + 1),
pad(date.getDate()),
'-',
pad(date.getHours()),
pad(date.getMinutes()),
pad(date.getSeconds()),
].join('');
}
export function resolveApiServerLogFile(
repoRootPath,
env = process.env,
now = new Date(),
) {
const explicitLogFile = String(
env.GENARRATIVE_API_SERVER_LOG_FILE ?? '',
).trim();
if (explicitLogFile) {
return isAbsolute(explicitLogFile)
? explicitLogFile
: resolve(repoRootPath, explicitLogFile);
}
const logDir =
String(env.GENARRATIVE_API_SERVER_LOG_DIR ?? '').trim() ||
'logs/api-server';
const resolvedLogDir = isAbsolute(logDir)
? logDir
: resolve(repoRootPath, logDir);
return resolve(
resolvedLogDir,
`api-server-${formatApiServerLogTimestamp(now)}.log`,
);
}
function createApiServerLogStream(logFilePath) {
mkdirSync(dirname(logFilePath), { recursive: true });
const logStream = createWriteStream(logFilePath, {
flags: 'a',
encoding: 'utf8',
});
logStream.on('error', (error) => {
console.error(`[api-server] 写入日志失败: ${error.message}`);
});
return logStream;
}
function writeLauncherLog(logStream, message, stream = process.stdout) {
const line = `${message}\n`;
stream.write(line);
if (!logStream.destroyed) {
logStream.write(line);
}
}
function stopExistingWindowsApiServer(logStream) {
if (process.platform !== 'win32') {
return;
}
@@ -104,7 +171,7 @@ function stopExistingWindowsApiServer() {
).trim();
if (output) {
console.log(`[api-server] 已停止旧 api-server 进程: ${output}`);
writeLauncherLog(logStream, `[api-server] 已停止旧 api-server 进程: ${output}`);
}
}
@@ -121,19 +188,55 @@ function main() {
mergedEnv.GENARRATIVE_SPACETIME_TOKEN =
mergedEnv.GENARRATIVE_SPACETIME_TOKEN || '';
const logFilePath = resolveApiServerLogFile(repoRoot, mergedEnv);
const logStream = createApiServerLogStream(logFilePath);
mergedEnv.GENARRATIVE_API_SERVER_LOG_FILE = logFilePath;
let didExit = false;
const exitAfterLogFlush = (code) => {
const finish = () => {
if (didExit) {
return;
}
didExit = true;
process.exit(code);
};
if (logStream.destroyed) {
finish();
return;
}
logStream.end(finish);
setTimeout(finish, 1000).unref();
};
writeLauncherLog(logStream, `[api-server] 日志: ${logFilePath}`);
if (!mergedEnv.GENARRATIVE_SPACETIME_DATABASE) {
console.error('[api-server] 缺少 GENARRATIVE_SPACETIME_DATABASE。');
process.exit(1);
writeLauncherLog(
logStream,
'[api-server] 缺少 GENARRATIVE_SPACETIME_DATABASE。',
process.stderr,
);
exitAfterLogFlush(1);
return;
}
try {
stopExistingWindowsApiServer();
stopExistingWindowsApiServer(logStream);
} catch (error) {
console.error(`[api-server] 清理旧 api-server 进程失败: ${error.message}`);
process.exit(1);
writeLauncherLog(
logStream,
`[api-server] 清理旧 api-server 进程失败: ${error.message}`,
process.stderr,
);
exitAfterLogFlush(1);
return;
}
console.log(
writeLauncherLog(
logStream,
`[api-server] SpacetimeDB ${mergedEnv.GENARRATIVE_SPACETIME_DATABASE} @ ${mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL}`,
);
@@ -143,22 +246,41 @@ function main() {
{
cwd: repoRoot,
env: mergedEnv,
stdio: 'inherit',
stdio: ['inherit', 'pipe', 'pipe'],
},
);
child.on('error', (error) => {
console.error(`[api-server] 启动 cargo 失败: ${error.message}`);
process.exit(1);
child.stdout?.on('data', (chunk) => {
process.stdout.write(chunk);
logStream.write(chunk);
});
child.on('exit', (code, signal) => {
child.stderr?.on('data', (chunk) => {
process.stderr.write(chunk);
logStream.write(chunk);
});
child.on('error', (error) => {
writeLauncherLog(
logStream,
`[api-server] 启动 cargo 失败: ${error.message}`,
process.stderr,
);
exitAfterLogFlush(1);
});
child.on('close', (code, signal) => {
if (signal) {
console.error(`[api-server] api-server 被信号终止: ${signal}`);
process.exit(1);
writeLauncherLog(
logStream,
`[api-server] api-server 被信号终止: ${signal}`,
process.stderr,
);
exitAfterLogFlush(1);
return;
}
process.exit(code ?? 0);
exitAfterLogFlush(code ?? 0);
});
}

View File

@@ -4,7 +4,11 @@ import { join } from 'node:path';
import { describe, expect, test } from 'vitest';
import { mergeApiServerEnv } from './api-server-dev.mjs';
import {
formatApiServerLogTimestamp,
mergeApiServerEnv,
resolveApiServerLogFile,
} from './api-server-dev.mjs';
type EnvMap = Record<string, string>;
@@ -92,3 +96,39 @@ describe('api-server-dev env merge', () => {
);
});
});
describe('api-server-dev log file resolution', () => {
const fixedDate = new Date(2026, 4, 15, 6, 7, 8);
test('默认写入 logs/api-server 的时间戳文件', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-log-'));
try {
expect(formatApiServerLogTimestamp(fixedDate)).toBe('20260515-060708');
expect(resolveApiServerLogFile(tempDir, {}, fixedDate)).toBe(
join(tempDir, 'logs/api-server/api-server-20260515-060708.log'),
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
test('GENARRATIVE_API_SERVER_LOG_FILE 优先于日志目录默认值', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-log-'));
try {
expect(
resolveApiServerLogFile(
tempDir,
{
GENARRATIVE_API_SERVER_LOG_DIR: 'logs/ignored',
GENARRATIVE_API_SERVER_LOG_FILE: 'logs/custom/api.log',
},
fixedDate,
),
).toBe(join(tempDir, 'logs/custom/api.log'));
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});

View File

@@ -658,11 +658,13 @@ wait_for_api_server() {
local health_url="$1"
local timeout_seconds="$2"
local process_pid="${3:-}"
local log_file="${4:-${API_SERVER_LOG_FILE:-}}"
local deadline=$((SECONDS + timeout_seconds))
while ((SECONDS < deadline)); do
if [[ -n "${process_pid}" ]] && ! kill -0 "${process_pid}" 2>/dev/null; then
echo "[dev:rust] api-server 进程在就绪前退出。" >&2
print_api_server_log_tail "${log_file}"
exit 1
fi
@@ -684,9 +686,58 @@ request.on("error", () => process.exit(1));
done
echo "[dev:rust] 等待 api-server 就绪超时: ${health_url}" >&2
print_api_server_log_tail "${log_file}"
exit 1
}
format_api_server_log_timestamp() {
date +%Y%m%d-%H%M%S
}
normalize_api_server_log_path() {
local path_value="$1"
if [[ "${path_value}" == *\\* ]]; then
path_value="${path_value//\\//}"
fi
echo "${path_value}"
}
resolve_api_server_log_file() {
local explicit_log_file="${GENARRATIVE_API_SERVER_LOG_FILE:-}"
local log_dir="${GENARRATIVE_API_SERVER_LOG_DIR:-${REPO_ROOT}/logs/api-server}"
if [[ -n "${explicit_log_file//[[:space:]]/}" ]]; then
explicit_log_file="$(normalize_api_server_log_path "${explicit_log_file}")"
if [[ "${explicit_log_file}" = /* || "${explicit_log_file}" =~ ^[A-Za-z]:[\\/] ]]; then
echo "${explicit_log_file}"
return
fi
echo "${REPO_ROOT}/${explicit_log_file}"
return
fi
log_dir="$(normalize_api_server_log_path "${log_dir}")"
if [[ ! "${log_dir}" = /* && ! "${log_dir}" =~ ^[A-Za-z]:[\\/] ]]; then
log_dir="${REPO_ROOT}/${log_dir}"
fi
echo "${log_dir}/api-server-dev-rust-$(format_api_server_log_timestamp).log"
}
print_api_server_log_tail() {
local log_file="${1:-}"
if [[ -z "${log_file}" || ! -f "${log_file}" ]]; then
return
fi
echo "[dev:rust] api-server 最近日志: ${log_file}" >&2
tail -n 80 "${log_file}" >&2 || true
}
generate_migration_bootstrap_secret() {
node -e 'const crypto = require("crypto"); process.stdout.write(crypto.randomBytes(32).toString("hex"));'
@@ -995,22 +1046,26 @@ API_PORT="$(find_nearest_available_port "${API_HOST}" "${API_PORT}" "api-server"
API_TARGET_HOST="$(resolve_client_host "${API_HOST}")"
# `.env.local` 可以给单独 `dev:web` 配置代理目标,但完整栈的前端必须跟随本次 `--api-port`。
RUST_SERVER_TARGET="http://${API_TARGET_HOST}:${API_PORT}"
API_SERVER_LOG_FILE="$(resolve_api_server_log_file)"
mkdir -p "$(dirname -- "${API_SERVER_LOG_FILE}")"
echo "[dev:rust] api actual: ${RUST_SERVER_TARGET}"
echo "[dev:rust] api-server log: ${API_SERVER_LOG_FILE}"
(
cd "${REPO_ROOT}"
GENARRATIVE_API_HOST="${API_HOST}" \
GENARRATIVE_API_PORT="${API_PORT}" \
GENARRATIVE_API_LOG="${API_LOG}" \
GENARRATIVE_API_SERVER_LOG_FILE="${API_SERVER_LOG_FILE}" \
GENARRATIVE_SPACETIME_SERVER_URL="${SPACETIME_SERVER}" \
GENARRATIVE_SPACETIME_DATABASE="${DATABASE}" \
exec cargo run -p api-server --manifest-path "${MANIFEST_PATH}"
) &
) > >(tee -a "${API_SERVER_LOG_FILE}") 2>&1 &
API_PID="$!"
PIDS+=("${API_PID}")
NAMES+=("api-server")
echo "[dev:rust] 等待 api-server 就绪"
wait_for_api_server "${RUST_SERVER_TARGET}/healthz" "${API_SERVER_TIMEOUT_SECONDS}" "${API_PID}"
wait_for_api_server "${RUST_SERVER_TARGET}/healthz" "${API_SERVER_TIMEOUT_SECONDS}" "${API_PID}" "${API_SERVER_LOG_FILE}"
echo "[dev:rust] 启动 vite"
(