feat: require production infrastructure evidence
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# 阿里云单机演示部署
|
# 阿里云单机演示部署
|
||||||
|
|
||||||
该配置用于单机公网演示,不是生产验收配置。只向公网发布HTTP 80和HTTPS 443;PostgreSQL、Redis、MinIO、Keycloak内部端口以及API/Worker端口均不映射到宿主机。当前阿里云演示机使用Let's Encrypt短期IP证书,`certbot-ip-renew.timer`负责自动续期。单机备份覆盖PostgreSQL、Redis、MinIO对象和当前Keycloak数据目录,并执行数据库级隔离恢复校验;五分钟健康检查覆盖公网入口、OIDC、容器、备份、证书、定时任务和磁盘。跨故障域备份、真实告警接收器、真实RTC、模型、内容安全和云KMS仍按阶段8验收清单执行。
|
该配置用于单机公网演示,不是生产验收配置。只向公网发布HTTP 80和HTTPS 443;PostgreSQL、Redis、MinIO、Keycloak内部端口以及API/Worker端口均不映射到宿主机。当前阿里云演示机使用Let's Encrypt短期IP证书,`certbot-ip-renew.timer`负责自动续期。单机备份覆盖PostgreSQL、Redis、MinIO对象和当前Keycloak数据目录,并执行数据库级隔离恢复校验;五分钟健康检查覆盖部署模式、公网入口、OIDC、容器、模拟服务、备份、外部告警、证书、定时任务和磁盘。跨故障域备份、真实告警接收器、真实RTC、模型、内容安全和云KMS仍按阶段8验收清单执行。
|
||||||
|
|
||||||
服务器至少需要Docker Engine 29与Compose 2.40。首次部署先生成权限为600的服务器专用配置:
|
服务器至少需要Docker Engine 29与Compose 2.40。首次部署先生成权限为600的服务器专用配置:
|
||||||
|
|
||||||
@@ -54,7 +54,9 @@ sudo systemctl show platform-health-monitor.service -p Result -p ExecMainStatus
|
|||||||
python3 -m json.tool /var/lib/data-terminal-cloud-monitor/status.json
|
python3 -m json.tool /var/lib/data-terminal-cloud-monitor/status.json
|
||||||
```
|
```
|
||||||
|
|
||||||
监测任务每五分钟执行,正常状态为`healthy`。它还要求宿主机所有非回环TCP监听只能使用22、80和443,避免安全组误放行与错误容器端口映射叠加后暴露数据库或缓存。默认只记录结构化状态和systemd日志;在`/etc/data-terminal-cloud/monitor.env`配置`ALERT_WEBHOOK_URL`后,状态变化会发送带时间戳和HMAC-SHA256签名的JSON通知。完整检查项、签名规则、阈值和失败分支见[《单机云端健康监测与告警手册》](../../docs/server-health-monitoring.md)。
|
监测任务每五分钟执行,正常状态为`healthy`。默认`HOST_DEPLOYMENT_PROFILE=demo`,要求两个模拟服务完整运行;切到`production`后则要求模拟服务全部停止。它还要求宿主机所有非回环TCP监听只能使用22、80和443,避免安全组误放行与错误容器端口映射叠加后暴露数据库或缓存。默认只记录结构化状态和systemd日志;在`/etc/data-terminal-cloud/monitor.env`配置`ALERT_WEBHOOK_URL`后,状态变化会发送带时间戳和HMAC-SHA256签名的JSON通知。完整检查项、签名规则、阈值和失败分支见[《单机云端健康监测与告警手册》](../../docs/server-health-monitoring.md)。
|
||||||
|
|
||||||
|
生产候选不能只修改部署模式。生产验收快照同时要求:`HOST_DEPLOYMENT_PROFILE=production`、OSS异地备份最近一次上传与回读验证成功、真实外部告警接收器已配置、`mock-provider`和`mock-alert-receiver`均不运行。任一条件缺失都会阻断生产基础设施门禁,但不会影响`LAB`或`STAGING`联调。
|
||||||
|
|
||||||
## OSS异地备份适配
|
## OSS异地备份适配
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ alert_webhook_secret_file=${ALERT_WEBHOOK_SECRET_FILE:-/etc/data-terminal-cloud/
|
|||||||
platform_health_report_url=${PLATFORM_HEALTH_REPORT_URL:-}
|
platform_health_report_url=${PLATFORM_HEALTH_REPORT_URL:-}
|
||||||
platform_health_report_secret_file=${PLATFORM_HEALTH_REPORT_SECRET_FILE:-/etc/data-terminal-cloud/platform-health-callback-secret}
|
platform_health_report_secret_file=${PLATFORM_HEALTH_REPORT_SECRET_FILE:-/etc/data-terminal-cloud/platform-health-callback-secret}
|
||||||
expected_public_tcp_ports=${EXPECTED_PUBLIC_TCP_PORTS:-22,80,443}
|
expected_public_tcp_ports=${EXPECTED_PUBLIC_TCP_PORTS:-22,80,443}
|
||||||
|
deployment_profile=${HOST_DEPLOYMENT_PROFILE:-demo}
|
||||||
if [[ -f "$offsite_config_file" ]]; then
|
if [[ -f "$offsite_config_file" ]]; then
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
source "$offsite_config_file"
|
source "$offsite_config_file"
|
||||||
@@ -41,6 +42,10 @@ if [[ ! "$expected_public_tcp_ports" =~ ^[0-9]+(,[0-9]+)*$ ]]; then
|
|||||||
echo "EXPECTED_PUBLIC_TCP_PORTS must contain comma-separated integers" >&2
|
echo "EXPECTED_PUBLIC_TCP_PORTS must contain comma-separated integers" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
if [[ "$deployment_profile" != demo && "$deployment_profile" != production ]]; then
|
||||||
|
echo "HOST_DEPLOYMENT_PROFILE must be demo or production" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
compose=(docker compose -f "$project_root/compose.yaml" -f "$project_root/deploy/server/compose.override.yaml" --env-file "$project_root/deploy/server/.env")
|
compose=(docker compose -f "$project_root/compose.yaml" -f "$project_root/deploy/server/compose.override.yaml" --env-file "$project_root/deploy/server/.env")
|
||||||
mkdir -p "$state_dir"
|
mkdir -p "$state_dir"
|
||||||
@@ -73,6 +78,21 @@ record_check() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record_check deployment_profile healthy "$deployment_profile"
|
||||||
|
if [[ -z "$alert_webhook_url" ]]; then
|
||||||
|
if [[ "$deployment_profile" == production ]]; then
|
||||||
|
record_check external_alert_receiver critical not_configured
|
||||||
|
else
|
||||||
|
record_check external_alert_receiver healthy not_configured
|
||||||
|
fi
|
||||||
|
elif [[ ! -r "$alert_webhook_secret_file" ]]; then
|
||||||
|
record_check external_alert_receiver critical secret_missing
|
||||||
|
elif (( $(tr -d '\r\n' <"$alert_webhook_secret_file" | wc -c) < 32 )); then
|
||||||
|
record_check external_alert_receiver critical secret_invalid
|
||||||
|
else
|
||||||
|
record_check external_alert_receiver healthy configured
|
||||||
|
fi
|
||||||
|
|
||||||
public_origin=$(sed -n 's/^PUBLIC_ORIGIN=//p' "$project_root/deploy/server/.env")
|
public_origin=$(sed -n 's/^PUBLIC_ORIGIN=//p' "$project_root/deploy/server/.env")
|
||||||
if [[ "$public_origin" == https://* ]]; then
|
if [[ "$public_origin" == https://* ]]; then
|
||||||
record_check public_origin healthy "$public_origin"
|
record_check public_origin healthy "$public_origin"
|
||||||
@@ -113,7 +133,10 @@ else
|
|||||||
record_check oidc_issuer critical request_failed
|
record_check oidc_issuer critical request_failed
|
||||||
fi
|
fi
|
||||||
|
|
||||||
required_services=(postgres redis minio keycloak api callbacks worker web gateway mock-provider mock-alert-receiver)
|
required_services=(postgres redis minio keycloak api callbacks worker web gateway)
|
||||||
|
if [[ "$deployment_profile" == demo ]]; then
|
||||||
|
required_services+=(mock-provider mock-alert-receiver)
|
||||||
|
fi
|
||||||
for service in "${required_services[@]}"; do
|
for service in "${required_services[@]}"; do
|
||||||
container=$("${compose[@]}" ps -q "$service")
|
container=$("${compose[@]}" ps -q "$service")
|
||||||
if [[ -z "$container" ]]; then
|
if [[ -z "$container" ]]; then
|
||||||
@@ -131,6 +154,25 @@ for service in "${required_services[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
mock_services_running=()
|
||||||
|
for mock_service in mock-provider mock-alert-receiver; do
|
||||||
|
mock_container=$("${compose[@]}" ps -q "$mock_service")
|
||||||
|
if [[ -n "$mock_container" && $(docker inspect -f '{{.State.Status}}' "$mock_container") == running ]]; then
|
||||||
|
mock_services_running+=("$mock_service")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$deployment_profile" == production ]]; then
|
||||||
|
if (( ${#mock_services_running[@]} > 0 )); then
|
||||||
|
record_check mock_services critical unexpected_running
|
||||||
|
else
|
||||||
|
record_check mock_services healthy disabled
|
||||||
|
fi
|
||||||
|
elif (( ${#mock_services_running[@]} == 2 )); then
|
||||||
|
record_check mock_services healthy enabled_for_demo
|
||||||
|
else
|
||||||
|
record_check mock_services critical incomplete_demo_stack
|
||||||
|
fi
|
||||||
|
|
||||||
latest_backup=$(find "$backup_root" -maxdepth 1 -type f -name 'platform-*.tar.gz.enc' -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -n1 | cut -d' ' -f2- || true)
|
latest_backup=$(find "$backup_root" -maxdepth 1 -type f -name 'platform-*.tar.gz.enc' -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -n1 | cut -d' ' -f2- || true)
|
||||||
if [[ -z "$latest_backup" || ! -f "$latest_backup" ]]; then
|
if [[ -z "$latest_backup" || ! -f "$latest_backup" ]]; then
|
||||||
record_check backup_freshness critical missing
|
record_check backup_freshness critical missing
|
||||||
@@ -168,7 +210,11 @@ except Exception: print("invalid|0|")' "$offsite_state_file")
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
if [[ "$deployment_profile" == production ]]; then
|
||||||
|
record_check offsite_backup critical not_required
|
||||||
|
else
|
||||||
record_check offsite_backup healthy not_required
|
record_check offsite_backup healthy not_required
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
backup_result=$(systemctl show platform-backup.service -p Result --value 2>/dev/null || true)
|
backup_result=$(systemctl show platform-backup.service -p Result --value 2>/dev/null || true)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ if [[ ! -e /etc/data-terminal-cloud/monitor.env ]]; then
|
|||||||
report_url="$public_origin/callbacks/v1/platform-health"
|
report_url="$public_origin/callbacks/v1/platform-health"
|
||||||
fi
|
fi
|
||||||
cat >/etc/data-terminal-cloud/monitor.env <<EOF
|
cat >/etc/data-terminal-cloud/monitor.env <<EOF
|
||||||
|
HOST_DEPLOYMENT_PROFILE=demo
|
||||||
PLATFORM_HEALTH_REPORT_URL=$report_url
|
PLATFORM_HEALTH_REPORT_URL=$report_url
|
||||||
PLATFORM_HEALTH_REPORT_SECRET_FILE=/etc/data-terminal-cloud/platform-health-callback-secret
|
PLATFORM_HEALTH_REPORT_SECRET_FILE=/etc/data-terminal-cloud/platform-health-callback-secret
|
||||||
ALERT_WEBHOOK_URL=
|
ALERT_WEBHOOK_URL=
|
||||||
|
|||||||
@@ -437,7 +437,7 @@
|
|||||||
|
|
||||||
## 9. 当前实施状态与后续顺序
|
## 9. 当前实施状态与后续顺序
|
||||||
|
|
||||||
截至2026-08-26,本地工程已经完成阶段1—7的本地开发与验证闭环,并在阿里云单机环境部署标准HTTPS、OIDC、加密本机备份、主机健康签名上报和七项发布候选技术门禁。1000活跃通话、50 RPS持续30分钟、500回调/秒、500条SSE、10000媒体分片、100并发删除、隔离数据库恢复、进程/Redis恢复、供应商故障、安全扫描、真实Chrome/Edge兼容、全仓检查、跨功能烟测和无测试残留对账均已完成。后续顺序为:
|
截至2026-08-26,本地工程已经完成阶段1—7的本地开发与验证闭环,并在阿里云单机环境部署标准HTTPS、OIDC、加密本机备份、主机健康签名上报和八项发布候选技术门禁。生产快照不能用演示部署模式、未验证的异地备份、未配置的外部告警或仍在运行的Mock服务冒充可上线。1000活跃通话、50 RPS持续30分钟、500回调/秒、500条SSE、10000媒体分片、100并发删除、隔离数据库恢复、进程/Redis恢复、供应商故障、安全扫描、真实Chrome/Edge兼容、全仓检查、跨功能烟测和无测试残留对账均已完成。后续顺序为:
|
||||||
|
|
||||||
1. 外部基础设施探针、运营预检台账、联调问题门禁、可归档技术验收快照、试点日/周证据和v3候选证据包均已完成;生产内容安全已具备本地前置拦截、托管HTTP策略、输入/输出双检和安全失败语义;远程CI供应链已具备绑定提交、运行、14项检查、双镜像摘要和三份SBOM的不可豁免门禁。`PILOT`周期、远程CI、真机固件、真实终端RTC六通矩阵、真实Agent生成/Embedding/内容安全质量、真实告警网关、云KMS/HSM、四类生产等价恢复、七类生产基础设施和Safari精简矩阵均不可由本地模拟报告放行。下一步在目标环境逐项替换证据索引中的`PENDING`。
|
1. 外部基础设施探针、运营预检台账、联调问题门禁、可归档技术验收快照、试点日/周证据和v3候选证据包均已完成;生产内容安全已具备本地前置拦截、托管HTTP策略、输入/输出双检和安全失败语义;远程CI供应链已具备绑定提交、运行、14项检查、双镜像摘要和三份SBOM的不可豁免门禁。`PILOT`周期、远程CI、真机固件、真实终端RTC六通矩阵、真实Agent生成/Embedding/内容安全质量、真实告警网关、云KMS/HSM、四类生产等价恢复、七类生产基础设施和Safari精简矩阵均不可由本地模拟报告放行。下一步在目标环境逐项替换证据索引中的`PENDING`。
|
||||||
2. OSS异地备份已完成无长期AK的IMDSv2/ECS RAM角色适配;待云账号侧绑定角色、创建私有Bucket后执行真实上传回读验收。随后待真实火山RTC/豆包、大模型/Embedding账号、终端硬件、外部告警接收器和其余生产资源到位,完成阶段8真机联调、家庭试点、预生产恢复与灰度发布。
|
2. OSS异地备份已完成无长期AK的IMDSv2/ECS RAM角色适配;待云账号侧绑定角色、创建私有Bucket后执行真实上传回读验收。随后待真实火山RTC/豆包、大模型/Embedding账号、终端硬件、外部告警接收器和其余生产资源到位,完成阶段8真机联调、家庭试点、预生产恢复与灰度发布。
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 开发状态与门禁记录
|
# 开发状态与门禁记录
|
||||||
|
|
||||||
> 更新日期:2026-08-26
|
> 更新日期:2026-08-26
|
||||||
> 当前接口契约版本:0.66.0
|
> 当前接口契约版本:0.67.0
|
||||||
> 当前主里程碑:M8目标环境联调、试点与上线验收(外部条件待提供)
|
> 当前主里程碑:M8目标环境联调、试点与上线验收(外部条件待提供)
|
||||||
|
|
||||||
## 阶段状态
|
## 阶段状态
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
| 第五阶段:SaaS核心业务开发 | 本地业务闭环完成 | 已完成家庭成员邀请与角色协作、长者、稳定事实、五类分项授权、设备读取/配置/绑定、可靠命令、签名固件灰度/暂停/回滚和逐设备升级进度、通话查询、双轨录音状态/授权/播放下载的数据库、服务、HTTP契约与React工作台;服务端隔离、ETag、审计、纵向烟测及Chrome/Edge兼容检查均通过 | 固件生产签名设施、macOS Safari和类生产容量仍待外部环境验证 |
|
| 第五阶段:SaaS核心业务开发 | 本地业务闭环完成 | 已完成家庭成员邀请与角色协作、长者、稳定事实、五类分项授权、设备读取/配置/绑定、可靠命令、签名固件灰度/暂停/回滚和逐设备升级进度、通话查询、双轨录音状态/授权/播放下载的数据库、服务、HTTP契约与React工作台;服务端隔离、ETag、审计、纵向烟测及Chrome/Edge兼容检查均通过 | 固件生产签名设施、macOS Safari和类生产容量仍待外部环境验证 |
|
||||||
| 第六阶段:Agent内容与记忆闭环 | 本地开发闭环完成,生产模型协议已接入 | 最终话轮、可切换本地/火山方舟内容生成器与Embedding、事务外模型调用、摘要、开放话题、故事/事实候选、人工版本审核、授权门禁、已确认记忆、索引失效、固定边界召回及React审核页面均已实现;端到端烟测通过 | 真实方舟API Key、内容效果与召回质量验收、模型评测和真实内容安全策略待完成 |
|
| 第六阶段:Agent内容与记忆闭环 | 本地开发闭环完成,生产模型协议已接入 | 最终话轮、可切换本地/火山方舟内容生成器与Embedding、事务外模型调用、摘要、开放话题、故事/事实候选、人工版本审核、授权门禁、已确认记忆、索引失效、固定边界召回及React审核页面均已实现;端到端烟测通过 | 真实方舟API Key、内容效果与召回质量验收、模型评测和真实内容安全策略待完成 |
|
||||||
| 第七阶段:生产加固 | 本地证据持续收敛 | 记忆可靠删除、隐私访问/导出/删除、运营配置灰度、用量费用额度、告警处置闭环、可重试HMAC告警Webhook及不污染正式告警的接收器联通测试、三类Grafana仪表盘、证书到期监测、OIDC/方舟/RTC外部基础设施探针、本地密钥轮换、生产配置拒绝启动门禁、全部本地专项容量、隔离恢复、供应商故障、安全扫描及Chrome/Edge兼容均通过;主要业务核心包已达到70%覆盖率,供应商客户端达到关键分支85%目标 | 全仓覆盖率及HTTP公共层仍低于需求目标;真实告警网关和值班组映射、生产等价容量、云密钥托管和Safari仍待完成 |
|
| 第七阶段:生产加固 | 本地证据持续收敛 | 记忆可靠删除、隐私访问/导出/删除、运营配置灰度、用量费用额度、告警处置闭环、可重试HMAC告警Webhook及不污染正式告警的接收器联通测试、三类Grafana仪表盘、证书到期监测、OIDC/方舟/RTC外部基础设施探针、本地密钥轮换、生产配置拒绝启动门禁、全部本地专项容量、隔离恢复、供应商故障、安全扫描及Chrome/Edge兼容均通过;主要业务核心包已达到70%覆盖率,供应商客户端达到关键分支85%目标 | 全仓覆盖率及HTTP公共层仍低于需求目标;真实告警网关和值班组映射、生产等价容量、云密钥托管和Safari仍待完成 |
|
||||||
| 第八阶段:联调试点与上线 | 本地准备完成,外部联调待执行 | 外部依赖预检、联调问题门禁、候选验收快照、阻断告警定位、接收器联通测试、内容安全TLS预检和按业务时区生成的试点日/周报均已形成可执行入口;本地真实OIDC、Worker、HMAC验签接收、六项技术门禁及日/周报下载通过 | 远程CI、真实投诉证据、告警网关和值班组映射、真实供应商/真机联调、家庭试点、预生产演练和生产发布 |
|
| 第八阶段:联调试点与上线 | 本地准备完成,外部联调待执行 | 外部依赖预检、联调问题门禁、候选验收快照、阻断告警定位、接收器联通测试、内容安全TLS预检和按业务时区生成的试点日/周报均已形成可执行入口;本地真实OIDC、Worker、HMAC验签接收、八项技术门禁及日/周报下载通过;生产快照不能再用演示模式、未启用异地备份、未配置外部告警或仍运行Mock服务冒充可上线 | 远程CI、真实投诉证据、告警网关和值班组映射、真实供应商/真机联调、家庭试点、预生产演练和生产发布 |
|
||||||
|
|
||||||
## 本轮验证记录
|
## 本轮验证记录
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
| 隐私删除并发 | 100个并发受理全部成功、HTTP失败率0%、提交p95 385.15ms;11.58秒内600个子任务、100个墓碑和100个主体冻结全部对账 |
|
| 隐私删除并发 | 100个并发受理全部成功、HTTP失败率0%、提交p95 385.15ms;11.58秒内600个子任务、100个墓碑和100个主体冻结全部对账 |
|
||||||
| PostgreSQL隔离恢复 | 实际`pg_dump`/`pg_restore`到随机隔离库,迁移`22|false`、66张表、190个外键和关键计数一致;本地约5.14秒,临时库/备份已清理 |
|
| PostgreSQL隔离恢复 | 实际`pg_dump`/`pg_restore`到随机隔离库,迁移`22|false`、66张表、190个外键和关键计数一致;本地约5.14秒,临时库/备份已清理 |
|
||||||
| 阿里云单机加密备份与恢复 | 目标服务器已安装每日systemd备份任务,覆盖PostgreSQL、认证Redis RDB、MinIO全部桶和暂停后的Keycloak数据目录;归档使用AES-256-CBC/PBKDF2加密、逐文件SHA-256并保留14天。首次实际归档成功,数据库从V38备份恢复到随机隔离库后`38|false|2|3|73|215`六项证据一致,归档内Redis RDB格式有效、Keycloak 3个文件一致、临时恢复库为0;同机备份尚不作为跨故障域生产恢复证据 |
|
| 阿里云单机加密备份与恢复 | 目标服务器已安装每日systemd备份任务,覆盖PostgreSQL、认证Redis RDB、MinIO全部桶和暂停后的Keycloak数据目录;归档使用AES-256-CBC/PBKDF2加密、逐文件SHA-256并保留14天。首次实际归档成功,数据库从V38备份恢复到随机隔离库后`38|false|2|3|73|215`六项证据一致,归档内Redis RDB格式有效、Keycloak 3个文件一致、临时恢复库为0;同机备份尚不作为跨故障域生产恢复证据 |
|
||||||
| 阿里云单机健康监测 | 目标服务器已安装五分钟systemd健康检查,结构化状态覆盖HTTPS、宿主机公网监听端口、OIDC签发地址、11个常驻容器、本机/异地备份状态与摘要、备份执行结果、三个运行timer、短期IP证书剩余时间和磁盘阈值。监听策略只允许非回环TCP 22/80/443,回环数据库端口不误报,额外端口会进入critical。实际正常路径23项全部healthy;把本机备份最大年龄临时设为0秒后仅`backup_freshness`进入critical并退出1。可选Webhook仅在状态变化时发送带时间戳和HMAC-SHA256的脱敏状态;真实接收器尚未配置,因此不作为值班通知送达证据 |
|
| 阿里云单机健康监测 | 目标服务器已安装五分钟systemd健康检查,结构化状态覆盖部署模式、外部告警配置、HTTPS、宿主机公网监听端口、OIDC签发地址、核心容器、模拟服务、本机/异地备份状态与摘要、备份执行结果、三个运行timer、短期IP证书剩余时间和磁盘阈值。监听策略只允许非回环TCP 22/80/443,回环数据库端口不误报,额外端口会进入critical。演示正常路径26项全部healthy;生产模式要求模拟服务退出。可选Webhook仅在状态变化时发送带时间戳和HMAC-SHA256的脱敏状态;真实接收器尚未配置,因此不作为值班通知送达证据 |
|
||||||
| 阿里云OSS异地备份适配 | 目标服务器已按官方SHA-256安装ossutil 2.3.0,新增每日OSS同步任务、加密归档及摘要上传、远端回读复算、原子状态和健康强制门禁。ECS正式路径现使用IMDSv2和`EcsRamRole`自动短期凭据,不保存长期AK;隔离配置模式验证上传/回读2个文件并得到`verified`,目标实例缺角色时退出4并记录`failed/ecs_ram_role_missing`。2026-08-26实测地域`cn-hangzhou`且角色元数据404;当前仍无RAM角色和Bucket,正式任务保持`disabled/not_enabled`且健康状态为`not_required`,不能作为真实异地备份证据 |
|
| 阿里云OSS异地备份适配 | 目标服务器已按官方SHA-256安装ossutil 2.3.0,新增每日OSS同步任务、加密归档及摘要上传、远端回读复算、原子状态和健康强制门禁。ECS正式路径现使用IMDSv2和`EcsRamRole`自动短期凭据,不保存长期AK;隔离配置模式验证上传/回读2个文件并得到`verified`,目标实例缺角色时退出4并记录`failed/ecs_ram_role_missing`。2026-08-26实测地域`cn-hangzhou`且角色元数据404;当前仍无RAM角色和Bucket,正式任务保持`disabled/not_enabled`且健康状态为`not_required`,不能作为真实异地备份证据 |
|
||||||
| 进程与Redis恢复 | API/Callbacks/Worker重启恢复;停机期间Outbox仅执行一次;Redis 14个热状态键清空后由签名设备请求重建,PostgreSQL权威计数不变 |
|
| 进程与Redis恢复 | API/Callbacks/Worker重启恢复;停机期间Outbox仅执行一次;Redis 14个热状态键清空后由签名设备请求重建,PostgreSQL权威计数不变 |
|
||||||
| 供应商故障恢复 | V23持久化按租户/供应商的可用状态;连续临时失败暂停新启动但停止任务继续尝试;演练确认暂停后新外呼0、停止尝试2次、历史接口200、CRITICAL告警出现,恢复后全部收敛;V23单版本回退/重升通过 |
|
| 供应商故障恢复 | V23持久化按租户/供应商的可用状态;连续临时失败暂停新启动但停止任务继续尝试;演练确认暂停后新外呼0、停止尝试2次、历史接口200、CRITICAL告警出现,恢复后全部收敛;V23单版本回退/重升通过 |
|
||||||
@@ -251,6 +251,7 @@
|
|||||||
| 托管内容安全外部预检与告警 | OpenAPI 0.64.0把`content_safety_tls`纳入外部依赖查询和候选验收快照。生产Worker仅执行DNS、TCP和受信TLS握手,不发送待分类正文;结果复用依赖可用性指标、数据库脱敏状态和运营诊断页面,连续5分钟不可用进入统一CRITICAL告警,记录缺失/失败/过期均可阻断候选技术快照但不驱逐Pod。按低强度策略执行App/探针/诊断定向测试、前端类型生成/类型检查/生产构建和Prometheus规则校验。 |
|
| 托管内容安全外部预检与告警 | OpenAPI 0.64.0把`content_safety_tls`纳入外部依赖查询和候选验收快照。生产Worker仅执行DNS、TCP和受信TLS握手,不发送待分类正文;结果复用依赖可用性指标、数据库脱敏状态和运营诊断页面,连续5分钟不可用进入统一CRITICAL告警,记录缺失/失败/过期均可阻断候选技术快照但不驱逐Pod。按低强度策略执行App/探针/诊断定向测试、前端类型生成/类型检查/生产构建和Prometheus规则校验。 |
|
||||||
| 云端主机健康状态接入 | OpenAPI 0.65.0新增独立HMAC签名的主机健康回调、30天脱敏快照表和运营只读状态页面。五分钟周期上报覆盖公网入口、容器、备份、证书、磁盘和定时器;无签名、过期、重复检查及综合状态不一致报告被拒绝,页面超过15分钟未更新明确标记过期。该能力不替代整机、Callbacks或数据库失联时必须使用的外部探针。 |
|
| 云端主机健康状态接入 | OpenAPI 0.65.0新增独立HMAC签名的主机健康回调、30天脱敏快照表和运营只读状态页面。五分钟周期上报覆盖公网入口、容器、备份、证书、磁盘和定时器;无签名、过期、重复检查及综合状态不一致报告被拒绝,页面超过15分钟未更新明确标记过期。该能力不替代整机、Callbacks或数据库失联时必须使用的外部探针。 |
|
||||||
| 云端主机健康发布门禁 | OpenAPI 0.66.0把最新主机健康快照纳入发布候选技术验收:无上报、超过15分钟、综合预警或严重异常均阻断,只有当前有效且23项检查全部正常才通过,其中包括宿主机公网监听端口策略。快照仅归档状态、时间和检查数量,不复制磁盘、证书、监听地址或备份明细值。 |
|
| 云端主机健康发布门禁 | OpenAPI 0.66.0把最新主机健康快照纳入发布候选技术验收:无上报、超过15分钟、综合预警或严重异常均阻断,只有当前有效且23项检查全部正常才通过,其中包括宿主机公网监听端口策略。快照仅归档状态、时间和检查数量,不复制磁盘、证书、监听地址或备份明细值。 |
|
||||||
|
| 生产基础设施强制门禁 | OpenAPI 0.67.0把生产基础设施状态作为第八项技术门禁。`PRODUCTION`快照必须从当前主机健康报告同时证明`production`部署模式、OSS异地备份上传回读校验不过期、外部告警接收器已配置及两个Mock服务均已退出;演示机继续明确上报`demo`,因此生产快照会被如实阻断。`LAB/STAGING`标记为不适用,不影响开发联调。前端同步展示部署模式、告警和Mock状态;该门禁只证明配置与当前进程状态,不替代真实告警送达演练和生产基础设施一致性报告。 |
|
||||||
| 阶段8外部执行轻量预检 | 新增`scripts/stage8-preflight.ps1`,以单一只读入口核对候选Git提交、远程仓库、Compose本地基线、外部配置变量名称和十类证据索引结构/状态;报告明确不读取密钥值,也不把环境变量或本地服务可用性当作验收证据。默认仅报告缺口,`-Strict`可供自动化阻断未就绪交接;当前已建立本地`main`候选提交,本地运行基线就绪且索引结构有效,仍缺远程仓库,样例构建摘要为占位值,十类外部证据均为`PENDING`。 |
|
| 阶段8外部执行轻量预检 | 新增`scripts/stage8-preflight.ps1`,以单一只读入口核对候选Git提交、远程仓库、Compose本地基线、外部配置变量名称和十类证据索引结构/状态;报告明确不读取密钥值,也不把环境变量或本地服务可用性当作验收证据。默认仅报告缺口,`-Strict`可供自动化阻断未就绪交接;当前已建立本地`main`候选提交,本地运行基线就绪且索引结构有效,仍缺远程仓库,样例构建摘要为占位值,十类外部证据均为`PENDING`。 |
|
||||||
| 候选构建身份贯通 | Compose新增统一候选构建参数,后端二进制和后端/Web镜像OCI标签共享版本、完整Git提交及UTC构建时间;远程CI从OpenAPI版本和运行编号形成候选版本,构建后重新读取两个镜像标签并与`GITHUB_SHA`和构建时间逐项比对,再把候选版本、提交、时间和双镜像摘要写入构建元数据。修复了此前Dockerfile虽支持注入但Compose/CI未传参、运行版本仍为`dev/unknown`的问题。 |
|
| 候选构建身份贯通 | Compose新增统一候选构建参数,后端二进制和后端/Web镜像OCI标签共享版本、完整Git提交及UTC构建时间;远程CI从OpenAPI版本和运行编号形成候选版本,构建后重新读取两个镜像标签并与`GITHUB_SHA`和构建时间逐项比对,再把候选版本、提交、时间和双镜像摘要写入构建元数据。修复了此前Dockerfile虽支持注入但Compose/CI未传参、运行版本仍为`dev/unknown`的问题。 |
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 设备 API 兼容性约定
|
# 设备 API 兼容性约定
|
||||||
|
|
||||||
> 当前设备接口主版本:`/device/v2`
|
> 当前设备接口主版本:`/device/v2`
|
||||||
> 当前接口契约:OpenAPI 0.66.0
|
> 当前接口契约:OpenAPI 0.67.0
|
||||||
> 适用对象:云端 API、终端固件、模拟终端和设备联调工具
|
> 适用对象:云端 API、终端固件、模拟终端和设备联调工具
|
||||||
|
|
||||||
## 1. 兼容目标
|
## 1. 兼容目标
|
||||||
@@ -42,9 +42,10 @@ CI在拉取请求中以目标分支OpenAPI为基线执行破坏性变更检查
|
|||||||
|
|
||||||
| OpenAPI版本 | 设备兼容变更 | 兼容结论 |
|
| OpenAPI版本 | 设备兼容变更 | 兼容结论 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 0.64.0 | 运营外部预检增加`content_safety_tls`枚举和候选快照第四类可选必需依赖;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 |
|
| 0.67.0 | 发布候选快照增加生产基础设施强制门禁及主机部署模式、异地备份、外部告警、模拟服务状态;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 |
|
||||||
| 0.65.0 | 新增主机健康签名回调和运营只读查询;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 |
|
|
||||||
| 0.66.0 | 发布候选技术验收快照增加主机健康当前性门禁;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 |
|
| 0.66.0 | 发布候选技术验收快照增加主机健康当前性门禁;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 |
|
||||||
|
| 0.65.0 | 新增主机健康签名回调和运营只读查询;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 |
|
||||||
|
| 0.64.0 | 运营外部预检增加`content_safety_tls`枚举和候选快照第四类可选必需依赖;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 |
|
||||||
| 0.63.0 | 增加设备固件阶段事件查询及云端证据字段;设备已有命令回执仅记录同一结果的不可变事件,未增加旧终端必填字段 | v2兼容,旧终端行为不变 |
|
| 0.63.0 | 增加设备固件阶段事件查询及云端证据字段;设备已有命令回执仅记录同一结果的不可变事件,未增加旧终端必填字段 | v2兼容,旧终端行为不变 |
|
||||||
| 0.62.0 | 命令结果增加A/B写入、待重启、启动健康和回退可选字段,省略时保持原有单分区结果语义 | v2兼容,旧终端可继续省略 |
|
| 0.62.0 | 命令结果增加A/B写入、待重启、启动健康和回退可选字段,省略时保持原有单分区结果语义 | v2兼容,旧终端可继续省略 |
|
||||||
| 0.61.0 | 新增家庭侧固件版本影响只读接口,未修改设备协议 | v2完全兼容 |
|
| 0.61.0 | 新增家庭侧固件版本影响只读接口,未修改设备协议 | v2完全兼容 |
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ powershell -ExecutionPolicy Bypass -File scripts/stage8-preflight.ps1 `
|
|||||||
- 发现的问题均进入V36联调问题台账并具备严重级别、根因、修复版本和脱敏回归证据;清单全量汇总显示未关闭HIGH/CRITICAL为0;
|
- 发现的问题均进入V36联调问题台账并具备严重级别、根因、修复版本和脱敏回归证据;清单全量汇总显示未关闭HIGH/CRITICAL为0;
|
||||||
- 真实凭据完成一次轮换,旧凭据失效且业务不中断。
|
- 真实凭据完成一次轮换,旧凭据失效且业务不中断。
|
||||||
- 使用`cmd/kms-rotation-conformance`归档同一候选构建的云提供器、工作负载身份、动态读取和滚动重启两类轮换;报告必须为`PASS/REAL_KMS_HSM`,证据包内`kms_rotation_validation.coverage_complete=true`。
|
- 使用`cmd/kms-rotation-conformance`归档同一候选构建的云提供器、工作负载身份、动态读取和滚动重启两类轮换;报告必须为`PASS/REAL_KMS_HSM`,证据包内`kms_rotation_validation.coverage_complete=true`。
|
||||||
- 以相同候选构建重新生成验收快照并归档,六项技术门禁均通过;若候选构建变化,原快照作废并重新执行受影响场景。
|
- 以相同候选构建重新生成验收快照并归档,八项技术门禁均通过;若候选构建变化,原快照作废并重新执行受影响场景。
|
||||||
|
|
||||||
## 3. 家庭试点
|
## 3. 家庭试点
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ powershell -ExecutionPolicy Bypass -File scripts/stage8-preflight.ps1 `
|
|||||||
- 最近两个Chrome/Edge和macOS最近两个Safari版本通过关键流程;
|
- 最近两个Chrome/Edge和macOS最近两个Safari版本通过关键流程;
|
||||||
- 使用`cmd/safari-conformance`归档同一候选在真实Safari `CURRENT/PREVIOUS`、两视口和六流程的24项结果;报告必须为`PASS/REAL_SAFARI`,证据包内`safari_compatibility_validation.coverage_complete=true`。
|
- 使用`cmd/safari-conformance`归档同一候选在真实Safari `CURRENT/PREVIOUS`、两视口和六流程的24项结果;报告必须为`PASS/REAL_SAFARI`,证据包内`safari_compatibility_validation.coverage_complete=true`。
|
||||||
- 核心模块覆盖率和关键分支覆盖率达到需求规定门槛,当前本地未达标项必须在发布前关闭或由风险负责人书面接受。
|
- 核心模块覆盖率和关键分支覆盖率达到需求规定门槛,当前本地未达标项必须在发布前关闭或由风险负责人书面接受。
|
||||||
- 已使用生产候选镜像摘要生成`PRODUCTION_RELEASE`验收快照;运行版本必须等于候选版本且包含提交号和构建时间,六项技术门禁全部通过。
|
- 已使用生产候选镜像摘要生成`PRODUCTION_RELEASE`验收快照;运行版本必须等于候选版本且包含提交号和构建时间,八项技术门禁全部通过,其中生产基础设施门禁必须证明生产部署模式、已验证异地备份、真实外部告警配置和Mock服务退出。
|
||||||
- 使用`cmd/release-evidence`为同一候选版本、镜像摘要和源提交生成最终证据包;十类证据索引不得存在`PENDING`。出现`WAIVED`时必须附有权责任人的书面风险接受,包状态只能进入带豁免的发布评审,不能自动视为批准;八类不可豁免门禁不得使用`WAIVED`。
|
- 使用`cmd/release-evidence`为同一候选版本、镜像摘要和源提交生成最终证据包;十类证据索引不得存在`PENDING`。出现`WAIVED`时必须附有权责任人的书面风险接受,包状态只能进入带豁免的发布评审,不能自动视为批准;八类不可豁免门禁不得使用`WAIVED`。
|
||||||
|
|
||||||
### 4.2 业务与合规门禁
|
### 4.2 业务与合规门禁
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 1. 目标与边界
|
## 1. 目标与边界
|
||||||
|
|
||||||
单机健康监测原先只把结果保存在主机`/var/lib/data-terminal-cloud-monitor/status.json`。本功能把同一份脱敏状态每五分钟签名上报到Callbacks进程,并在运营“联调诊断”工作台展示最新结果,覆盖公网HTTPS、OIDC签发地址、核心容器、本地及异地备份、证书、磁盘和systemd定时器。
|
单机健康监测原先只把结果保存在主机`/var/lib/data-terminal-cloud-monitor/status.json`。本功能把同一份脱敏状态每五分钟签名上报到Callbacks进程,并在运营“联调诊断”工作台展示最新结果,覆盖部署模式、公网HTTPS、OIDC签发地址、核心容器、模拟服务、本地及异地备份、外部告警配置、证书、磁盘和systemd定时器。
|
||||||
|
|
||||||
该链路用于当前状态查看和排障,不替代外部告警网关:Callbacks、数据库或整机同时不可用时,平台无法把自身故障写入自身数据库,仍需阿里云云监控、独立探针或真实值班网关从系统外发现。
|
该链路用于当前状态查看和排障,不替代外部告警网关:Callbacks、数据库或整机同时不可用时,平台无法把自身故障写入自身数据库,仍需阿里云云监控、独立探针或真实值班网关从系统外发现。
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
| `POST /callbacks/v1/platform-health` | 独立HMAC签名 | 新快照返回201;完全重复返回200 |
|
| `POST /callbacks/v1/platform-health` | 独立HMAC签名 | 新快照返回201;完全重复返回200 |
|
||||||
| `GET /api/v1/ops/diagnostics/platform-health` | OIDC + 运营角色 + 客户组织选择 | 返回全平台最新脱敏快照 |
|
| `GET /api/v1/ops/diagnostics/platform-health` | OIDC + 运营角色 + 客户组织选择 | 返回全平台最新脱敏快照 |
|
||||||
|
|
||||||
接口契约见`openapi/openapi.yaml` 0.66.0。最新快照同时进入发布候选技术验收门禁:缺失、超过15分钟、warning或critical都会阻断,只有当前healthy快照通过。
|
接口契约见`openapi/openapi.yaml` 0.67.0。最新快照同时进入发布候选技术验收门禁:缺失、超过15分钟、warning或critical都会阻断,只有当前healthy快照通过。`PRODUCTION`快照还执行独立的生产基础设施门禁:主机必须明确上报`production`部署模式,异地备份必须已完成远端回读校验且不过期,外部告警接收器必须配置,两个模拟服务必须停止。`LAB`和`STAGING`保留该门禁字段但标记为不适用,不影响现有开发与联调。
|
||||||
|
|
||||||
## 5. 单机配置
|
## 5. 单机配置
|
||||||
|
|
||||||
@@ -44,8 +44,11 @@
|
|||||||
```dotenv
|
```dotenv
|
||||||
PLATFORM_HEALTH_REPORT_URL=https://121.41.46.238/callbacks/v1/platform-health
|
PLATFORM_HEALTH_REPORT_URL=https://121.41.46.238/callbacks/v1/platform-health
|
||||||
PLATFORM_HEALTH_REPORT_SECRET_FILE=/etc/data-terminal-cloud/platform-health-callback-secret
|
PLATFORM_HEALTH_REPORT_SECRET_FILE=/etc/data-terminal-cloud/platform-health-callback-secret
|
||||||
|
HOST_DEPLOYMENT_PROFILE=demo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
不得仅把`HOST_DEPLOYMENT_PROFILE`改为`production`来制造放行结果。切换前应先停用模拟服务,启用并验证OSS异地备份,配置真实外部告警接收器及HMAC密钥,再执行一次主机健康检查并重新生成生产快照。
|
||||||
|
|
||||||
启用后执行:
|
启用后执行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
`cmd/release-evidence`用于阶段8联调、试点和生产发布前,把同一候选版本的技术验收快照、外部验收状态、真机固件一致性报告、接口契约、数据库迁移、部署清单、关键交付文档和可用SBOM封装为一个带SHA-256清单的ZIP。`PILOT`包还必须携带同一客户组织、同一业务时区、上一完整自然周的7份日报和1份周报。它解决证据散落、候选身份不一致、真机场景缺口被模拟结果掩盖、试点周期证据缺失以及归档后无法判断文件是否被替换的问题。
|
`cmd/release-evidence`用于阶段8联调、试点和生产发布前,把同一候选版本的技术验收快照、外部验收状态、真机固件一致性报告、接口契约、数据库迁移、部署清单、关键交付文档和可用SBOM封装为一个带SHA-256清单的ZIP。`PILOT`包还必须携带同一客户组织、同一业务时区、上一完整自然周的7份日报和1份周报。它解决证据散落、候选身份不一致、真机场景缺口被模拟结果掩盖、试点周期证据缺失以及归档后无法判断文件是否被替换的问题。
|
||||||
|
|
||||||
证据包不是发布批准。生成器只在技术验收快照六项门禁全部通过时工作,但外部证据可以保持`PENDING`,此时包内状态固定为`EXTERNAL_VALIDATION_PENDING`。真实设备、供应商、KMS、告警接收器、Safari或业务签署不能用本地模拟结果替代。
|
证据包不是发布批准。生成器只在技术验收快照八项门禁全部通过时工作;其中生产快照必须额外证明生产部署模式、已验证异地备份、外部告警配置和模拟服务退出。外部证据可以保持`PENDING`,此时包内状态固定为`EXTERNAL_VALIDATION_PENDING`。真实设备、供应商、KMS、告警接收器、Safari或业务签署不能用本地模拟结果替代。
|
||||||
|
|
||||||
## 2. 输入设计
|
## 2. 输入设计
|
||||||
|
|
||||||
@@ -143,4 +143,4 @@ go test ./internal/releaseevidence ./cmd/release-evidence
|
|||||||
.\scripts\smoke-release-evidence.ps1
|
.\scripts\smoke-release-evidence.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
烟测使用真实OIDC生成绑定同一候选版本和API镜像摘要的`PILOT`技术验收快照、上一完整自然周的7份日报和1份周报,构造十项全部`PENDING`的外部索引,生成并解包临时ZIP,核对OpenAPI 0.66.0、v3清单、候选一致性、周期证据、摘要、待验计数和真机固件四场景缺口,随后清理临时文件、审计和幂等记录。技术验收快照还要求主机健康上报在15分钟内且没有warning/critical。该结果只证明打包链路可用,不证明十项外部验收、真机固件或投诉证据已经完成。
|
烟测使用真实OIDC生成绑定同一候选版本和API镜像摘要的`PILOT`技术验收快照、上一完整自然周的7份日报和1份周报,构造十项全部`PENDING`的外部索引,生成并解包临时ZIP,核对OpenAPI 0.67.0、v3清单、候选一致性、周期证据、摘要、待验计数和真机固件四场景缺口,随后清理临时文件、审计和幂等记录。技术验收快照还要求主机健康上报在15分钟内且没有warning/critical;生产类型另要求生产基础设施门禁通过。该结果只证明打包链路可用,不证明十项外部验收、真机固件或投诉证据已经完成。
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
|
|
||||||
| 来源 | 要求 | 当前处理 |
|
| 来源 | 要求 | 当前处理 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| P0414 | 首期需求、验收用例、缺陷和风险形成对应关系 | 本文完成首期功能到代码、接口、数据和验证的基线;V30问题清单由V36补充严重级别、根因、修复版本、回归证据及未关闭HIGH/CRITICAL发布阻断汇总;OpenAPI 0.50.0—0.66.0可按候选构建导出七项技术门禁及脱敏阻断告警定位快照,绑定候选版本/构建摘要后按业务时区归档日/周运行证据。第七项门禁要求主机健康快照在15分钟内且全部检查正常。`cmd/release-evidence`现使用v3清单,把同一候选的快照、接口、迁移、部署、SBOM和十类外部状态形成逐文件摘要;远程CI供应链、真机固件、真实终端RTC、真实Agent模型、真实告警网关、云KMS/HSM轮换、生产等价恢复、生产基础设施和Safari兼容分别要求结构化报告,并把14项CI检查/镜像/SBOM、设备固件场景、语音方式与恢复场景、生成/Embedding/内容安全质量、路由演练、轮换模式、恢复类型、基础设施控制项及Safari矩阵覆盖与缺口写入清单。V38及九个离线校验器把云端状态与远程构建、外部设备、RTC/语音供应商、模型提供器、网关、密钥提供器、目标环境和浏览器证据绑定;`content_safety_tls`把策略服务网络/证书状态纳入指标、告警、运营页面和候选快照。缺检查、缺制品、缺场景、缺演练、缺质量类别、缺控制项、`PENDING`和本地模拟证据均不会被误报为外部验收完成,整体发布仍必须基于真实联调、灰度和审批记录确认。 |
|
| P0414 | 首期需求、验收用例、缺陷和风险形成对应关系 | 本文完成首期功能到代码、接口、数据和验证的基线;V30问题清单由V36补充严重级别、根因、修复版本、回归证据及未关闭HIGH/CRITICAL发布阻断汇总;OpenAPI 0.50.0—0.67.0可按候选构建导出八项技术门禁及脱敏阻断告警定位快照,绑定候选版本/构建摘要后按业务时区归档日/周运行证据。第七项门禁要求主机健康快照在15分钟内且全部检查正常;第八项在`PRODUCTION`强制要求生产部署模式、已验证异地备份、外部告警配置及Mock服务退出,在`LAB/STAGING`明确标记不适用。`cmd/release-evidence`现使用v3清单,把同一候选的快照、接口、迁移、部署、SBOM和十类外部状态形成逐文件摘要;远程CI供应链、真机固件、真实终端RTC、真实Agent模型、真实告警网关、云KMS/HSM轮换、生产等价恢复、生产基础设施和Safari兼容分别要求结构化报告,并把14项CI检查/镜像/SBOM、设备固件场景、语音方式与恢复场景、生成/Embedding/内容安全质量、路由演练、轮换模式、恢复类型、基础设施控制项及Safari矩阵覆盖与缺口写入清单。V38及九个离线校验器把云端状态与远程构建、外部设备、RTC/语音供应商、模型提供器、网关、密钥提供器、目标环境和浏览器证据绑定;`content_safety_tls`把策略服务网络/证书状态纳入指标、告警、运营页面和候选快照。缺检查、缺制品、缺场景、缺演练、缺质量类别、缺控制项、`PENDING`和本地模拟证据均不会被误报为外部验收完成,整体发布仍必须基于真实联调、灰度和审批记录确认。 |
|
||||||
| P0445 | 需求编号关联代码、测试、接口或配置变更 | 本文逐项建立关联;后续功能变更必须同步更新本矩阵和`development-status.md`。 |
|
| P0445 | 需求编号关联代码、测试、接口或配置变更 | 本文逐项建立关联;后续功能变更必须同步更新本矩阵和`development-status.md`。 |
|
||||||
|
|
||||||
当前仍不能宣告完成的部分集中为:真实火山RTC与豆包三模式联调、真实方舟内容与Embedding质量、真实听筒的长者交流体验、量产设备/KMS注入、跨故障域生产对象存储与备份、真机异常恢复、真实账单和生产级内容安全。阿里云单机环境已经形成加密全组件归档、数据库隔离恢复和Redis RDB校验,但同机归档不能替代托管PITR、异地副本和生产恢复演练。这些剩余项属于阶段8外部验收,不应通过增加本地低价值测试伪造完成证据。
|
当前仍不能宣告完成的部分集中为:真实火山RTC与豆包三模式联调、真实方舟内容与Embedding质量、真实听筒的长者交流体验、量产设备/KMS注入、跨故障域生产对象存储与备份、真机异常恢复、真实账单和生产级内容安全。阿里云单机环境已经形成加密全组件归档、数据库隔离恢复和Redis RDB校验,但同机归档不能替代托管PITR、异地副本和生产恢复演练。这些剩余项属于阶段8外部验收,不应通过增加本地低价值测试伪造完成证据。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 1. 目标与边界
|
## 1. 目标与边界
|
||||||
|
|
||||||
`deploy/server/health-monitor.sh`为单机阿里云演示环境提供五分钟一次的轻量健康检查。它用于发现“公网入口不可用、宿主机出现未授权公网监听端口、身份签发地址漂移、容器退出、备份过期或损坏、证书接近到期、定时任务停止、磁盘空间不足”等直接影响可用性或暴露面的事件。
|
`deploy/server/health-monitor.sh`为单机阿里云环境提供五分钟一次的轻量健康检查。它用于发现“部署模式与运行服务不一致、公网入口不可用、宿主机出现未授权公网监听端口、身份签发地址漂移、容器退出、备份过期或损坏、外部告警未配置、证书接近到期、定时任务停止、磁盘空间不足”等直接影响可用性或暴露面的事件。
|
||||||
|
|
||||||
该脚本不替代Prometheus、Grafana、云监控或真实值班系统。默认只写结构化状态文件和systemd日志;配置外部接收器后,状态发生变化时才发送一次HMAC签名Webhook,避免每五分钟重复通知。
|
该脚本不替代Prometheus、Grafana、云监控或真实值班系统。默认只写结构化状态文件和systemd日志;配置外部接收器后,状态发生变化时才发送一次HMAC签名Webhook,避免每五分钟重复通知。
|
||||||
|
|
||||||
@@ -10,12 +10,15 @@
|
|||||||
|
|
||||||
| 检查 | 正常条件 | 失败状态 |
|
| 检查 | 正常条件 | 失败状态 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 部署模式 | `HOST_DEPLOYMENT_PROFILE`只能为`demo`或`production` | 非法值直接拒绝执行 |
|
||||||
|
| 外部告警接收器 | `demo`未配置时明确记录`not_configured`;`production`必须配置且HMAC密钥不少于32字节 | `not_configured`、`secret_missing`或`secret_invalid` |
|
||||||
| 公网地址 | `PUBLIC_ORIGIN`为HTTPS | `invalid_https_origin` |
|
| 公网地址 | `PUBLIC_ORIGIN`为HTTPS | `invalid_https_origin` |
|
||||||
| HTTPS入口 | `/api/v1/version`在10秒内返回成功 | `request_failed` |
|
| HTTPS入口 | `/api/v1/version`在10秒内返回成功 | `request_failed` |
|
||||||
| 公网监听端口 | 宿主机非回环TCP监听端口只能属于`22,80,443`;回环地址上的数据库或监控端口不计入公网暴露 | `unexpected_ports=...`、`probe_unavailable`或`invalid_probe_result` |
|
| 公网监听端口 | 宿主机非回环TCP监听端口只能属于`22,80,443`;回环地址上的数据库或监控端口不计入公网暴露 | `unexpected_ports=...`、`probe_unavailable`或`invalid_probe_result` |
|
||||||
| OIDC | Discovery中的`issuer`与公网地址完全一致 | `issuer_mismatch`或`request_failed` |
|
| OIDC | Discovery中的`issuer`与公网地址完全一致 | `issuer_mismatch`或`request_failed` |
|
||||||
| 容器 | PostgreSQL、Redis、MinIO、Keycloak、API、Callbacks、Worker、Web、Gateway和两个模拟服务均为运行态;有健康检查的容器必须为healthy | `missing`、容器状态或健康状态 |
|
| 容器 | PostgreSQL、Redis、MinIO、Keycloak、API、Callbacks、Worker、Web和Gateway均为运行态;有健康检查的容器必须为healthy | `missing`、容器状态或健康状态 |
|
||||||
| 备份 | 最新加密归档不超过36小时且外层SHA-256有效 | `missing`、`age_seconds=...`或`checksum_failed` |
|
| 模拟服务 | `demo`要求模拟语音和模拟告警服务都运行;`production`要求两者都不运行 | `incomplete_demo_stack`或`unexpected_running` |
|
||||||
|
| 备份 | 最新加密归档不超过36小时且外层SHA-256有效;`production`还要求异地备份启用并完成最新归档的回读验证 | `missing`、`age_seconds=...`、`checksum_failed`或异地备份状态错误 |
|
||||||
| 备份任务 | 最近一次`platform-backup.service`结果成功且退出码为0 | `result=.../exit=...` |
|
| 备份任务 | 最近一次`platform-backup.service`结果成功且退出码为0 | `result=.../exit=...` |
|
||||||
| 定时任务 | 备份和证书续期timer均为enabled/active | 当前启用/活动状态 |
|
| 定时任务 | 备份和证书续期timer均为enabled/active | 当前启用/活动状态 |
|
||||||
| 证书 | 本地活动证书剩余时间不少于36小时 | `missing`或`remaining_seconds=...` |
|
| 证书 | 本地活动证书剩余时间不少于36小时 | `missing`或`remaining_seconds=...` |
|
||||||
@@ -86,6 +89,7 @@ PLATFORM_HEALTH_REPORT_SECRET_FILE=/etc/data-terminal-cloud/platform-health-call
|
|||||||
|
|
||||||
| 环境变量 | 默认值 | 说明 |
|
| 环境变量 | 默认值 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| `HOST_DEPLOYMENT_PROFILE` | `demo` | 主机部署模式;生产切换必须与异地备份、告警接收器和模拟服务退出同时完成 |
|
||||||
| `BACKUP_MAX_AGE_SECONDS` | `129600` | 最新备份最大年龄,36小时 |
|
| `BACKUP_MAX_AGE_SECONDS` | `129600` | 最新备份最大年龄,36小时 |
|
||||||
| `CERTIFICATE_MIN_SECONDS` | `129600` | 证书最小剩余时间,36小时 |
|
| `CERTIFICATE_MIN_SECONDS` | `129600` | 证书最小剩余时间,36小时 |
|
||||||
| `DISK_WARNING_PERCENT` | `80` | 磁盘警告阈值 |
|
| `DISK_WARNING_PERCENT` | `80` | 磁盘警告阈值 |
|
||||||
@@ -130,9 +134,11 @@ systemctl list-timers platform-health-monitor.timer platform-backup.timer certbo
|
|||||||
docker compose -f compose.yaml -f deploy/server/compose.override.yaml --env-file deploy/server/.env ps -a
|
docker compose -f compose.yaml -f deploy/server/compose.override.yaml --env-file deploy/server/.env ps -a
|
||||||
```
|
```
|
||||||
|
|
||||||
|
生产分支可在不改变线上容器的情况下验证。临时配置`HOST_DEPLOYMENT_PROFILE=production`运行检查时,只要模拟服务仍在运行,结果必须为critical且`mock_services=unexpected_running`;测试状态目录应与正式目录隔离。
|
||||||
|
|
||||||
## 8. 下一步生产化
|
## 8. 下一步生产化
|
||||||
|
|
||||||
1. 配置真实告警网关、主值班组和升级组,并完成HMAC验签联通测试。
|
1. 配置真实告警网关、主值班组和升级组,并完成HMAC验签联通测试。
|
||||||
2. 将状态接入云监控或Prometheus长期存储,建立可用率和告警送达趋势。
|
2. 将状态接入云监控或Prometheus长期存储,建立可用率和告警送达趋势。
|
||||||
3. 将加密备份复制到不同故障域,并对“异地复制最后成功时间”增加检查。
|
3. 启用现有OSS异地备份任务,确认上传、回读和SHA-256复算成功,并把最近验证时间纳入健康状态。
|
||||||
4. 使用外部OIDC、托管数据库/Redis/对象存储后,把资源级高可用、PITR和容量指标纳入同一状态模型。
|
4. 使用外部OIDC、托管数据库/Redis/对象存储后,把资源级高可用、PITR和容量指标纳入同一状态模型。
|
||||||
|
|||||||
@@ -82,6 +82,15 @@ type AcceptancePlatformHealthState struct {
|
|||||||
GateSatisfied bool `json:"gate_satisfied"`
|
GateSatisfied bool `json:"gate_satisfied"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AcceptanceProductionInfrastructureState struct {
|
||||||
|
Applicable bool `json:"applicable"`
|
||||||
|
DeploymentProfile string `json:"deployment_profile"`
|
||||||
|
OffsiteBackupReady bool `json:"offsite_backup_ready"`
|
||||||
|
ExternalAlertReceiverReady bool `json:"external_alert_receiver_ready"`
|
||||||
|
MockServicesDisabled bool `json:"mock_services_disabled"`
|
||||||
|
GateSatisfied bool `json:"gate_satisfied"`
|
||||||
|
}
|
||||||
|
|
||||||
type AcceptanceBlockingAlert struct {
|
type AcceptanceBlockingAlert struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
RuleCode string `json:"rule_code"`
|
RuleCode string `json:"rule_code"`
|
||||||
@@ -117,6 +126,7 @@ type AcceptanceSnapshot struct {
|
|||||||
Outbox AcceptanceOutboxState `json:"outbox"`
|
Outbox AcceptanceOutboxState `json:"outbox"`
|
||||||
Alerts AcceptanceAlertState `json:"alerts"`
|
Alerts AcceptanceAlertState `json:"alerts"`
|
||||||
PlatformHealth AcceptancePlatformHealthState `json:"platform_health"`
|
PlatformHealth AcceptancePlatformHealthState `json:"platform_health"`
|
||||||
|
ProductionInfrastructure AcceptanceProductionInfrastructureState `json:"production_infrastructure"`
|
||||||
Gates []AcceptanceGate `json:"gates"`
|
Gates []AcceptanceGate `json:"gates"`
|
||||||
OverallGateSatisfied bool `json:"overall_gate_satisfied"`
|
OverallGateSatisfied bool `json:"overall_gate_satisfied"`
|
||||||
Disclaimer string `json:"disclaimer"`
|
Disclaimer string `json:"disclaimer"`
|
||||||
@@ -289,11 +299,12 @@ func buildAcceptanceSnapshot(ctx context.Context, tx pgx.Tx, tenantPublicID, ten
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return AcceptanceSnapshot{}, err
|
return AcceptanceSnapshot{}, err
|
||||||
}
|
}
|
||||||
platformHealth, err := loadAcceptancePlatformHealth(ctx, tx, now)
|
platformHealth, productionInfrastructure, err := loadAcceptancePlatformHealth(ctx, tx, now, input.Environment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AcceptanceSnapshot{}, err
|
return AcceptanceSnapshot{}, err
|
||||||
}
|
}
|
||||||
snapshot.PlatformHealth = platformHealth
|
snapshot.PlatformHealth = platformHealth
|
||||||
|
snapshot.ProductionInfrastructure = productionInfrastructure
|
||||||
|
|
||||||
runtimeSatisfied := input.Environment == "LAB" || (snapshot.RuntimeBuild.Version == input.CandidateVersion && snapshot.RuntimeBuild.Commit != "unknown" && snapshot.RuntimeBuild.BuildTime != "unknown")
|
runtimeSatisfied := input.Environment == "LAB" || (snapshot.RuntimeBuild.Version == input.CandidateVersion && snapshot.RuntimeBuild.Commit != "unknown" && snapshot.RuntimeBuild.BuildTime != "unknown")
|
||||||
dependenciesSatisfied := true
|
dependenciesSatisfied := true
|
||||||
@@ -310,6 +321,7 @@ func buildAcceptanceSnapshot(ctx context.Context, tx pgx.Tx, tenantPublicID, ten
|
|||||||
{Code: "outbox_clear", Satisfied: outboxSatisfied, Summary: "租户及平台级待处理、处理中和死信任务必须清零"},
|
{Code: "outbox_clear", Satisfied: outboxSatisfied, Summary: "租户及平台级待处理、处理中和死信任务必须清零"},
|
||||||
{Code: "critical_alerts_clear", Satisfied: alertsSatisfied, Summary: "不得存在未解决的高或严重级告警"},
|
{Code: "critical_alerts_clear", Satisfied: alertsSatisfied, Summary: "不得存在未解决的高或严重级告警"},
|
||||||
{Code: "platform_health_current", Satisfied: snapshot.PlatformHealth.GateSatisfied, Summary: acceptancePlatformHealthSummary(snapshot.PlatformHealth)},
|
{Code: "platform_health_current", Satisfied: snapshot.PlatformHealth.GateSatisfied, Summary: acceptancePlatformHealthSummary(snapshot.PlatformHealth)},
|
||||||
|
{Code: "production_infrastructure_ready", Satisfied: snapshot.ProductionInfrastructure.GateSatisfied, Summary: acceptanceProductionInfrastructureSummary(snapshot.ProductionInfrastructure)},
|
||||||
}
|
}
|
||||||
snapshot.OverallGateSatisfied = true
|
snapshot.OverallGateSatisfied = true
|
||||||
for _, gate := range snapshot.Gates {
|
for _, gate := range snapshot.Gates {
|
||||||
@@ -318,7 +330,7 @@ func buildAcceptanceSnapshot(ctx context.Context, tx pgx.Tx, tenantPublicID, ten
|
|||||||
return snapshot, nil
|
return snapshot, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAcceptancePlatformHealth(ctx context.Context, tx pgx.Tx, now time.Time) (AcceptancePlatformHealthState, error) {
|
func loadAcceptancePlatformHealth(ctx context.Context, tx pgx.Tx, now time.Time, environment string) (AcceptancePlatformHealthState, AcceptanceProductionInfrastructureState, error) {
|
||||||
state := AcceptancePlatformHealthState{State: "MISSING"}
|
state := AcceptancePlatformHealthState{State: "MISSING"}
|
||||||
var observedAt, receivedAt time.Time
|
var observedAt, receivedAt time.Time
|
||||||
var checksJSON []byte
|
var checksJSON []byte
|
||||||
@@ -328,16 +340,16 @@ func loadAcceptancePlatformHealth(ctx context.Context, tx pgx.Tx, now time.Time)
|
|||||||
&state.Status, &observedAt, &receivedAt, &checksJSON,
|
&state.Status, &observedAt, &receivedAt, &checksJSON,
|
||||||
)
|
)
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return state, nil
|
return state, evaluateAcceptanceProductionInfrastructure(environment, nil), nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AcceptancePlatformHealthState{}, err
|
return AcceptancePlatformHealthState{}, AcceptanceProductionInfrastructureState{}, err
|
||||||
}
|
}
|
||||||
var checks []PlatformHealthCheck
|
var checks []PlatformHealthCheck
|
||||||
if err = json.Unmarshal(checksJSON, &checks); err != nil {
|
if err = json.Unmarshal(checksJSON, &checks); err != nil {
|
||||||
return AcceptancePlatformHealthState{}, err
|
return AcceptancePlatformHealthState{}, AcceptanceProductionInfrastructureState{}, err
|
||||||
}
|
}
|
||||||
return evaluateAcceptancePlatformHealth(state.Status, observedAt, receivedAt, checks, now), nil
|
return evaluateAcceptancePlatformHealth(state.Status, observedAt, receivedAt, checks, now), evaluateAcceptanceProductionInfrastructure(environment, checks), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func evaluateAcceptancePlatformHealth(status string, observedAt, receivedAt time.Time, checks []PlatformHealthCheck, now time.Time) AcceptancePlatformHealthState {
|
func evaluateAcceptancePlatformHealth(status string, observedAt, receivedAt time.Time, checks []PlatformHealthCheck, now time.Time) AcceptancePlatformHealthState {
|
||||||
@@ -372,6 +384,53 @@ func acceptancePlatformHealthSummary(state AcceptancePlatformHealthState) string
|
|||||||
return fmt.Sprintf("云端主机健康快照当前有效,共%d项检查", state.CheckCount)
|
return fmt.Sprintf("云端主机健康快照当前有效,共%d项检查", state.CheckCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func evaluateAcceptanceProductionInfrastructure(environment string, checks []PlatformHealthCheck) AcceptanceProductionInfrastructureState {
|
||||||
|
state := AcceptanceProductionInfrastructureState{Applicable: environment == "PRODUCTION", DeploymentProfile: "MISSING"}
|
||||||
|
if !state.Applicable {
|
||||||
|
state.GateSatisfied = true
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
profileReady := false
|
||||||
|
for _, check := range checks {
|
||||||
|
switch check.Name {
|
||||||
|
case "deployment_profile":
|
||||||
|
state.DeploymentProfile = check.Value
|
||||||
|
profileReady = check.Status == "healthy" && check.Value == "production"
|
||||||
|
case "offsite_backup":
|
||||||
|
state.OffsiteBackupReady = check.Status == "healthy" && strings.HasPrefix(check.Value, "age_seconds=")
|
||||||
|
case "external_alert_receiver":
|
||||||
|
state.ExternalAlertReceiverReady = check.Status == "healthy" && check.Value == "configured"
|
||||||
|
case "mock_services":
|
||||||
|
state.MockServicesDisabled = check.Status == "healthy" && check.Value == "disabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.GateSatisfied = profileReady && state.OffsiteBackupReady && state.ExternalAlertReceiverReady && state.MockServicesDisabled
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptanceProductionInfrastructureSummary(state AcceptanceProductionInfrastructureState) string {
|
||||||
|
if !state.Applicable {
|
||||||
|
return "非生产候选不适用生产基础设施强制门禁"
|
||||||
|
}
|
||||||
|
missing := make([]string, 0, 4)
|
||||||
|
if state.DeploymentProfile != "production" {
|
||||||
|
missing = append(missing, "生产部署模式")
|
||||||
|
}
|
||||||
|
if !state.OffsiteBackupReady {
|
||||||
|
missing = append(missing, "已验证异地备份")
|
||||||
|
}
|
||||||
|
if !state.ExternalAlertReceiverReady {
|
||||||
|
missing = append(missing, "真实外部告警接收器")
|
||||||
|
}
|
||||||
|
if !state.MockServicesDisabled {
|
||||||
|
missing = append(missing, "关闭模拟服务")
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
return "生产基础设施尚缺:" + strings.Join(missing, "、")
|
||||||
|
}
|
||||||
|
return "生产部署模式、异地备份、外部告警和模拟服务退出均已确认"
|
||||||
|
}
|
||||||
|
|
||||||
func acceptanceRuntimeSummary(environment string) string {
|
func acceptanceRuntimeSummary(environment string) string {
|
||||||
if environment == "LAB" {
|
if environment == "LAB" {
|
||||||
return "实验室环境允许开发构建,但候选版本与镜像摘要必须明确"
|
return "实验室环境允许开发构建,但候选版本与镜像摘要必须明确"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func TestAcceptanceSnapshotIsAuditedIdempotentAndTenantScoped(t *testing.T) {
|
|||||||
if err != nil || !created {
|
if err != nil || !created {
|
||||||
t.Fatalf("first=%+v created=%t err=%v", first, created, err)
|
t.Fatalf("first=%+v created=%t err=%v", first, created, err)
|
||||||
}
|
}
|
||||||
if first.ID == "" || first.Database.MigrationVersion < 39 || first.Database.Dirty || first.RedactionPolicy != "ops-acceptance-minimal-v1" || len(first.ExternalDependencies) != 1 || len(first.Gates) != 7 {
|
if first.ID == "" || first.Database.MigrationVersion < 39 || first.Database.Dirty || first.RedactionPolicy != "ops-acceptance-minimal-v1" || len(first.ExternalDependencies) != 1 || len(first.Gates) != 8 {
|
||||||
t.Fatalf("unexpected snapshot=%+v", first)
|
t.Fatalf("unexpected snapshot=%+v", first)
|
||||||
}
|
}
|
||||||
if first.ExternalDependencies[0].Dependency != "oidc_jwks" || first.ExternalDependencies[0].ServiceName != "api" {
|
if first.ExternalDependencies[0].Dependency != "oidc_jwks" || first.ExternalDependencies[0].ServiceName != "api" {
|
||||||
@@ -77,6 +77,9 @@ func TestAcceptanceSnapshotIsAuditedIdempotentAndTenantScoped(t *testing.T) {
|
|||||||
if first.Gates[6].Code != "platform_health_current" || first.PlatformHealth.State == "" {
|
if first.Gates[6].Code != "platform_health_current" || first.PlatformHealth.State == "" {
|
||||||
t.Fatalf("platform health=%+v gates=%+v", first.PlatformHealth, first.Gates)
|
t.Fatalf("platform health=%+v gates=%+v", first.PlatformHealth, first.Gates)
|
||||||
}
|
}
|
||||||
|
if first.Gates[7].Code != "production_infrastructure_ready" || !first.Gates[7].Satisfied || first.ProductionInfrastructure.Applicable {
|
||||||
|
t.Fatalf("production infrastructure=%+v gates=%+v", first.ProductionInfrastructure, first.Gates)
|
||||||
|
}
|
||||||
replayed, createdAgain, err := service.ExportAcceptanceSnapshot(ctx, admin, tenantPublicID, key, requestID, input)
|
replayed, createdAgain, err := service.ExportAcceptanceSnapshot(ctx, admin, tenantPublicID, key, requestID, input)
|
||||||
if err != nil || createdAgain || replayed.ID != first.ID || !replayed.GeneratedAt.Equal(first.GeneratedAt) {
|
if err != nil || createdAgain || replayed.ID != first.ID || !replayed.GeneratedAt.Equal(first.GeneratedAt) {
|
||||||
t.Fatalf("replayed=%+v created=%t err=%v", replayed, createdAgain, err)
|
t.Fatalf("replayed=%+v created=%t err=%v", replayed, createdAgain, err)
|
||||||
|
|||||||
@@ -71,6 +71,32 @@ func TestAcceptancePlatformHealthSummaryCoversMissingAndCounts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEvaluateAcceptanceProductionInfrastructure(t *testing.T) {
|
||||||
|
readyChecks := []PlatformHealthCheck{
|
||||||
|
{Name: "deployment_profile", Status: "healthy", Value: "production"},
|
||||||
|
{Name: "offsite_backup", Status: "healthy", Value: "age_seconds=120"},
|
||||||
|
{Name: "external_alert_receiver", Status: "healthy", Value: "configured"},
|
||||||
|
{Name: "mock_services", Status: "healthy", Value: "disabled"},
|
||||||
|
}
|
||||||
|
ready := evaluateAcceptanceProductionInfrastructure("PRODUCTION", readyChecks)
|
||||||
|
if !ready.Applicable || !ready.GateSatisfied || !ready.OffsiteBackupReady || !ready.ExternalAlertReceiverReady || !ready.MockServicesDisabled {
|
||||||
|
t.Fatalf("ready=%+v", ready)
|
||||||
|
}
|
||||||
|
demo := evaluateAcceptanceProductionInfrastructure("PRODUCTION", []PlatformHealthCheck{
|
||||||
|
{Name: "deployment_profile", Status: "healthy", Value: "demo"},
|
||||||
|
{Name: "offsite_backup", Status: "healthy", Value: "not_required"},
|
||||||
|
{Name: "external_alert_receiver", Status: "healthy", Value: "not_configured"},
|
||||||
|
{Name: "mock_services", Status: "healthy", Value: "enabled_for_demo"},
|
||||||
|
})
|
||||||
|
if demo.GateSatisfied || demo.DeploymentProfile != "demo" || !strings.Contains(acceptanceProductionInfrastructureSummary(demo), "异地备份") {
|
||||||
|
t.Fatalf("demo=%+v summary=%q", demo, acceptanceProductionInfrastructureSummary(demo))
|
||||||
|
}
|
||||||
|
lab := evaluateAcceptanceProductionInfrastructure("LAB", nil)
|
||||||
|
if lab.Applicable || !lab.GateSatisfied {
|
||||||
|
t.Fatalf("lab=%+v", lab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAcceptanceSnapshotAllowsAllProductionExternalDependencies(t *testing.T) {
|
func TestAcceptanceSnapshotAllowsAllProductionExternalDependencies(t *testing.T) {
|
||||||
input := AcceptanceSnapshotInput{
|
input := AcceptanceSnapshotInput{
|
||||||
CandidateVersion: "0.64.0-rc.1", BuildDigest: "sha256:" + strings.Repeat("b", 64),
|
CandidateVersion: "0.64.0-rc.1", BuildDigest: "sha256:" + strings.Repeat("b", 64),
|
||||||
|
|||||||
+17
-5
@@ -1,8 +1,8 @@
|
|||||||
openapi: 3.1.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
title: 数据采集终端—云端系统 API
|
title: 数据采集终端—云端系统 API
|
||||||
version: 0.66.0
|
version: 0.67.0
|
||||||
description: 工程框架、SaaS核心业务、设备v2能力协商与智能体启动状态查询、首次通话就绪与最低固件门禁、背景资料准备失败闭环、活动通话授权撤回安全停止、语音内容闭环、终端终稿补传、批量设备遥测、录音上传队列、隐私对账、固件、双轨录音、状态事件、具体资源分享、受控运营配置、Agent组合组件引用、质量证据门禁、运营自定义版本族投放、新通话运行时冻结与供应商启动传递、带并发版本校验的配置投放暂停恢复、带严重度和验收证据门禁的联调问题闭环、含阻断告警定位及当前主机健康证据的发布候选技术验收快照、绑定候选版本与构建摘要并按业务时区生成的可审计试点日/周运行证据、可审计且不生成正式告警的接收器联通测试、外部依赖预检台账、证书到期清单,以及带价格版本冻结、供应商用量去重和硬额度控制的费用治理接口。
|
description: 工程框架、SaaS核心业务、设备v2能力协商与智能体启动状态查询、首次通话就绪与最低固件门禁、背景资料准备失败闭环、活动通话授权撤回安全停止、语音内容闭环、终端终稿补传、批量设备遥测、录音上传队列、隐私对账、固件、双轨录音、状态事件、具体资源分享、受控运营配置、Agent组合组件引用、质量证据门禁、运营自定义版本族投放、新通话运行时冻结与供应商启动传递、带并发版本校验的配置投放暂停恢复、带严重度和验收证据门禁的联调问题闭环、含阻断告警定位、当前主机健康及生产基础设施强制条件的发布候选技术验收快照、绑定候选版本与构建摘要并按业务时区生成的可审计试点日/周运行证据、可审计且不生成正式告警的接收器联通测试、外部依赖预检台账、证书到期清单,以及带价格版本冻结、供应商用量去重和硬额度控制的费用治理接口。
|
||||||
servers:
|
servers:
|
||||||
- url: http://localhost:58080
|
- url: http://localhost:58080
|
||||||
description: 本地开发
|
description: 本地开发
|
||||||
@@ -3284,6 +3284,17 @@ components:
|
|||||||
warning_count: { type: integer, minimum: 0, maximum: 64 }
|
warning_count: { type: integer, minimum: 0, maximum: 64 }
|
||||||
critical_count: { type: integer, minimum: 0, maximum: 64 }
|
critical_count: { type: integer, minimum: 0, maximum: 64 }
|
||||||
gate_satisfied: { type: boolean, description: 仅当快照未超过15分钟且综合状态及全部检查均为healthy时为true。 }
|
gate_satisfied: { type: boolean, description: 仅当快照未超过15分钟且综合状态及全部检查均为healthy时为true。 }
|
||||||
|
OpsAcceptanceProductionInfrastructureState:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required: [applicable, deployment_profile, offsite_backup_ready, external_alert_receiver_ready, mock_services_disabled, gate_satisfied]
|
||||||
|
properties:
|
||||||
|
applicable: { type: boolean, description: 目标环境为PRODUCTION时为true。 }
|
||||||
|
deployment_profile: { type: string, enum: [MISSING, demo, production] }
|
||||||
|
offsite_backup_ready: { type: boolean, description: 最近主机报告证明异地备份已启用、已回读验证且未过期。 }
|
||||||
|
external_alert_receiver_ready: { type: boolean, description: 最近主机报告证明真实外部告警接收器已配置且密钥可读。 }
|
||||||
|
mock_services_disabled: { type: boolean, description: 最近主机报告证明模拟语音与模拟告警服务均未运行。 }
|
||||||
|
gate_satisfied: { type: boolean }
|
||||||
OpsAcceptanceDependencyState:
|
OpsAcceptanceDependencyState:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
@@ -3338,13 +3349,13 @@ components:
|
|||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
required: [code, satisfied, summary]
|
required: [code, satisfied, summary]
|
||||||
properties:
|
properties:
|
||||||
code: { type: string, enum: [candidate_build_identified, database_migration_clean, required_dependencies_available, integration_issues_clear, outbox_clear, critical_alerts_clear, platform_health_current] }
|
code: { type: string, enum: [candidate_build_identified, database_migration_clean, required_dependencies_available, integration_issues_clear, outbox_clear, critical_alerts_clear, platform_health_current, production_infrastructure_ready] }
|
||||||
satisfied: { type: boolean }
|
satisfied: { type: boolean }
|
||||||
summary: { type: string }
|
summary: { type: string }
|
||||||
OpsAcceptanceSnapshot:
|
OpsAcceptanceSnapshot:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
required: [id, generated_at, tenant_id, candidate_version, build_digest, environment, run_type, scope, redaction_policy, runtime_build, database, integration_issues, external_dependencies, outbox, alerts, platform_health, gates, overall_gate_satisfied, disclaimer]
|
required: [id, generated_at, tenant_id, candidate_version, build_digest, environment, run_type, scope, redaction_policy, runtime_build, database, integration_issues, external_dependencies, outbox, alerts, platform_health, production_infrastructure, gates, overall_gate_satisfied, disclaimer]
|
||||||
properties:
|
properties:
|
||||||
id: { type: string, format: uuid }
|
id: { type: string, format: uuid }
|
||||||
generated_at: { type: string, format: date-time }
|
generated_at: { type: string, format: date-time }
|
||||||
@@ -3362,7 +3373,8 @@ components:
|
|||||||
outbox: { $ref: '#/components/schemas/OpsAcceptanceOutboxState' }
|
outbox: { $ref: '#/components/schemas/OpsAcceptanceOutboxState' }
|
||||||
alerts: { $ref: '#/components/schemas/OpsAcceptanceAlertState' }
|
alerts: { $ref: '#/components/schemas/OpsAcceptanceAlertState' }
|
||||||
platform_health: { $ref: '#/components/schemas/OpsAcceptancePlatformHealthState' }
|
platform_health: { $ref: '#/components/schemas/OpsAcceptancePlatformHealthState' }
|
||||||
gates: { type: array, minItems: 7, maxItems: 7, items: { $ref: '#/components/schemas/OpsAcceptanceGate' } }
|
production_infrastructure: { $ref: '#/components/schemas/OpsAcceptanceProductionInfrastructureState' }
|
||||||
|
gates: { type: array, minItems: 8, maxItems: 8, items: { $ref: '#/components/schemas/OpsAcceptanceGate' } }
|
||||||
overall_gate_satisfied: { type: boolean }
|
overall_gate_satisfied: { type: boolean }
|
||||||
disclaimer: { type: string }
|
disclaimer: { type: string }
|
||||||
OpsAcceptanceSnapshotResponse:
|
OpsAcceptanceSnapshotResponse:
|
||||||
|
|||||||
@@ -37,9 +37,11 @@ try {
|
|||||||
if ($createdResponse.StatusCode -ne 201 -or $snapshot.redaction_policy -ne 'ops-acceptance-minimal-v1') { throw '验收快照首次生成结果不正确' }
|
if ($createdResponse.StatusCode -ne 201 -or $snapshot.redaction_policy -ne 'ops-acceptance-minimal-v1') { throw '验收快照首次生成结果不正确' }
|
||||||
if ($snapshot.database.migration_version -lt 36 -or $snapshot.database.dirty) { throw '验收快照未正确冻结数据库迁移状态' }
|
if ($snapshot.database.migration_version -lt 36 -or $snapshot.database.dirty) { throw '验收快照未正确冻结数据库迁移状态' }
|
||||||
if (@($snapshot.external_dependencies).Count -ne 1 -or $snapshot.external_dependencies[0].dependency -ne 'oidc_jwks' -or -not $snapshot.external_dependencies[0].gate_satisfied) { throw '验收快照未正确冻结OIDC外部依赖状态' }
|
if (@($snapshot.external_dependencies).Count -ne 1 -or $snapshot.external_dependencies[0].dependency -ne 'oidc_jwks' -or -not $snapshot.external_dependencies[0].gate_satisfied) { throw '验收快照未正确冻结OIDC外部依赖状态' }
|
||||||
if (@($snapshot.gates).Count -ne 6) { throw '验收快照门禁数量不正确' }
|
if (@($snapshot.gates).Count -ne 8) { throw '验收快照门禁数量不正确' }
|
||||||
|
$productionGate = @($snapshot.gates | Where-Object { $_.code -eq 'production_infrastructure_ready' })
|
||||||
|
if ($productionGate.Count -ne 1 -or -not $productionGate[0].satisfied -or $snapshot.production_infrastructure.applicable -or -not $snapshot.production_infrastructure.gate_satisfied) { throw 'LAB快照不应被生产基础设施门禁阻断' }
|
||||||
$failedGates = @($snapshot.gates | Where-Object { -not $_.satisfied })
|
$failedGates = @($snapshot.gates | Where-Object { -not $_.satisfied })
|
||||||
if ($snapshot.overall_gate_satisfied -ne ($failedGates.Count -eq 0)) { throw '验收快照总结果与六项门禁不一致' }
|
if ($snapshot.overall_gate_satisfied -ne ($failedGates.Count -eq 0)) { throw '验收快照总结果与八项门禁不一致' }
|
||||||
$blockingItems = @($snapshot.alerts.blocking_items)
|
$blockingItems = @($snapshot.alerts.blocking_items)
|
||||||
$expectedItems = [Math]::Min([int]$snapshot.alerts.blocking, 100)
|
$expectedItems = [Math]::Min([int]$snapshot.alerts.blocking, 100)
|
||||||
if ($blockingItems.Count -ne $expectedItems -or $snapshot.alerts.blocking_items_truncated -ne ($snapshot.alerts.blocking -gt 100)) { throw '阻断告警定位数量或截断标识不正确' }
|
if ($blockingItems.Count -ne $expectedItems -or $snapshot.alerts.blocking_items_truncated -ne ($snapshot.alerts.blocking -gt 100)) { throw '阻断告警定位数量或截断标识不正确' }
|
||||||
@@ -60,7 +62,7 @@ try {
|
|||||||
if ($null -eq $_.Exception.Response -or [int]$_.Exception.Response.StatusCode -ne 403) { throw }
|
if ($null -eq $_.Exception.Response -or [int]$_.Exception.Response.StatusCode -ne 403) { throw }
|
||||||
}
|
}
|
||||||
$failedCodes = if ($failedGates.Count) { ($failedGates.code -join ',') } else { 'none' }
|
$failedCodes = if ($failedGates.Count) { ($failedGates.code -join ',') } else { 'none' }
|
||||||
Write-Host "发布候选验收快照烟测通过:真实OIDC、迁移冻结、外部依赖、六项门禁、阻断告警定位、幂等重放、租户隔离和端点脱敏均正确;当前阻断项=$failedCodes。"
|
Write-Host "发布候选验收快照烟测通过:真实OIDC、迁移冻结、外部依赖、八项门禁、生产条件按环境适用、阻断告警定位、幂等重放、租户隔离和端点脱敏均正确;当前阻断项=$failedCodes。"
|
||||||
} finally {
|
} finally {
|
||||||
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c "BEGIN; DELETE FROM audit_event WHERE request_id='$requestID'; DELETE FROM idempotency_record WHERE idempotency_key='$key'; COMMIT;" | Out-Null
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c "BEGIN; DELETE FROM audit_event WHERE request_id='$requestID'; DELETE FROM idempotency_record WHERE idempotency_key='$key'; COMMIT;" | Out-Null
|
||||||
Write-Host '已清理验收快照烟测审计和幂等记录;快照本身不在服务端留存。'
|
Write-Host '已清理验收快照烟测审计和幂等记录;快照本身不在服务端留存。'
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ try {
|
|||||||
if ($result.status -ne 'EXTERNAL_VALIDATION_PENDING' -or $result.pending -ne 10 -or $result.verified -ne 0 -or $result.sha256 -notmatch '^sha256:[a-f0-9]{64}$') { throw '证据包生成结果不正确' }
|
if ($result.status -ne 'EXTERNAL_VALIDATION_PENDING' -or $result.pending -ne 10 -or $result.verified -ne 0 -or $result.sha256 -notmatch '^sha256:[a-f0-9]{64}$') { throw '证据包生成结果不正确' }
|
||||||
Expand-Archive -LiteralPath $bundlePath -DestinationPath $expandedPath
|
Expand-Archive -LiteralPath $bundlePath -DestinationPath $expandedPath
|
||||||
$manifest = Get-Content -Raw -LiteralPath (Join-Path $expandedPath 'release-manifest.json') | ConvertFrom-Json
|
$manifest = Get-Content -Raw -LiteralPath (Join-Path $expandedPath 'release-manifest.json') | ConvertFrom-Json
|
||||||
if (-not $manifest.technical_gate_satisfied -or $manifest.evidence_status -ne 'EXTERNAL_VALIDATION_PENDING' -or $manifest.pending_count -ne 10 -or $manifest.openapi_version -ne '0.66.0' -or $manifest.schema_version -ne 'release-candidate-evidence-bundle-v3' -or -not $manifest.pilot_evidence_included -or $manifest.pilot_daily_count -ne 7 -or $manifest.pilot_week_start -ne $weekStartText -or $manifest.pilot_timezone -ne 'Asia/Shanghai') { throw '证据包清单内容不正确' }
|
if (-not $manifest.technical_gate_satisfied -or $manifest.evidence_status -ne 'EXTERNAL_VALIDATION_PENDING' -or $manifest.pending_count -ne 10 -or $manifest.openapi_version -ne '0.67.0' -or $manifest.schema_version -ne 'release-candidate-evidence-bundle-v3' -or -not $manifest.pilot_evidence_included -or $manifest.pilot_daily_count -ne 7 -or $manifest.pilot_week_start -ne $weekStartText -or $manifest.pilot_timezone -ne 'Asia/Shanghai') { throw '证据包清单内容不正确' }
|
||||||
if ($manifest.firmware_validation.status -ne 'PENDING' -or $manifest.firmware_validation.coverage_complete -or $manifest.firmware_validation.required_device_count -ne 3 -or @($manifest.firmware_validation.required_scenarios).Count -ne 4 -or @($manifest.firmware_validation.gaps).Count -ne 5) { throw '真机固件场景缺口未在证据包中保留' }
|
if ($manifest.firmware_validation.status -ne 'PENDING' -or $manifest.firmware_validation.coverage_complete -or $manifest.firmware_validation.required_device_count -ne 3 -or @($manifest.firmware_validation.required_scenarios).Count -ne 4 -or @($manifest.firmware_validation.gaps).Count -ne 5) { throw '真机固件场景缺口未在证据包中保留' }
|
||||||
if ($manifest.alert_receiver_validation.status -ne 'PENDING' -or $manifest.alert_receiver_validation.coverage_complete -or $manifest.alert_receiver_validation.report_present -or @($manifest.alert_receiver_validation.gaps).Count -ne 2) { throw '真实告警接收器与正式演练缺口未在证据包中保留' }
|
if ($manifest.alert_receiver_validation.status -ne 'PENDING' -or $manifest.alert_receiver_validation.coverage_complete -or $manifest.alert_receiver_validation.report_present -or @($manifest.alert_receiver_validation.gaps).Count -ne 2) { throw '真实告警接收器与正式演练缺口未在证据包中保留' }
|
||||||
if ($manifest.kms_rotation_validation.status -ne 'PENDING' -or $manifest.kms_rotation_validation.coverage_complete -or $manifest.kms_rotation_validation.report_present -or @($manifest.kms_rotation_validation.gaps).Count -ne 3) { throw '真实KMS/HSM两类轮换缺口未在证据包中保留' }
|
if ($manifest.kms_rotation_validation.status -ne 'PENDING' -or $manifest.kms_rotation_validation.coverage_complete -or $manifest.kms_rotation_validation.report_present -or @($manifest.kms_rotation_validation.gaps).Count -ne 3) { throw '真实KMS/HSM两类轮换缺口未在证据包中保留' }
|
||||||
|
|||||||
Vendored
+15
-1
@@ -2709,6 +2709,19 @@ export interface components {
|
|||||||
/** @description 仅当快照未超过15分钟且综合状态及全部检查均为healthy时为true。 */
|
/** @description 仅当快照未超过15分钟且综合状态及全部检查均为healthy时为true。 */
|
||||||
gate_satisfied: boolean;
|
gate_satisfied: boolean;
|
||||||
};
|
};
|
||||||
|
OpsAcceptanceProductionInfrastructureState: {
|
||||||
|
/** @description 目标环境为PRODUCTION时为true。 */
|
||||||
|
applicable: boolean;
|
||||||
|
/** @enum {string} */
|
||||||
|
deployment_profile: "MISSING" | "demo" | "production";
|
||||||
|
/** @description 最近主机报告证明异地备份已启用、已回读验证且未过期。 */
|
||||||
|
offsite_backup_ready: boolean;
|
||||||
|
/** @description 最近主机报告证明真实外部告警接收器已配置且密钥可读。 */
|
||||||
|
external_alert_receiver_ready: boolean;
|
||||||
|
/** @description 最近主机报告证明模拟语音与模拟告警服务均未运行。 */
|
||||||
|
mock_services_disabled: boolean;
|
||||||
|
gate_satisfied: boolean;
|
||||||
|
};
|
||||||
OpsAcceptanceDependencyState: {
|
OpsAcceptanceDependencyState: {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
service_name: "api" | "worker";
|
service_name: "api" | "worker";
|
||||||
@@ -2773,7 +2786,7 @@ export interface components {
|
|||||||
};
|
};
|
||||||
OpsAcceptanceGate: {
|
OpsAcceptanceGate: {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
code: "candidate_build_identified" | "database_migration_clean" | "required_dependencies_available" | "integration_issues_clear" | "outbox_clear" | "critical_alerts_clear" | "platform_health_current";
|
code: "candidate_build_identified" | "database_migration_clean" | "required_dependencies_available" | "integration_issues_clear" | "outbox_clear" | "critical_alerts_clear" | "platform_health_current" | "production_infrastructure_ready";
|
||||||
satisfied: boolean;
|
satisfied: boolean;
|
||||||
summary: string;
|
summary: string;
|
||||||
};
|
};
|
||||||
@@ -2800,6 +2813,7 @@ export interface components {
|
|||||||
outbox: components["schemas"]["OpsAcceptanceOutboxState"];
|
outbox: components["schemas"]["OpsAcceptanceOutboxState"];
|
||||||
alerts: components["schemas"]["OpsAcceptanceAlertState"];
|
alerts: components["schemas"]["OpsAcceptanceAlertState"];
|
||||||
platform_health: components["schemas"]["OpsAcceptancePlatformHealthState"];
|
platform_health: components["schemas"]["OpsAcceptancePlatformHealthState"];
|
||||||
|
production_infrastructure: components["schemas"]["OpsAcceptanceProductionInfrastructureState"];
|
||||||
gates: components["schemas"]["OpsAcceptanceGate"][];
|
gates: components["schemas"]["OpsAcceptanceGate"][];
|
||||||
overall_gate_satisfied: boolean;
|
overall_gate_satisfied: boolean;
|
||||||
disclaimer: string;
|
disclaimer: string;
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ function serviceLabel(value:OpsExternalDependency["service_name"]){return ({api:
|
|||||||
function externalDependencyState(item:OpsExternalDependency){const stale=Date.now()-new Date(item.checked_at).getTime()>item.probe_interval_ms*3;return stale?{label:"结果过期",tone:"warning",stale:true}:item.status==="AVAILABLE"?{label:"可用",tone:"active",stale:false}:{label:"不可用",tone:"danger",stale:false}}
|
function externalDependencyState(item:OpsExternalDependency){const stale=Date.now()-new Date(item.checked_at).getTime()>item.probe_interval_ms*3;return stale?{label:"结果过期",tone:"warning",stale:true}:item.status==="AVAILABLE"?{label:"可用",tone:"active",stale:false}:{label:"不可用",tone:"danger",stale:false}}
|
||||||
function platformHealthState(item:PlatformHealthSnapshot){const stale=Date.now()-new Date(item.observed_at).getTime()>15*60*1000;if(stale)return{label:"上报过期",tone:"danger",stale:true};return item.status==="healthy"?{label:"运行正常",tone:"active",stale:false}:item.status==="warning"?{label:"存在预警",tone:"warning",stale:false}:{label:"严重异常",tone:"danger",stale:false}}
|
function platformHealthState(item:PlatformHealthSnapshot){const stale=Date.now()-new Date(item.observed_at).getTime()>15*60*1000;if(stale)return{label:"上报过期",tone:"danger",stale:true};return item.status==="healthy"?{label:"运行正常",tone:"active",stale:false}:item.status==="warning"?{label:"存在预警",tone:"warning",stale:false}:{label:"严重异常",tone:"danger",stale:false}}
|
||||||
function platformHealthCheckStatusLabel(value:PlatformHealthSnapshot["checks"][number]["status"]){return value==="healthy"?"正常":value==="warning"?"预警":"严重"}
|
function platformHealthCheckStatusLabel(value:PlatformHealthSnapshot["checks"][number]["status"]){return value==="healthy"?"正常":value==="warning"?"预警":"严重"}
|
||||||
function platformHealthCheckLabel(value:string){const labels:Record<string,string>={public_origin:"公网地址",public_https:"HTTPS入口",public_tcp_listeners:"公网监听端口",oidc_issuer:"身份签发地址",backup_freshness:"本地备份",offsite_backup:"异地备份",backup_service:"备份服务","platform-backup.timer":"备份定时器","platform-offsite-backup.timer":"异地备份定时器","certbot-ip-renew.timer":"证书续期定时器",certificate:"HTTPS证书",disk_usage:"磁盘空间",container_postgres:"PostgreSQL",container_redis:"Redis",container_minio:"对象存储",container_keycloak:"身份服务",container_api:"业务API",container_callbacks:"回调服务",container_worker:"后台任务",container_web:"前端页面",container_gateway:"HTTPS网关","container_mock-provider":"模拟语音提供器","container_mock-alert-receiver":"模拟告警接收器"};return labels[value]??value}
|
function platformHealthCheckLabel(value:string){const labels:Record<string,string>={deployment_profile:"部署模式",external_alert_receiver:"外部告警接收器",mock_services:"模拟服务",public_origin:"公网地址",public_https:"HTTPS入口",public_tcp_listeners:"公网监听端口",oidc_issuer:"身份签发地址",backup_freshness:"本地备份",offsite_backup:"异地备份",backup_service:"备份服务","platform-backup.timer":"备份定时器","platform-offsite-backup.timer":"异地备份定时器","certbot-ip-renew.timer":"证书续期定时器",certificate:"HTTPS证书",disk_usage:"磁盘空间",container_postgres:"PostgreSQL",container_redis:"Redis",container_minio:"对象存储",container_keycloak:"身份服务",container_api:"业务API",container_callbacks:"回调服务",container_worker:"后台任务",container_web:"前端页面",container_gateway:"HTTPS网关","container_mock-provider":"模拟语音提供器","container_mock-alert-receiver":"模拟告警接收器"};return labels[value]??value}
|
||||||
function platformHealthValueLabel(_name:string,value:string){if(value==="not_required")return "当前未要求启用";if(value==="issuer_matches")return "签发地址一致";if(value==="http_200")return "公网访问成功";if(value==="enabled/active")return "已启用并运行";if(value==="success")return "最近执行成功";if(value.startsWith("ports="))return `当前对外端口 ${value.slice(6)}`;if(value.startsWith("unexpected_ports="))return `发现未授权端口 ${value.slice(17)}`;if(value.startsWith("age_seconds=")){const seconds=Number(value.slice(12));return Number.isFinite(seconds)?`距最近成功 ${formatDuration(seconds*1000)}`:value}if(value.startsWith("remaining_seconds=")){const seconds=Number(value.slice(18));return Number.isFinite(seconds)?`剩余 ${Math.floor(seconds/86400)} 天` : value}if(value.startsWith("used_percent="))return `已使用 ${value.slice(13)}%`;if(value==="running/healthy"||value==="running/none")return "容器正在运行";return value}
|
function platformHealthValueLabel(_name:string,value:string){if(value==="demo")return "演示环境";if(value==="production")return "生产环境";if(value==="not_configured")return "尚未配置";if(value==="configured")return "已配置";if(value==="enabled_for_demo")return "演示模式已启用";if(value==="disabled")return "已关闭";if(value==="unexpected_running")return "生产环境仍在运行模拟服务";if(value==="not_required")return "当前未要求启用";if(value==="issuer_matches")return "签发地址一致";if(value==="http_200")return "公网访问成功";if(value==="enabled/active")return "已启用并运行";if(value==="success")return "最近执行成功";if(value.startsWith("ports="))return `当前对外端口 ${value.slice(6)}`;if(value.startsWith("unexpected_ports="))return `发现未授权端口 ${value.slice(17)}`;if(value.startsWith("age_seconds=")){const seconds=Number(value.slice(12));return Number.isFinite(seconds)?`距最近成功 ${formatDuration(seconds*1000)}`:value}if(value.startsWith("remaining_seconds=")){const seconds=Number(value.slice(18));return Number.isFinite(seconds)?`剩余 ${Math.floor(seconds/86400)} 天` : value}if(value.startsWith("used_percent="))return `已使用 ${value.slice(13)}%`;if(value==="running/healthy"||value==="running/none")return "容器正在运行";return value}
|
||||||
function downloadJSON(value:unknown,filename:string){const blob=new Blob([JSON.stringify(value,null,2)],{type:"application/json"});const url=URL.createObjectURL(blob);const anchor=document.createElement("a");anchor.href=url;anchor.download=filename;anchor.click();URL.revokeObjectURL(url)}
|
function downloadJSON(value:unknown,filename:string){const blob=new Blob([JSON.stringify(value,null,2)],{type:"application/json"});const url=URL.createObjectURL(blob);const anchor=document.createElement("a");anchor.href=url;anchor.download=filename;anchor.click();URL.revokeObjectURL(url)}
|
||||||
function acceptanceGateLabel(value:OpsAcceptanceSnapshot["gates"][number]["code"]){return ({candidate_build_identified:"候选构建",database_migration_clean:"数据库迁移",required_dependencies_available:"必需外部依赖",integration_issues_clear:"高严重度问题",outbox_clear:"可靠任务",critical_alerts_clear:"高严重度告警",platform_health_current:"云端主机健康"} as Record<OpsAcceptanceSnapshot["gates"][number]["code"],string>)[value]}
|
function acceptanceGateLabel(value:OpsAcceptanceSnapshot["gates"][number]["code"]){return ({candidate_build_identified:"候选构建",database_migration_clean:"数据库迁移",required_dependencies_available:"必需外部依赖",integration_issues_clear:"高严重度问题",outbox_clear:"可靠任务",critical_alerts_clear:"高严重度告警",platform_health_current:"云端主机健康",production_infrastructure_ready:"生产基础设施"} as Record<OpsAcceptanceSnapshot["gates"][number]["code"],string>)[value]}
|
||||||
function acceptanceResourceStateLabel(value:OpsAcceptanceSnapshot["alerts"]["blocking_items"][number]["resource_state"]){return ({PRESENT:"资源存在",MISSING:"资源已不存在,仍需人工复核关闭",AGGREGATE:"聚合队列",UNKNOWN:"未提供自动核对规则"} as Record<OpsAcceptanceSnapshot["alerts"]["blocking_items"][number]["resource_state"],string>)[value]}
|
function acceptanceResourceStateLabel(value:OpsAcceptanceSnapshot["alerts"]["blocking_items"][number]["resource_state"]){return ({PRESENT:"资源存在",MISSING:"资源已不存在,仍需人工复核关闭",AGGREGATE:"聚合队列",UNKNOWN:"未提供自动核对规则"} as Record<OpsAcceptanceSnapshot["alerts"]["blocking_items"][number]["resource_state"],string>)[value]}
|
||||||
|
|||||||
Reference in New Issue
Block a user