diff --git a/deploy/server/README.md b/deploy/server/README.md index f34abaa..b259577 100644 --- a/deploy/server/README.md +++ b/deploy/server/README.md @@ -56,7 +56,7 @@ python3 -m json.tool /var/lib/data-terminal-cloud-monitor/status.json 监测任务每五分钟执行,正常状态为`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`联调。 +生产候选不能只修改部署模式。生产验收快照同时要求:`HOST_DEPLOYMENT_PROFILE=production`;API、Callbacks和Worker实际容器`APP_ENV=prod`;Keycloak不使用`start-dev`;OSS异地备份最近一次上传与回读验证成功;真实外部告警接收器已配置;`mock-provider`和`mock-alert-receiver`均不运行。任一条件缺失都会阻断生产基础设施门禁,但不会影响`LAB`或`STAGING`联调。当前Compose覆盖层固定为演示用途,不能通过改标签升级成生产部署。 ## OSS异地备份适配 diff --git a/deploy/server/health-monitor.sh b/deploy/server/health-monitor.sh index e924755..6248167 100755 --- a/deploy/server/health-monitor.sh +++ b/deploy/server/health-monitor.sh @@ -154,6 +154,46 @@ for service in "${required_services[@]}"; do fi done +application_environments=() +application_environment_missing=false +for application_service in api callbacks worker; do + application_container=$("${compose[@]}" ps -q "$application_service") + if [[ -z "$application_container" ]]; then + application_environment_missing=true + continue + fi + application_environment=$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$application_container" | sed -n 's/^APP_ENV=//p' | head -n1) + if [[ -z "$application_environment" ]]; then + application_environment_missing=true + else + application_environments+=("$application_environment") + fi +done +if [[ "$application_environment_missing" == true || ${#application_environments[@]} -ne 3 ]]; then + record_check application_environment critical missing +elif [[ "${application_environments[0]}" != "${application_environments[1]}" || "${application_environments[0]}" != "${application_environments[2]}" ]]; then + record_check application_environment critical mixed +elif [[ "$deployment_profile" == production && "${application_environments[0]}" != prod ]]; then + record_check application_environment critical "${application_environments[0]}" +elif [[ "$deployment_profile" == demo && "${application_environments[0]}" != local ]]; then + record_check application_environment critical "${application_environments[0]}" +else + record_check application_environment healthy "${application_environments[0]}" +fi + +keycloak_container=$("${compose[@]}" ps -q keycloak) +if [[ -z "$keycloak_container" ]]; then + record_check identity_service_mode critical missing +elif docker inspect -f '{{json .Config.Cmd}}' "$keycloak_container" | grep -q 'start-dev'; then + if [[ "$deployment_profile" == production ]]; then + record_check identity_service_mode critical development + else + record_check identity_service_mode healthy development + fi +else + record_check identity_service_mode healthy hardened +fi + mock_services_running=() for mock_service in mock-provider mock-alert-receiver; do mock_container=$("${compose[@]}" ps -q "$mock_service") diff --git a/docs/development-status.md b/docs/development-status.md index ef3f907..def0bb6 100644 --- a/docs/development-status.md +++ b/docs/development-status.md @@ -1,7 +1,7 @@ # 开发状态与门禁记录 > 更新日期:2026-08-26 -> 当前接口契约版本:0.67.0 +> 当前接口契约版本:0.68.0 > 当前主里程碑:M8目标环境联调、试点与上线验收(外部条件待提供) ## 阶段状态 @@ -181,7 +181,7 @@ | 隐私删除并发 | 100个并发受理全部成功、HTTP失败率0%、提交p95 385.15ms;11.58秒内600个子任务、100个墓碑和100个主体冻结全部对账 | | 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健康检查,结构化状态覆盖部署模式、外部告警配置、HTTPS、宿主机公网监听端口、OIDC签发地址、核心容器、模拟服务、本机/异地备份状态与摘要、备份执行结果、三个运行timer、短期IP证书剩余时间和磁盘阈值。监听策略只允许非回环TCP 22/80/443,回环数据库端口不误报,额外端口会进入critical。演示正常路径26项全部healthy;生产模式要求模拟服务退出。可选Webhook仅在状态变化时发送带时间戳和HMAC-SHA256的脱敏状态;真实接收器尚未配置,因此不作为值班通知送达证据 | +| 阿里云单机健康监测 | 目标服务器已安装五分钟systemd健康检查,结构化状态覆盖部署模式、实际应用运行环境、身份服务模式、外部告警配置、HTTPS、宿主机公网监听端口、OIDC签发地址、核心容器、模拟服务、本机/异地备份状态与摘要、备份执行结果、三个运行timer、短期IP证书剩余时间和磁盘阈值。监听策略只允许非回环TCP 22/80/443,回环数据库端口不误报,额外端口会进入critical。演示正常路径28项全部healthy;生产模式要求三个业务进程实际运行在`prod`、Keycloak不使用`start-dev`并退出模拟服务。可选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`,不能作为真实异地备份证据 | | 进程与Redis恢复 | API/Callbacks/Worker重启恢复;停机期间Outbox仅执行一次;Redis 14个热状态键清空后由签名设备请求重建,PostgreSQL权威计数不变 | | 供应商故障恢复 | V23持久化按租户/供应商的可用状态;连续临时失败暂停新启动但停止任务继续尝试;演练确认暂停后新外呼0、停止尝试2次、历史接口200、CRITICAL告警出现,恢复后全部收敛;V23单版本回退/重升通过 | @@ -252,6 +252,7 @@ | 云端主机健康状态接入 | OpenAPI 0.65.0新增独立HMAC签名的主机健康回调、30天脱敏快照表和运营只读状态页面。五分钟周期上报覆盖公网入口、容器、备份、证书、磁盘和定时器;无签名、过期、重复检查及综合状态不一致报告被拒绝,页面超过15分钟未更新明确标记过期。该能力不替代整机、Callbacks或数据库失联时必须使用的外部探针。 | | 云端主机健康发布门禁 | OpenAPI 0.66.0把最新主机健康快照纳入发布候选技术验收:无上报、超过15分钟、综合预警或严重异常均阻断,只有当前有效且23项检查全部正常才通过,其中包括宿主机公网监听端口策略。快照仅归档状态、时间和检查数量,不复制磁盘、证书、监听地址或备份明细值。 | | 生产基础设施强制门禁 | OpenAPI 0.67.0把生产基础设施状态作为第八项技术门禁。`PRODUCTION`快照必须从当前主机健康报告同时证明`production`部署模式、OSS异地备份上传回读校验不过期、外部告警接收器已配置及两个Mock服务均已退出;演示机继续明确上报`demo`,因此生产快照会被如实阻断。`LAB/STAGING`标记为不适用,不影响开发联调。前端同步展示部署模式、告警和Mock状态;该门禁只证明配置与当前进程状态,不替代真实告警送达演练和生产基础设施一致性报告。 | +| 生产运行时完整性门禁 | OpenAPI 0.68.0继续收紧第八项门禁:主机健康脚本直接读取API、Callbacks和Worker容器的`APP_ENV`及Keycloak实际启动命令。生产快照新增`application_runtime_ready`和`identity_service_ready`,只有三个进程均为`prod`且身份服务不使用`start-dev`才可通过;只修改`HOST_DEPLOYMENT_PROFILE`会同时触发运行环境和身份模式严重检查。演示路径保持`local/development`且功能可用,前端用业务化中文展示两项状态。 | | 阶段8外部执行轻量预检 | 新增`scripts/stage8-preflight.ps1`,以单一只读入口核对候选Git提交、远程仓库、Compose本地基线、外部配置变量名称和十类证据索引结构/状态;报告明确不读取密钥值,也不把环境变量或本地服务可用性当作验收证据。默认仅报告缺口,`-Strict`可供自动化阻断未就绪交接;当前已建立本地`main`候选提交,本地运行基线就绪且索引结构有效,仍缺远程仓库,样例构建摘要为占位值,十类外部证据均为`PENDING`。 | | 候选构建身份贯通 | Compose新增统一候选构建参数,后端二进制和后端/Web镜像OCI标签共享版本、完整Git提交及UTC构建时间;远程CI从OpenAPI版本和运行编号形成候选版本,构建后重新读取两个镜像标签并与`GITHUB_SHA`和构建时间逐项比对,再把候选版本、提交、时间和双镜像摘要写入构建元数据。修复了此前Dockerfile虽支持注入但Compose/CI未传参、运行版本仍为`dev/unknown`的问题。 | diff --git a/docs/device-api-compatibility.md b/docs/device-api-compatibility.md index b787abe..933cdaa 100644 --- a/docs/device-api-compatibility.md +++ b/docs/device-api-compatibility.md @@ -1,7 +1,7 @@ # 设备 API 兼容性约定 > 当前设备接口主版本:`/device/v2` -> 当前接口契约:OpenAPI 0.67.0 +> 当前接口契约:OpenAPI 0.68.0 > 适用对象:云端 API、终端固件、模拟终端和设备联调工具 ## 1. 兼容目标 @@ -42,6 +42,7 @@ CI在拉取请求中以目标分支OpenAPI为基线执行破坏性变更检查 | OpenAPI版本 | 设备兼容变更 | 兼容结论 | |---|---|---| +| 0.68.0 | 生产基础设施状态增加实际应用运行环境与身份服务模式证明;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 | | 0.67.0 | 发布候选快照增加生产基础设施强制门禁及主机部署模式、异地备份、外部告警、模拟服务状态;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 | | 0.66.0 | 发布候选技术验收快照增加主机健康当前性门禁;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 | | 0.65.0 | 新增主机健康签名回调和运营只读查询;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 | diff --git a/docs/external-acceptance-checklist.md b/docs/external-acceptance-checklist.md index 19cb140..0dbdbf8 100644 --- a/docs/external-acceptance-checklist.md +++ b/docs/external-acceptance-checklist.md @@ -97,7 +97,7 @@ powershell -ExecutionPolicy Bypass -File scripts/stage8-preflight.ps1 ` - 最近两个Chrome/Edge和macOS最近两个Safari版本通过关键流程; - 使用`cmd/safari-conformance`归档同一候选在真实Safari `CURRENT/PREVIOUS`、两视口和六流程的24项结果;报告必须为`PASS/REAL_SAFARI`,证据包内`safari_compatibility_validation.coverage_complete=true`。 - 核心模块覆盖率和关键分支覆盖率达到需求规定门槛,当前本地未达标项必须在发布前关闭或由风险负责人书面接受。 -- 已使用生产候选镜像摘要生成`PRODUCTION_RELEASE`验收快照;运行版本必须等于候选版本且包含提交号和构建时间,八项技术门禁全部通过,其中生产基础设施门禁必须证明生产部署模式、已验证异地备份、真实外部告警配置和Mock服务退出。 +- 已使用生产候选镜像摘要生成`PRODUCTION_RELEASE`验收快照;运行版本必须等于候选版本且包含提交号和构建时间,八项技术门禁全部通过,其中生产基础设施门禁必须证明生产部署模式、三个业务进程实际运行在`prod`、身份服务不是开发模式、异地备份已验证、真实外部告警已配置和Mock服务已退出。 - 使用`cmd/release-evidence`为同一候选版本、镜像摘要和源提交生成最终证据包;十类证据索引不得存在`PENDING`。出现`WAIVED`时必须附有权责任人的书面风险接受,包状态只能进入带豁免的发布评审,不能自动视为批准;八类不可豁免门禁不得使用`WAIVED`。 ### 4.2 业务与合规门禁 diff --git a/docs/platform-health-reporting.md b/docs/platform-health-reporting.md index bb9a6af..440f102 100644 --- a/docs/platform-health-reporting.md +++ b/docs/platform-health-reporting.md @@ -35,7 +35,7 @@ | `POST /callbacks/v1/platform-health` | 独立HMAC签名 | 新快照返回201;完全重复返回200 | | `GET /api/v1/ops/diagnostics/platform-health` | OIDC + 运营角色 + 客户组织选择 | 返回全平台最新脱敏快照 | -接口契约见`openapi/openapi.yaml` 0.67.0。最新快照同时进入发布候选技术验收门禁:缺失、超过15分钟、warning或critical都会阻断,只有当前healthy快照通过。`PRODUCTION`快照还执行独立的生产基础设施门禁:主机必须明确上报`production`部署模式,异地备份必须已完成远端回读校验且不过期,外部告警接收器必须配置,两个模拟服务必须停止。`LAB`和`STAGING`保留该门禁字段但标记为不适用,不影响现有开发与联调。 +接口契约见`openapi/openapi.yaml` 0.68.0。最新快照同时进入发布候选技术验收门禁:缺失、超过15分钟、warning或critical都会阻断,只有当前healthy快照通过。`PRODUCTION`快照还执行独立的生产基础设施门禁:主机必须明确上报`production`部署模式,API、Callbacks和Worker实际容器环境必须均为`prod`,身份服务不得使用Keycloak开发启动模式,异地备份必须已完成远端回读校验且不过期,外部告警接收器必须配置,两个模拟服务必须停止。`LAB`和`STAGING`保留该门禁字段但标记为不适用,不影响现有开发与联调。 ## 5. 单机配置 @@ -47,7 +47,7 @@ PLATFORM_HEALTH_REPORT_SECRET_FILE=/etc/data-terminal-cloud/platform-health-call HOST_DEPLOYMENT_PROFILE=demo ``` -不得仅把`HOST_DEPLOYMENT_PROFILE`改为`production`来制造放行结果。切换前应先停用模拟服务,启用并验证OSS异地备份,配置真实外部告警接收器及HMAC密钥,再执行一次主机健康检查并重新生成生产快照。 +不得仅把`HOST_DEPLOYMENT_PROFILE`改为`production`来制造放行结果。监控脚本会读取三个业务进程的实际`APP_ENV`和Keycloak启动命令。切换前应先使用能通过服务端生产配置校验的`APP_ENV=prod`部署,停用身份服务开发模式和模拟服务,启用并验证OSS异地备份,配置真实外部告警接收器及HMAC密钥,再执行一次主机健康检查并重新生成生产快照。 启用后执行: diff --git a/docs/release-evidence-bundle.md b/docs/release-evidence-bundle.md index 0265685..a3f3558 100644 --- a/docs/release-evidence-bundle.md +++ b/docs/release-evidence-bundle.md @@ -4,7 +4,7 @@ `cmd/release-evidence`用于阶段8联调、试点和生产发布前,把同一候选版本的技术验收快照、外部验收状态、真机固件一致性报告、接口契约、数据库迁移、部署清单、关键交付文档和可用SBOM封装为一个带SHA-256清单的ZIP。`PILOT`包还必须携带同一客户组织、同一业务时区、上一完整自然周的7份日报和1份周报。它解决证据散落、候选身份不一致、真机场景缺口被模拟结果掩盖、试点周期证据缺失以及归档后无法判断文件是否被替换的问题。 -证据包不是发布批准。生成器只在技术验收快照八项门禁全部通过时工作;其中生产快照必须额外证明生产部署模式、已验证异地备份、外部告警配置和模拟服务退出。外部证据可以保持`PENDING`,此时包内状态固定为`EXTERNAL_VALIDATION_PENDING`。真实设备、供应商、KMS、告警接收器、Safari或业务签署不能用本地模拟结果替代。 +证据包不是发布批准。生成器只在技术验收快照八项门禁全部通过时工作;其中生产快照必须额外证明生产部署模式、业务进程实际`prod`环境、身份服务非开发模式、已验证异地备份、外部告警配置和模拟服务退出。外部证据可以保持`PENDING`,此时包内状态固定为`EXTERNAL_VALIDATION_PENDING`。真实设备、供应商、KMS、告警接收器、Safari或业务签署不能用本地模拟结果替代。 ## 2. 输入设计 @@ -143,4 +143,4 @@ go test ./internal/releaseevidence ./cmd/release-evidence .\scripts\smoke-release-evidence.ps1 ``` -烟测使用真实OIDC生成绑定同一候选版本和API镜像摘要的`PILOT`技术验收快照、上一完整自然周的7份日报和1份周报,构造十项全部`PENDING`的外部索引,生成并解包临时ZIP,核对OpenAPI 0.67.0、v3清单、候选一致性、周期证据、摘要、待验计数和真机固件四场景缺口,随后清理临时文件、审计和幂等记录。技术验收快照还要求主机健康上报在15分钟内且没有warning/critical;生产类型另要求生产基础设施门禁通过。该结果只证明打包链路可用,不证明十项外部验收、真机固件或投诉证据已经完成。 +烟测使用真实OIDC生成绑定同一候选版本和API镜像摘要的`PILOT`技术验收快照、上一完整自然周的7份日报和1份周报,构造十项全部`PENDING`的外部索引,生成并解包临时ZIP,核对OpenAPI 0.68.0、v3清单、候选一致性、周期证据、摘要、待验计数和真机固件四场景缺口,随后清理临时文件、审计和幂等记录。技术验收快照还要求主机健康上报在15分钟内且没有warning/critical;生产类型另要求生产部署标记、实际应用环境、身份服务模式、异地备份、外部告警和Mock退出全部通过。该结果只证明打包链路可用,不证明十项外部验收、真机固件或投诉证据已经完成。 diff --git a/docs/requirements-traceability.md b/docs/requirements-traceability.md index e8ef595..012ca99 100644 --- a/docs/requirements-traceability.md +++ b/docs/requirements-traceability.md @@ -87,7 +87,7 @@ | 来源 | 要求 | 当前处理 | |---|---|---| -| 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`和本地模拟证据均不会被误报为外部验收完成,整体发布仍必须基于真实联调、灰度和审批记录确认。 | +| P0414 | 首期需求、验收用例、缺陷和风险形成对应关系 | 本文完成首期功能到代码、接口、数据和验证的基线;V30问题清单由V36补充严重级别、根因、修复版本、回归证据及未关闭HIGH/CRITICAL发布阻断汇总;OpenAPI 0.50.0—0.68.0可按候选构建导出八项技术门禁及脱敏阻断告警定位快照,绑定候选版本/构建摘要后按业务时区归档日/周运行证据。第七项门禁要求主机健康快照在15分钟内且全部检查正常;第八项在`PRODUCTION`强制要求生产部署模式、三个业务进程实际`APP_ENV=prod`、身份服务非开发模式、已验证异地备份、外部告警配置及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`。 | 当前仍不能宣告完成的部分集中为:真实火山RTC与豆包三模式联调、真实方舟内容与Embedding质量、真实听筒的长者交流体验、量产设备/KMS注入、跨故障域生产对象存储与备份、真机异常恢复、真实账单和生产级内容安全。阿里云单机环境已经形成加密全组件归档、数据库隔离恢复和Redis RDB校验,但同机归档不能替代托管PITR、异地副本和生产恢复演练。这些剩余项属于阶段8外部验收,不应通过增加本地低价值测试伪造完成证据。 diff --git a/docs/server-health-monitoring.md b/docs/server-health-monitoring.md index 0c25ad2..b77b677 100644 --- a/docs/server-health-monitoring.md +++ b/docs/server-health-monitoring.md @@ -11,6 +11,8 @@ | 检查 | 正常条件 | 失败状态 | |---|---|---| | 部署模式 | `HOST_DEPLOYMENT_PROFILE`只能为`demo`或`production` | 非法值直接拒绝执行 | +| 应用运行环境 | API、Callbacks和Worker实际容器`APP_ENV`必须一致;`demo`要求`local`,`production`要求`prod` | `missing`、`mixed`或与部署模式不符的实际值 | +| 身份服务模式 | `demo`可使用开发模式;`production`不得使用Keycloak `start-dev` | `missing`或`development` | | 外部告警接收器 | `demo`未配置时明确记录`not_configured`;`production`必须配置且HMAC密钥不少于32字节 | `not_configured`、`secret_missing`或`secret_invalid` | | 公网地址 | `PUBLIC_ORIGIN`为HTTPS | `invalid_https_origin` | | HTTPS入口 | `/api/v1/version`在10秒内返回成功 | `request_failed` | @@ -134,7 +136,7 @@ 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 ``` -生产分支可在不改变线上容器的情况下验证。临时配置`HOST_DEPLOYMENT_PROFILE=production`运行检查时,只要模拟服务仍在运行,结果必须为critical且`mock_services=unexpected_running`;测试状态目录应与正式目录隔离。 +生产分支可在不改变线上容器的情况下验证。临时配置`HOST_DEPLOYMENT_PROFILE=production`运行检查时,当前演示栈必须因`application_environment=local`、`identity_service_mode=development`、外部告警、异地备份及模拟服务同时进入critical;测试状态目录应与正式目录隔离。该检查读取容器实际环境和Keycloak启动命令,不能只改主机配置文件伪造生产结果。 ## 8. 下一步生产化 diff --git a/internal/diagnostics/acceptance_snapshot.go b/internal/diagnostics/acceptance_snapshot.go index 298ea83..70e2f77 100644 --- a/internal/diagnostics/acceptance_snapshot.go +++ b/internal/diagnostics/acceptance_snapshot.go @@ -85,6 +85,10 @@ type AcceptancePlatformHealthState struct { type AcceptanceProductionInfrastructureState struct { Applicable bool `json:"applicable"` DeploymentProfile string `json:"deployment_profile"` + ApplicationEnvironment string `json:"application_environment"` + IdentityServiceMode string `json:"identity_service_mode"` + ApplicationRuntimeReady bool `json:"application_runtime_ready"` + IdentityServiceReady bool `json:"identity_service_ready"` OffsiteBackupReady bool `json:"offsite_backup_ready"` ExternalAlertReceiverReady bool `json:"external_alert_receiver_ready"` MockServicesDisabled bool `json:"mock_services_disabled"` @@ -385,7 +389,12 @@ func acceptancePlatformHealthSummary(state AcceptancePlatformHealthState) string } func evaluateAcceptanceProductionInfrastructure(environment string, checks []PlatformHealthCheck) AcceptanceProductionInfrastructureState { - state := AcceptanceProductionInfrastructureState{Applicable: environment == "PRODUCTION", DeploymentProfile: "MISSING"} + state := AcceptanceProductionInfrastructureState{ + Applicable: environment == "PRODUCTION", + DeploymentProfile: "MISSING", + ApplicationEnvironment: "MISSING", + IdentityServiceMode: "MISSING", + } if !state.Applicable { state.GateSatisfied = true return state @@ -396,6 +405,12 @@ func evaluateAcceptanceProductionInfrastructure(environment string, checks []Pla case "deployment_profile": state.DeploymentProfile = check.Value profileReady = check.Status == "healthy" && check.Value == "production" + case "application_environment": + state.ApplicationEnvironment = check.Value + state.ApplicationRuntimeReady = check.Status == "healthy" && check.Value == "prod" + case "identity_service_mode": + state.IdentityServiceMode = check.Value + state.IdentityServiceReady = check.Status == "healthy" && check.Value == "hardened" case "offsite_backup": state.OffsiteBackupReady = check.Status == "healthy" && strings.HasPrefix(check.Value, "age_seconds=") case "external_alert_receiver": @@ -404,7 +419,7 @@ func evaluateAcceptanceProductionInfrastructure(environment string, checks []Pla state.MockServicesDisabled = check.Status == "healthy" && check.Value == "disabled" } } - state.GateSatisfied = profileReady && state.OffsiteBackupReady && state.ExternalAlertReceiverReady && state.MockServicesDisabled + state.GateSatisfied = profileReady && state.ApplicationRuntimeReady && state.IdentityServiceReady && state.OffsiteBackupReady && state.ExternalAlertReceiverReady && state.MockServicesDisabled return state } @@ -412,10 +427,16 @@ func acceptanceProductionInfrastructureSummary(state AcceptanceProductionInfrast if !state.Applicable { return "非生产候选不适用生产基础设施强制门禁" } - missing := make([]string, 0, 4) + missing := make([]string, 0, 6) if state.DeploymentProfile != "production" { missing = append(missing, "生产部署模式") } + if !state.ApplicationRuntimeReady { + missing = append(missing, "生产应用运行环境") + } + if !state.IdentityServiceReady { + missing = append(missing, "非开发模式身份服务") + } if !state.OffsiteBackupReady { missing = append(missing, "已验证异地备份") } @@ -428,7 +449,7 @@ func acceptanceProductionInfrastructureSummary(state AcceptanceProductionInfrast if len(missing) > 0 { return "生产基础设施尚缺:" + strings.Join(missing, "、") } - return "生产部署模式、异地备份、外部告警和模拟服务退出均已确认" + return "生产部署与应用环境、身份服务、异地备份、外部告警和模拟服务退出均已确认" } func acceptanceRuntimeSummary(environment string) string { diff --git a/internal/diagnostics/acceptance_snapshot_test.go b/internal/diagnostics/acceptance_snapshot_test.go index 5403ea9..4e7d457 100644 --- a/internal/diagnostics/acceptance_snapshot_test.go +++ b/internal/diagnostics/acceptance_snapshot_test.go @@ -74,16 +74,20 @@ func TestAcceptancePlatformHealthSummaryCoversMissingAndCounts(t *testing.T) { func TestEvaluateAcceptanceProductionInfrastructure(t *testing.T) { readyChecks := []PlatformHealthCheck{ {Name: "deployment_profile", Status: "healthy", Value: "production"}, + {Name: "application_environment", Status: "healthy", Value: "prod"}, + {Name: "identity_service_mode", Status: "healthy", Value: "hardened"}, {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 { + if !ready.Applicable || !ready.GateSatisfied || !ready.ApplicationRuntimeReady || !ready.IdentityServiceReady || !ready.OffsiteBackupReady || !ready.ExternalAlertReceiverReady || !ready.MockServicesDisabled { t.Fatalf("ready=%+v", ready) } demo := evaluateAcceptanceProductionInfrastructure("PRODUCTION", []PlatformHealthCheck{ {Name: "deployment_profile", Status: "healthy", Value: "demo"}, + {Name: "application_environment", Status: "healthy", Value: "local"}, + {Name: "identity_service_mode", Status: "healthy", Value: "development"}, {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"}, diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 419adb1..d83aa0a 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1,8 +1,8 @@ openapi: 3.1.0 info: title: 数据采集终端—云端系统 API - version: 0.67.0 - description: 工程框架、SaaS核心业务、设备v2能力协商与智能体启动状态查询、首次通话就绪与最低固件门禁、背景资料准备失败闭环、活动通话授权撤回安全停止、语音内容闭环、终端终稿补传、批量设备遥测、录音上传队列、隐私对账、固件、双轨录音、状态事件、具体资源分享、受控运营配置、Agent组合组件引用、质量证据门禁、运营自定义版本族投放、新通话运行时冻结与供应商启动传递、带并发版本校验的配置投放暂停恢复、带严重度和验收证据门禁的联调问题闭环、含阻断告警定位、当前主机健康及生产基础设施强制条件的发布候选技术验收快照、绑定候选版本与构建摘要并按业务时区生成的可审计试点日/周运行证据、可审计且不生成正式告警的接收器联通测试、外部依赖预检台账、证书到期清单,以及带价格版本冻结、供应商用量去重和硬额度控制的费用治理接口。 + version: 0.68.0 + description: 工程框架、SaaS核心业务、设备v2能力协商与智能体启动状态查询、首次通话就绪与最低固件门禁、背景资料准备失败闭环、活动通话授权撤回安全停止、语音内容闭环、终端终稿补传、批量设备遥测、录音上传队列、隐私对账、固件、双轨录音、状态事件、具体资源分享、受控运营配置、Agent组合组件引用、质量证据门禁、运营自定义版本族投放、新通话运行时冻结与供应商启动传递、带并发版本校验的配置投放暂停恢复、带严重度和验收证据门禁的联调问题闭环、含阻断告警定位、当前主机健康及生产部署标记、实际应用环境、身份服务模式、异地备份、外部告警与模拟服务强制条件的发布候选技术验收快照、绑定候选版本与构建摘要并按业务时区生成的可审计试点日/周运行证据、可审计且不生成正式告警的接收器联通测试、外部依赖预检台账、证书到期清单,以及带价格版本冻结、供应商用量去重和硬额度控制的费用治理接口。 servers: - url: http://localhost:58080 description: 本地开发 @@ -3287,10 +3287,14 @@ components: OpsAcceptanceProductionInfrastructureState: type: object additionalProperties: false - required: [applicable, deployment_profile, offsite_backup_ready, external_alert_receiver_ready, mock_services_disabled, gate_satisfied] + required: [applicable, deployment_profile, application_environment, identity_service_mode, application_runtime_ready, identity_service_ready, 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] } + application_environment: { type: string, enum: [MISSING, local, test, stage, prod, mixed] } + identity_service_mode: { type: string, enum: [MISSING, missing, development, hardened] } + application_runtime_ready: { type: boolean, description: API、Callbacks和Worker实际容器环境均为prod时为true。 } + identity_service_ready: { type: boolean, description: 身份服务未使用start-dev开发模式时为true。 } offsite_backup_ready: { type: boolean, description: 最近主机报告证明异地备份已启用、已回读验证且未过期。 } external_alert_receiver_ready: { type: boolean, description: 最近主机报告证明真实外部告警接收器已配置且密钥可读。 } mock_services_disabled: { type: boolean, description: 最近主机报告证明模拟语音与模拟告警服务均未运行。 } diff --git a/scripts/smoke-release-evidence.ps1 b/scripts/smoke-release-evidence.ps1 index 51d184d..d091b2d 100644 --- a/scripts/smoke-release-evidence.ps1 +++ b/scripts/smoke-release-evidence.ps1 @@ -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 '证据包生成结果不正确' } Expand-Archive -LiteralPath $bundlePath -DestinationPath $expandedPath $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.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 (-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.68.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.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两类轮换缺口未在证据包中保留' } diff --git a/web/src/api/schema.d.ts b/web/src/api/schema.d.ts index 552f083..4140e86 100644 --- a/web/src/api/schema.d.ts +++ b/web/src/api/schema.d.ts @@ -2714,6 +2714,14 @@ export interface components { applicable: boolean; /** @enum {string} */ deployment_profile: "MISSING" | "demo" | "production"; + /** @enum {string} */ + application_environment: "MISSING" | "local" | "test" | "stage" | "prod" | "mixed"; + /** @enum {string} */ + identity_service_mode: "MISSING" | "missing" | "development" | "hardened"; + /** @description API、Callbacks和Worker实际容器环境均为prod时为true。 */ + application_runtime_ready: boolean; + /** @description 身份服务未使用start-dev开发模式时为true。 */ + identity_service_ready: boolean; /** @description 最近主机报告证明异地备份已启用、已回读验证且未过期。 */ offsite_backup_ready: boolean; /** @description 最近主机报告证明真实外部告警接收器已配置且密钥可读。 */ diff --git a/web/src/pages/OpsDiagnosticsWorkspace.tsx b/web/src/pages/OpsDiagnosticsWorkspace.tsx index 10e2304..6fc805d 100644 --- a/web/src/pages/OpsDiagnosticsWorkspace.tsx +++ b/web/src/pages/OpsDiagnosticsWorkspace.tsx @@ -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 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 platformHealthCheckLabel(value:string){const labels:Record={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==="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 platformHealthCheckLabel(value:string){const labels:Record={deployment_profile:"部署模式",application_environment:"应用运行环境",identity_service_mode:"身份服务模式",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(name==="application_environment"){if(value==="local")return "本地开发运行环境";if(value==="prod")return "生产运行环境";if(value==="stage")return "预发布运行环境";if(value==="test")return "测试运行环境";if(value==="mixed")return "各进程环境不一致"}if(name==="identity_service_mode"){if(value==="development")return "开发模式";if(value==="hardened")return "加固模式"}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 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)[value]} function acceptanceResourceStateLabel(value:OpsAcceptanceSnapshot["alerts"]["blocking_items"][number]["resource_state"]){return ({PRESENT:"资源存在",MISSING:"资源已不存在,仍需人工复核关闭",AGGREGATE:"聚合队列",UNKNOWN:"未提供自动核对规则"} as Record)[value]}