chore(deploy): finalize 0309 SAE rollout updates

Sync deployment documentation to the final successful SAE state and clear pending deployment checklist items. Include backend/frontend/R hardening and diagnostics improvements required for stable production behavior.

Made-with: Cursor
This commit is contained in:
2026-03-09 22:27:11 +08:00
parent d30bf95815
commit 971e903acf
23 changed files with 810 additions and 180 deletions

View File

@@ -40,6 +40,9 @@ RUN R -e "install.packages(c( \
'meta' \
), repos='https://cloud.r-project.org/', Ncpus=2)"
# 构建期校验:关键包缺失则直接失败,阻止坏镜像发布
RUN R -e "required <- c('plumber','jsonlite','ggplot2','glue','dplyr','tidyr','base64enc','yaml','car','httr','scales','gridExtra','gtsummary','gt','broom','meta'); installed <- rownames(installed.packages()); missing <- setdiff(required, installed); if (length(missing) > 0) { stop(paste('Missing required R packages:', paste(missing, collapse=', '))) } else { cat('All required R packages installed.\\n') }"
# ===== 安全加固:创建非特权用户 =====
RUN useradd -m -s /bin/bash appuser

View File

@@ -11,6 +11,9 @@ library(jsonlite)
# 环境配置
DEV_MODE <- Sys.getenv("DEV_MODE", "false") == "true"
# 空值合并操作符(避免 `%||%` 未定义导致 execute-code 入口报错)
`%||%` <- function(x, y) if (is.null(x)) y else x
# 加载公共函数
source("utils/error_codes.R")
source("utils/data_loader.R")
@@ -116,6 +119,32 @@ function() {
)
}
#* 诊断:返回 R 运行时包清单(只读)
#* @get /api/v1/debug/packages
#* @serializer unboxedJSON
function() {
required_packages <- c(
"plumber", "jsonlite", "ggplot2", "glue", "dplyr", "tidyr",
"base64enc", "yaml", "car", "httr", "scales", "gridExtra",
"gtsummary", "gt", "broom", "meta"
)
installed <- rownames(installed.packages())
missing <- setdiff(required_packages, installed)
list(
status = "ok",
r_version = R.version.string,
dev_mode = DEV_MODE,
lib_paths = .libPaths(),
required_count = length(required_packages),
installed_count = length(installed),
missing_required = missing,
required_status = if (length(missing) == 0) "complete" else "incomplete",
sample_installed = head(sort(installed), 120)
)
}
#* JIT Guardrails Check
#* @post /api/v1/guardrails/jit
#* @serializer unboxedJSON

View File

@@ -60,6 +60,12 @@ ERROR_CODES <- list(
type = "system",
message_template = "缺少依赖包: {package}",
user_hint = "请联系管理员"
),
E102_FUNCTION_NOT_FOUND = list(
code = "E102",
type = "business",
message_template = "找不到函数: {func}",
user_hint = "请检查函数名是否正确,或确认已加载相关包"
)
)
@@ -76,7 +82,7 @@ R_ERROR_MAPPING <- list(
"not meaningful for factors" = "E002_TYPE_MISMATCH",
"missing value where TRUE/FALSE needed" = "E100_INTERNAL_ERROR",
"replacement has" = "E100_INTERNAL_ERROR",
"could not find function" = "E101_PACKAGE_MISSING",
"could not find function" = "E102_FUNCTION_NOT_FOUND",
"there is no package called" = "E101_PACKAGE_MISSING",
"cannot open the connection" = "E100_INTERNAL_ERROR",
"singular gradient" = "E005_SINGULAR_MATRIX",
@@ -167,6 +173,34 @@ map_r_error <- function(raw_error_msg) {
for (pattern in names(R_ERROR_MAPPING)) {
if (grepl(pattern, raw_error_msg, ignore.case = TRUE)) {
error_key <- R_ERROR_MAPPING[[pattern]]
# E101: 提取缺失包名there is no package called 'xxx'
if (error_key == "E101_PACKAGE_MISSING") {
pkg <- "unknown"
m <- regexec("there is no package called ['\"]([^'\"]+)['\"]", raw_error_msg, ignore.case = TRUE)
mm <- regmatches(raw_error_msg, m)[[1]]
if (length(mm) >= 2) pkg <- mm[2]
return(make_error(ERROR_CODES[[error_key]], package = pkg))
}
# E102: 提取找不到的函数名could not find function "xxx"
if (error_key == "E102_FUNCTION_NOT_FOUND") {
func <- "unknown"
m <- regexec("could not find function ['\"]([^'\"]+)['\"]", raw_error_msg, ignore.case = TRUE)
mm <- regmatches(raw_error_msg, m)[[1]]
if (length(mm) >= 2) func <- mm[2]
return(make_error(ERROR_CODES[[error_key]], func = func))
}
# E001: 尝试提取缺失对象名object 'xxx' not found
if (error_key == "E001_COLUMN_NOT_FOUND") {
col <- "unknown"
m <- regexec("object ['\"]([^'\"]+)['\"] not found", raw_error_msg, ignore.case = TRUE)
mm <- regmatches(raw_error_msg, m)[[1]]
if (length(mm) >= 2) col <- mm[2]
return(make_error(ERROR_CODES[[error_key]], col = col))
}
return(make_error(ERROR_CODES[[error_key]], details = raw_error_msg))
}
}