- 添加HNSW向量索引提升检索性能 - 实现Hybrid检索模式支持向量和关键词双路召回 - 采用RRF算法融合多路召回结果 - 支持ParentChild分块模式的上下文回填 - 优化向量写入采用批量SQL操作提升性能 - 添加RAG PostgreSQL连接字符串配置 - 更新部署脚本增加PGVector集成测试 - 修正表名引用从document_chunks到b_document_chunks - 重构RagRetrievalService实现完整的检索流程 - 增加重排策略支持关键词和LLM重排 - 修改服务器部署地址配置
409 lines
16 KiB
Bash
Executable File
409 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
|
SOLUTION="$REPO_ROOT/BuildFlow.slnx"
|
|
COMPOSE_FILE="$SCRIPT_DIR/backend-optimization-compose.yml"
|
|
RUN_TIMESTAMP=$(date -u +%Y%m%dT%H%M%SZ)
|
|
RUN_ID="${RUN_TIMESTAMP}-$$"
|
|
ARTIFACT_ROOT=${BACKEND_SOP_ARTIFACT_ROOT:-"$REPO_ROOT/artifacts/backend-optimization-sop"}
|
|
ARTIFACT_DIR="$ARTIFACT_ROOT/$RUN_ID"
|
|
LOG_DIR="$ARTIFACT_DIR/logs"
|
|
TRX_DIR="$ARTIFACT_DIR/trx"
|
|
SUMMARY_FILE="$ARTIFACT_DIR/summary.md"
|
|
STAGE_FILE="$ARTIFACT_DIR/stages.tsv"
|
|
ENV_FILE="$ARTIFACT_DIR/infrastructure.env"
|
|
SUCCESS_FILE="$ARTIFACT_DIR/SUCCESS"
|
|
COMPOSE_PROJECT="buildflow-sop-$(date -u +%Y%m%d%H%M%S)-$$"
|
|
CURRENT_STAGE="not-started"
|
|
CURRENT_LOG=""
|
|
INFRA_STARTED=0
|
|
RUN_COMPLETED=0
|
|
|
|
mkdir -p "$LOG_DIR" "$TRX_DIR"
|
|
printf 'stage\tstarted_utc\tended_utc\tduration_seconds\texit_status\tlog\tcommand\n' > "$STAGE_FILE"
|
|
|
|
run_logged() {
|
|
printf '+ '
|
|
printf '%q ' "$@"
|
|
printf '\n'
|
|
"$@"
|
|
}
|
|
|
|
run_stage() {
|
|
local stage=$1
|
|
shift
|
|
local log="$LOG_DIR/$stage.log"
|
|
local started_at
|
|
local ended_at
|
|
local started_epoch
|
|
local ended_epoch
|
|
local status
|
|
|
|
CURRENT_STAGE=$stage
|
|
CURRENT_LOG=$log
|
|
started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
started_epoch=$(date +%s)
|
|
{
|
|
printf 'stage=%s\n' "$stage"
|
|
printf 'started_utc=%s\n' "$started_at"
|
|
printf 'command='
|
|
printf '%q ' "$@"
|
|
printf '\n'
|
|
} | tee "$log"
|
|
|
|
set +e
|
|
(
|
|
set -e
|
|
cd "$REPO_ROOT"
|
|
"$@"
|
|
) 2>&1 | tee -a "$log"
|
|
status=${PIPESTATUS[0]}
|
|
set -e
|
|
|
|
ended_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
ended_epoch=$(date +%s)
|
|
printf 'ended_utc=%s\nduration_seconds=%s\nexit_status=%s\n' \
|
|
"$ended_at" "$((ended_epoch - started_epoch))" "$status" | tee -a "$log"
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t' \
|
|
"$stage" "$started_at" "$ended_at" "$((ended_epoch - started_epoch))" \
|
|
"$status" "$log" >> "$STAGE_FILE"
|
|
printf '%q ' "$@" >> "$STAGE_FILE"
|
|
printf '\n' >> "$STAGE_FILE"
|
|
|
|
return "$status"
|
|
}
|
|
|
|
require_command() {
|
|
local name=$1
|
|
command -v "$name" >/dev/null 2>&1 || {
|
|
printf 'Required command is missing: %s\n' "$name" >&2
|
|
return 1
|
|
}
|
|
}
|
|
|
|
assert_clean_repository() {
|
|
local status
|
|
status=$(git status --porcelain --untracked-files=normal)
|
|
if [[ -n "$status" ]]; then
|
|
printf 'Repository must be clean before running the closure SOP:\n%s\n' "$status" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
assert_no_matches() {
|
|
local description=$1
|
|
local pattern=$2
|
|
shift 2
|
|
local status
|
|
|
|
printf 'scan=%s pattern=%s\n' "$description" "$pattern"
|
|
set +e
|
|
rg -n --pcre2 -U "$pattern" "$@"
|
|
status=$?
|
|
set -e
|
|
if [[ $status -eq 0 ]]; then
|
|
printf 'Forbidden source debt found: %s\n' "$description" >&2
|
|
return 1
|
|
fi
|
|
if [[ $status -ne 1 ]]; then
|
|
printf 'Static scan failed: %s (rg exit %s)\n' "$description" "$status" >&2
|
|
return "$status"
|
|
fi
|
|
}
|
|
|
|
extract_counter() {
|
|
local trx=$1
|
|
local counter=$2
|
|
local value
|
|
value=$(rg -o "${counter}=\"[0-9]+\"" "$trx" | head -n 1 | tr -dc '0-9' || true)
|
|
printf '%s' "${value:-0}"
|
|
}
|
|
|
|
collect_sop_metrics() {
|
|
rg -o --no-filename \
|
|
-g '*.log' \
|
|
-g '*.trx' \
|
|
'SOP_METRIC [A-Za-z0-9_]+=[0-9]+' \
|
|
"$LOG_DIR" "$TRX_DIR" 2>/dev/null | sort -u
|
|
}
|
|
|
|
assert_no_skipped_tests() {
|
|
local target=$1
|
|
local found=0
|
|
local skipped=0
|
|
local trx
|
|
|
|
while IFS= read -r trx; do
|
|
found=1
|
|
skipped=$((skipped + $(extract_counter "$trx" notExecuted)))
|
|
done < <(find "$target" -type f -name '*.trx' -print | sort)
|
|
|
|
if [[ $found -eq 0 ]]; then
|
|
printf 'No TRX result was produced under %s\n' "$target" >&2
|
|
return 1
|
|
fi
|
|
if [[ $skipped -gt 0 ]]; then
|
|
printf 'Skipped tests are forbidden: skipped=%s target=%s\n' "$skipped" "$target" >&2
|
|
return 1
|
|
fi
|
|
|
|
printf 'skipped=0 target=%s\n' "$target"
|
|
}
|
|
|
|
run_test() {
|
|
local label=$1
|
|
local project=$2
|
|
local filter=${3:-}
|
|
local args
|
|
|
|
args=(
|
|
dotnet test "$project"
|
|
--no-restore
|
|
--no-build
|
|
--logger "trx;LogFileName=${label}.trx"
|
|
--results-directory "$TRX_DIR"
|
|
--logger "console;verbosity=normal"
|
|
)
|
|
if [[ -n "$filter" ]]; then
|
|
args+=(--filter "$filter")
|
|
fi
|
|
|
|
run_logged "${args[@]}"
|
|
assert_no_skipped_tests "$TRX_DIR/${label}.trx"
|
|
}
|
|
|
|
load_infrastructure_env() {
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
printf 'Infrastructure environment file is missing: %s\n' "$ENV_FILE" >&2
|
|
return 1
|
|
fi
|
|
# shellcheck disable=SC1090
|
|
source "$ENV_FILE"
|
|
}
|
|
|
|
compose_port() {
|
|
local service=$1
|
|
local container_port=$2
|
|
local endpoint
|
|
printf '+ docker compose -p %q -f %q port %q %q\n' \
|
|
"$COMPOSE_PROJECT" "$COMPOSE_FILE" "$service" "$container_port" >&2
|
|
endpoint=$(docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" port "$service" "$container_port")
|
|
printf 'endpoint=%s\n' "$endpoint" >&2
|
|
printf '%s' "${endpoint##*:}"
|
|
}
|
|
|
|
stage_preflight() {
|
|
local command
|
|
for command in dotnet docker git rg curl; do
|
|
require_command "$command"
|
|
done
|
|
run_logged dotnet --version
|
|
[[ $(dotnet --version) == 10.* ]] || {
|
|
printf '.NET SDK 10 is required.\n' >&2
|
|
return 1
|
|
}
|
|
run_logged docker --version
|
|
run_logged docker compose version
|
|
run_logged docker version --format '{{.Server.Version}}'
|
|
run_logged git --version
|
|
run_logged rg --version
|
|
run_logged curl --version
|
|
run_logged git rev-parse --show-toplevel
|
|
assert_clean_repository
|
|
}
|
|
|
|
stage_static_guards() {
|
|
local fixed_password_pattern='Password\s*=\s*(?:(?!REPLACE_IN_SECRETS|YOUR_[A-Z_]+)[A-Za-z0-9_-]{3,}(?:;|")|config\[[^]]+\]\s*\?\?\s*"[^"]+")'
|
|
local one_argument_exception_pattern='new\s+(?:[A-Za-z_][A-Za-z0-9_:]*\.)*(?:BusinessException|NotFoundException|UnauthorizedException)\s*\(\s*"'
|
|
local endpoint_dbcontext_pattern='class\s+[A-Za-z0-9_]+Endpoint\s*\([^)]*[A-Za-z0-9_]+DbContext\b'
|
|
|
|
assert_no_matches 'fixed password' "$fixed_password_pattern" src -g '*.cs' -g '*.json'
|
|
assert_no_matches 'one-argument business exception' "$one_argument_exception_pattern" src -g '*.cs'
|
|
assert_no_matches 'Endpoint DbContext dependency' "$endpoint_dbcontext_pattern" src/BuildFlow.Host/Endpoints -g '*.cs'
|
|
assert_no_matches 'InProcess bridge' 'InProcess(UserInfoService|FileStorageClient|AvatarUrlResolver)' src -g '*.cs'
|
|
assert_no_matches 'local wall-clock usage' 'DateTime\.Now' src -g '*.cs'
|
|
assert_no_matches 'detached workflow work' 'Task\.Run' src/modules/workflow src/BuildFlow.Host -g '*.cs'
|
|
}
|
|
|
|
stage_restore_build() {
|
|
run_logged dotnet restore "$SOLUTION"
|
|
run_logged dotnet build "$SOLUTION" --no-restore -warnaserror
|
|
}
|
|
|
|
stage_unit_tests() {
|
|
local source_guard_filter='FullyQualifiedName~EndpointConstructorsShouldNotDependOnDbContexts|FullyQualifiedName~HostShouldNotContainInProcessBridges|FullyQualifiedName~BusinessExceptionsShouldSpecifySemanticErrorCodes|FullyQualifiedName~SourceFilesShouldContainAtMostOneTopLevelClassRecordOrEnum'
|
|
local workflow_filter='FullyQualifiedName!~BuildFlow.Workflow.Tests.Reliability&FullyQualifiedName!~OpenXmlExportBoundaryTests&FullyQualifiedName!~FormAggregateQueryCountTests&FullyQualifiedName!~ImportJobBatchingTests'
|
|
|
|
run_test 04-source-architecture tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.csproj "$source_guard_filter"
|
|
run_test 04-host-architecture tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.csproj 'FullyQualifiedName~HostArchitectureTests'
|
|
run_test 04-sop-contract tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.csproj 'FullyQualifiedName~BackendOptimizationSopTests'
|
|
run_test 04-host tests/BuildFlow.Host.Tests/BuildFlow.Host.Tests.csproj
|
|
run_test 04-platform tests/BuildFlow.Platform.Tests/BuildFlow.Platform.Tests.csproj
|
|
run_test 04-scripting tests/BuildFlow.Scripting.Tests/BuildFlow.Scripting.Tests.csproj
|
|
run_test 04-file tests/BuildFlow.File.Tests/BuildFlow.File.Tests.csproj 'Category!=External'
|
|
run_test 04-rag tests/BuildFlow.Rag.Tests/BuildFlow.Rag.Tests.csproj 'FullyQualifiedName!~RedisChatMessageCacheTests'
|
|
run_test 04-im tests/BuildFlow.IM.Tests/BuildFlow.IM.Tests.csproj 'FullyQualifiedName!~SendMessageCommandHandlerTests'
|
|
run_test 04-workflow tests/BuildFlow.Workflow.Tests/BuildFlow.Workflow.Tests.csproj "$workflow_filter"
|
|
}
|
|
|
|
stage_start_infrastructure() {
|
|
local postgres_port
|
|
local redis_port
|
|
local mongo_port
|
|
local minio_port
|
|
|
|
run_logged docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" up -d --wait
|
|
postgres_port=$(compose_port postgres 5432)
|
|
redis_port=$(compose_port redis 6379)
|
|
mongo_port=$(compose_port mongo 27017)
|
|
minio_port=$(compose_port minio 9000)
|
|
|
|
{
|
|
printf 'export BUILDFLOW_WORKFLOW_TEST_CONNECTION=%q\n' "Host=127.0.0.1;Port=$postgres_port;Username=buildflow;Password=buildflow;Database=postgres"
|
|
printf 'export BUILDFLOW_MONITORING_TEST_CONNECTION=%q\n' "Host=127.0.0.1;Port=$postgres_port;Username=buildflow;Password=buildflow;Database=postgres"
|
|
printf 'export BUILDFLOW_RAG_TEST_POSTGRES_CONNECTION=%q\n' "Host=127.0.0.1;Port=$postgres_port;Username=buildflow;Password=buildflow;Database=postgres"
|
|
printf 'export BUILDFLOW_RAG_REDIS_TEST_CONNECTION=%q\n' "127.0.0.1:$redis_port,abortConnect=false"
|
|
printf 'export BUILDFLOW_IM_TEST_POSTGRES_CONNECTION=%q\n' "Host=127.0.0.1;Port=$postgres_port;Username=buildflow;Password=buildflow;Database=im_system_test"
|
|
printf 'export BUILDFLOW_IM_TEST_MONGO_CONNECTION=%q\n' "mongodb://buildflow:buildflow@127.0.0.1:$mongo_port/im_system_test?authSource=admin"
|
|
printf 'export BUILDFLOW_IM_TEST_REDIS_CONNECTION=%q\n' "127.0.0.1:$redis_port,abortConnect=false"
|
|
printf 'export BUILDFLOW_FILE_S3_TEST_ENDPOINT=%q\n' "http://127.0.0.1:$minio_port"
|
|
printf 'export BUILDFLOW_FILE_S3_TEST_ACCESS_KEY=%q\n' buildflowminio
|
|
printf 'export BUILDFLOW_FILE_S3_TEST_SECRET_KEY=%q\n' buildflowminiosecret
|
|
} > "$ENV_FILE"
|
|
sed -E 's/(SECRET_KEY=).*/\1<redacted>/' "$ENV_FILE"
|
|
}
|
|
|
|
stage_database_migrations() {
|
|
load_infrastructure_env
|
|
run_test 06-workflow-migrations tests/BuildFlow.Workflow.Tests/BuildFlow.Workflow.Tests.csproj 'FullyQualifiedName~WorkflowJobMigrationTests'
|
|
}
|
|
|
|
stage_integration_tests() {
|
|
load_infrastructure_env
|
|
run_test 07-monitoring-upsert tests/BuildFlow.Monitoring.Tests/BuildFlow.Monitoring.Tests.csproj
|
|
run_test 07-rag-redis tests/BuildFlow.Rag.Tests/BuildFlow.Rag.Tests.csproj 'FullyQualifiedName~RedisChatMessageCacheTests'
|
|
BUILDFLOW_IM_TEST_MONGO_CONNECTION=${BUILDFLOW_IM_TEST_MONGO_CONNECTION/im_system_test/im_unit_test} \
|
|
run_test 07-im-mongo tests/BuildFlow.IM.Tests/BuildFlow.IM.Tests.csproj 'FullyQualifiedName~SendMessageCommandHandlerTests'
|
|
run_test 07-im-signalr tests/BuildFlow.IM.Integration.Tests/BuildFlow.IM.Integration.Tests.csproj
|
|
run_test 07-file-s3 tests/BuildFlow.File.Tests/BuildFlow.File.Tests.csproj 'FullyQualifiedName~S3StorageServiceIntegrationTests'
|
|
run_test 07-rag-stream tests/BuildFlow.Rag.Tests/BuildFlow.Rag.Tests.csproj 'FullyQualifiedName~RagStreamQueryHandlerTests'
|
|
run_test 07-rag-pgvector tests/BuildFlow.Rag.Tests/BuildFlow.Rag.Tests.csproj 'FullyQualifiedName~RagPgVectorIntegrationTests'
|
|
run_test 07-host-sse tests/BuildFlow.Host.Tests/BuildFlow.Host.Tests.csproj 'FullyQualifiedName~RagStreamEndpointTests'
|
|
}
|
|
|
|
stage_recovery_tests() {
|
|
load_infrastructure_env
|
|
run_test 08-workflow-recovery tests/BuildFlow.Workflow.Tests/BuildFlow.Workflow.Tests.csproj 'FullyQualifiedName~WorkflowTimerProcessorTests|FullyQualifiedName~ImportJobClaimTests|FullyQualifiedName~ImportJobRecoveryTests'
|
|
}
|
|
|
|
stage_performance_tests() {
|
|
load_infrastructure_env
|
|
run_test 09-workflow-performance tests/BuildFlow.Workflow.Tests/BuildFlow.Workflow.Tests.csproj 'FullyQualifiedName~FormAggregateQueryCountTests|FullyQualifiedName~ImportJobBatchingTests|FullyQualifiedName~OpenXmlMemoryProfileTests|FullyQualifiedName~OpenXmlExportBoundaryTests'
|
|
run_test 09-rag-performance tests/BuildFlow.Rag.Tests/BuildFlow.Rag.Tests.csproj 'FullyQualifiedName~ChatContextWindowBuilderTests|FullyQualifiedName~RedisChatMessageCacheTests'
|
|
run_test 09-file-performance tests/BuildFlow.File.Tests/BuildFlow.File.Tests.csproj 'FullyQualifiedName~FileSizeVerificationProcessorTests'
|
|
}
|
|
|
|
stage_full_solution() {
|
|
load_infrastructure_env
|
|
run_logged dotnet test "$SOLUTION" \
|
|
--no-restore \
|
|
--no-build \
|
|
--logger 'trx;LogFilePrefix=10-full-solution' \
|
|
--results-directory "$TRX_DIR/10-full-solution" \
|
|
--logger 'console;verbosity=minimal'
|
|
assert_no_skipped_tests "$TRX_DIR/10-full-solution"
|
|
}
|
|
|
|
write_summary() {
|
|
local status=$1
|
|
local total=0
|
|
local passed=0
|
|
local failed=0
|
|
local skipped=0
|
|
local trx
|
|
local failure_log='none'
|
|
|
|
while IFS= read -r trx; do
|
|
total=$((total + $(extract_counter "$trx" total)))
|
|
passed=$((passed + $(extract_counter "$trx" passed)))
|
|
failed=$((failed + $(extract_counter "$trx" failed)))
|
|
skipped=$((skipped + $(extract_counter "$trx" notExecuted)))
|
|
done < <(find "$TRX_DIR" -type f -name '*.trx' -print 2>/dev/null | sort)
|
|
|
|
if [[ "$status" != success ]]; then
|
|
failure_log=${CURRENT_LOG:-none}
|
|
fi
|
|
|
|
{
|
|
printf '# Backend Optimization SOP Report\n\n'
|
|
printf -- '- Run: `%s`\n' "$RUN_ID"
|
|
printf -- '- Status: `%s`\n' "$status"
|
|
printf -- '- Compose project: `%s`\n' "$COMPOSE_PROJECT"
|
|
printf -- '- Tests: total `%s`, passed `%s`, failed `%s`, skipped `%s`\n' "$total" "$passed" "$failed" "$skipped"
|
|
printf -- '- Failure log: `%s`\n\n' "$failure_log"
|
|
printf '## Stages\n\n'
|
|
printf '| Stage | Started UTC | Ended UTC | Seconds | Exit | Log | Command |\n'
|
|
printf '|---|---|---|---:|---:|---|---|\n'
|
|
tail -n +2 "$STAGE_FILE" | while IFS=$'\t' read -r stage started ended duration exit_code log command; do
|
|
printf '| `%s` | `%s` | `%s` | %s | %s | `%s` | `%s` |\n' \
|
|
"$stage" "$started" "$ended" "$duration" "$exit_code" "$log" "$command"
|
|
done
|
|
printf '\n## Metrics\n\n'
|
|
if ! collect_sop_metrics; then
|
|
printf 'No SOP_METRIC lines were produced.\n'
|
|
fi
|
|
printf '\n## Evidence\n\n'
|
|
printf -- '- Stage ledger: `%s`\n' "$STAGE_FILE"
|
|
printf -- '- Logs: `%s`\n' "$LOG_DIR"
|
|
printf -- '- TRX: `%s`\n' "$TRX_DIR"
|
|
} > "$SUMMARY_FILE"
|
|
}
|
|
|
|
cleanup() {
|
|
local status=$?
|
|
local cleanup_status=0
|
|
trap - EXIT
|
|
set +e
|
|
|
|
if [[ $INFRA_STARTED -eq 1 ]]; then
|
|
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" down -v \
|
|
>> "$ARTIFACT_DIR/cleanup.log" 2>&1
|
|
cleanup_status=$?
|
|
fi
|
|
if [[ $status -eq 0 && $cleanup_status -ne 0 ]]; then
|
|
status=$cleanup_status
|
|
CURRENT_STAGE=cleanup
|
|
CURRENT_LOG="$ARTIFACT_DIR/cleanup.log"
|
|
fi
|
|
|
|
if [[ $status -ne 0 || $RUN_COMPLETED -ne 1 ]]; then
|
|
rm -f "$SUCCESS_FILE"
|
|
write_summary failure
|
|
printf 'Backend optimization SOP failed in stage %s. Report: %s\n' \
|
|
"$CURRENT_STAGE" "$SUMMARY_FILE" >&2
|
|
fi
|
|
exit "$status"
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
run_stage '01-preflight' stage_preflight
|
|
run_stage '02-static-guards' stage_static_guards
|
|
run_stage '03-restore-build' stage_restore_build
|
|
run_stage '04-unit-tests' stage_unit_tests
|
|
INFRA_STARTED=1
|
|
run_stage '05-start-infrastructure' stage_start_infrastructure
|
|
run_stage '06-database-migrations' stage_database_migrations
|
|
run_stage '07-integration-tests' stage_integration_tests
|
|
run_stage '08-recovery-tests' stage_recovery_tests
|
|
run_stage '09-performance-tests' stage_performance_tests
|
|
run_stage '10-full-solution' stage_full_solution
|
|
assert_no_skipped_tests "$TRX_DIR"
|
|
write_summary success
|
|
printf 'success\n' > "$SUCCESS_FILE"
|
|
RUN_COMPLETED=1
|
|
printf 'Backend optimization SOP completed. Report: %s\n' "$SUMMARY_FILE"
|