feat: monitor public listener exposure

This commit is contained in:
Codex Local
2026-08-26 22:38:36 +08:00
parent 7435af7c07
commit 3786db0de8
9 changed files with 164 additions and 9 deletions
+3 -1
View File
@@ -43,7 +43,9 @@ jobs:
- name: Validate compose
run: docker compose config --quiet
- name: Validate offsite backup authentication and round trip
run: bash deploy/server/test-sync-backup-offsite.sh
run: |
bash deploy/server/test-sync-backup-offsite.sh
python3 -B deploy/server/test_public_listener_policy.py
- name: Resolve immutable build identity
run: |
openapi_version="$(awk '/^ version:/{print $2; exit}' openapi/openapi.yaml)"
+2
View File
@@ -2,6 +2,8 @@
.idea/
.vscode/
.DS_Store
__pycache__/
*.py[cod]
*.log
coverage.out
coverage*.out
+1 -1
View File
@@ -54,7 +54,7 @@ sudo systemctl show platform-health-monitor.service -p Result -p ExecMainStatus
python3 -m json.tool /var/lib/data-terminal-cloud-monitor/status.json
```
监测任务每五分钟执行,正常状态为`healthy`。默认只记录结构化状态和systemd日志;在`/etc/data-terminal-cloud/monitor.env`配置`ALERT_WEBHOOK_URL`后,状态变化会发送带时间戳和HMAC-SHA256签名的JSON通知。完整检查项、签名规则、阈值和失败分支见[《单机云端健康监测与告警手册》](../../docs/server-health-monitoring.md)。
监测任务每五分钟执行,正常状态为`healthy`。它还要求宿主机所有非回环TCP监听只能使用22、80和443,避免安全组误放行与错误容器端口映射叠加后暴露数据库或缓存。默认只记录结构化状态和systemd日志;在`/etc/data-terminal-cloud/monitor.env`配置`ALERT_WEBHOOK_URL`后,状态变化会发送带时间戳和HMAC-SHA256签名的JSON通知。完整检查项、签名规则、阈值和失败分支见[《单机云端健康监测与告警手册》](../../docs/server-health-monitoring.md)。
## OSS异地备份适配
+19
View File
@@ -20,6 +20,7 @@ alert_webhook_url=${ALERT_WEBHOOK_URL:-}
alert_webhook_secret_file=${ALERT_WEBHOOK_SECRET_FILE:-/etc/data-terminal-cloud/monitor-webhook-secret}
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}
expected_public_tcp_ports=${EXPECTED_PUBLIC_TCP_PORTS:-22,80,443}
if [[ -f "$offsite_config_file" ]]; then
# shellcheck disable=SC1090
source "$offsite_config_file"
@@ -36,6 +37,10 @@ if (( disk_warning_percent >= disk_critical_percent || disk_critical_percent > 1
echo "Disk thresholds are invalid" >&2
exit 2
fi
if [[ ! "$expected_public_tcp_ports" =~ ^[0-9]+(,[0-9]+)*$ ]]; then
echo "EXPECTED_PUBLIC_TCP_PORTS must contain comma-separated integers" >&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")
mkdir -p "$state_dir"
@@ -81,6 +86,20 @@ else
record_check public_https critical request_failed
fi
if ! command -v ss >/dev/null 2>&1; then
record_check public_tcp_listeners critical probe_unavailable
else
listener_evidence=$(ss -H -ltn | python3 "$project_root/deploy/server/public_listener_policy.py" "$expected_public_tcp_ports" 2>/dev/null || true)
IFS='|' read -r listener_status listener_detail <<<"$listener_evidence"
if [[ "$listener_status" == healthy ]]; then
record_check public_tcp_listeners healthy "$listener_detail"
elif [[ "$listener_status" == critical ]]; then
record_check public_tcp_listeners critical "$listener_detail"
else
record_check public_tcp_listeners critical invalid_probe_result
fi
fi
expected_issuer="$public_origin/realms/voice-local"
if curl --fail --silent --show-error --max-time 10 \
"$expected_issuer/.well-known/openid-configuration" -o "$oidc_tmp"; then
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
import ipaddress
import re
import sys
def parse_endpoint(value: str) -> tuple[str, int] | None:
value = value.strip()
if value.startswith("[") and "]:" in value:
address, port_text = value[1:].rsplit("]:", 1)
elif ":" in value:
address, port_text = value.rsplit(":", 1)
else:
return None
if not port_text.isdigit():
return None
return address, int(port_text)
def externally_reachable(address: str) -> bool:
if address in {"*", "0.0.0.0", "::", "[::]"}:
return True
try:
return not ipaddress.ip_address(address.split("%", 1)[0]).is_loopback
except ValueError:
return True
def public_ports(lines: list[str]) -> set[int]:
ports: set[int] = set()
for line in lines:
fields = line.split()
if len(fields) < 4:
continue
endpoint = parse_endpoint(fields[3])
if endpoint is not None and externally_reachable(endpoint[0]):
ports.add(endpoint[1])
return ports
def parse_allowed(value: str) -> set[int]:
if not re.fullmatch(r"[0-9]+(?:,[0-9]+)*", value):
raise ValueError("allowed ports must be comma-separated integers")
ports = {int(item) for item in value.split(",")}
if any(port < 1 or port > 65535 for port in ports):
raise ValueError("allowed port is outside 1..65535")
return ports
def main() -> int:
if len(sys.argv) != 2:
print("usage: public_listener_policy.py ALLOWED_PORTS", file=sys.stderr)
return 2
try:
allowed = parse_allowed(sys.argv[1])
except ValueError as error:
print(str(error), file=sys.stderr)
return 2
exposed = public_ports(list(sys.stdin))
unexpected = sorted(exposed - allowed)
if unexpected:
print("critical|unexpected_ports=" + ",".join(map(str, unexpected)))
else:
detail = ",".join(map(str, sorted(exposed))) or "none"
print("healthy|ports=" + detail)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import importlib.util
import pathlib
import subprocess
import sys
import unittest
module_path = pathlib.Path(__file__).with_name("public_listener_policy.py")
spec = importlib.util.spec_from_file_location("public_listener_policy", module_path)
policy = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(policy)
class PublicListenerPolicyTest(unittest.TestCase):
def test_allows_expected_public_ports_and_ignores_loopback(self) -> None:
lines = [
"LISTEN 0 4096 0.0.0.0:443 0.0.0.0:*",
"LISTEN 0 4096 [::]:80 [::]:*",
"LISTEN 0 128 *:22 *:*",
"LISTEN 0 128 127.0.0.1:5432 0.0.0.0:*",
"LISTEN 0 128 [::1]:6379 [::]:*",
]
self.assertEqual(policy.public_ports(lines), {22, 80, 443})
def test_reports_wildcard_and_interface_bound_database_ports(self) -> None:
lines = [
"LISTEN 0 4096 0.0.0.0:3306 0.0.0.0:*",
"LISTEN 0 4096 172.16.0.8:5432 0.0.0.0:*",
"LISTEN 0 4096 [::]:6379 [::]:*",
]
self.assertEqual(policy.public_ports(lines) - {22, 80, 443}, {3306, 5432, 6379})
def test_validates_allowed_port_range(self) -> None:
self.assertEqual(policy.parse_allowed("22,80,443"), {22, 80, 443})
with self.assertRaises(ValueError):
policy.parse_allowed("22,0,443")
with self.assertRaises(ValueError):
policy.parse_allowed("22, 443")
def test_cli_marks_unexpected_port_critical(self) -> None:
result = subprocess.run(
[sys.executable, str(module_path), "22,80,443"],
input="LISTEN 0 4096 0.0.0.0:5432 0.0.0.0:*\n",
text=True,
capture_output=True,
check=True,
)
self.assertEqual(result.stdout.strip(), "critical|unexpected_ports=5432")
if __name__ == "__main__":
unittest.main()
+2 -2
View File
@@ -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签发地址、11个常驻容器、本机/异地备份状态与摘要、备份执行结果、三个运行timer、短期IP证书剩余时间和磁盘阈值。异地适配安装后的实际正常路径22项全部healthy;把本机备份最大年龄临时设为0秒后仅`backup_freshness`进入critical并退出1,测试目录已清理且线上容器未停止。可选Webhook仅在状态变化时发送带时间戳和HMAC-SHA256的脱敏状态;真实接收器尚未配置,因此不作为值班通知送达证据 |
| 阿里云单机健康监测 | 目标服务器已安装五分钟systemd健康检查,结构化状态覆盖HTTPS、宿主机公网监听端口、OIDC签发地址、11个常驻容器、本机/异地备份状态与摘要、备份执行结果、三个运行timer、短期IP证书剩余时间和磁盘阈值。监听策略只允许非回环TCP 22/80/443,回环数据库端口不误报,额外端口会进入critical。实际正常路径23项全部healthy;把本机备份最大年龄临时设为0秒后仅`backup_freshness`进入critical并退出1。可选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单版本回退/重升通过 |
@@ -250,7 +250,7 @@
| 真实Agent生成、Embedding和内容安全质量门禁 | 新增`real-agent-models-capture-v1`、`real-agent-models-conformance-report-v1`和`cmd/agent-models-conformance`。内容生成复用现有固定10类Agent评测,并使用评测集、脱敏观测和报告重新计算防篡改;Embedding按版本化阈值覆盖精确/改写召回、跨长者隔离、撤销/敏感排除和无匹配降级六类;内容安全覆盖验证码、金融账户、提示注入、敏感医疗、正常内容和模型输出六类。真实证据还必须绑定受控提供器、账号/区域、数据条款、额度和审计引用;本地生成器、哈希向量和规则固定为`LOCAL_AGENT_TOOLCHAIN/external_validation_required=true`。v3证据包新增`real_agent_models_validation`,该门禁不可豁免。按低强度策略执行聚合器/证据包定向测试和一条重算现有模拟报告的本地工具链冒烟:既有10类模拟报告重新计算一致,两组六类结构完整,输出`PASS/LOCAL_AGENT_TOOLCHAIN`并继续外部待验;证据包保留真实报告、三提供器和12类别共16项缺口,临时文件已清理。 |
| 托管内容安全外部预检与告警 | 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.66.0把最新主机健康快照纳入发布候选技术验收:无上报、超过15分钟、综合预警或严重异常均阻断,只有当前有效且22项检查全部正常才通过。快照仅归档状态、时间和检查数量,不复制磁盘、证书或备份明细值。 |
| 云端主机健康发布门禁 | OpenAPI 0.66.0把最新主机健康快照纳入发布候选技术验收:无上报、超过15分钟、综合预警或严重异常均阻断,只有当前有效且23项检查全部正常才通过,其中包括宿主机公网监听端口策略。快照仅归档状态、时间和检查数量,不复制磁盘、证书、监听地址或备份明细值。 |
| 阶段8外部执行轻量预检 | 新增`scripts/stage8-preflight.ps1`,以单一只读入口核对候选Git提交、远程仓库、Compose本地基线、外部配置变量名称和十类证据索引结构/状态;报告明确不读取密钥值,也不把环境变量或本地服务可用性当作验收证据。默认仅报告缺口,`-Strict`可供自动化阻断未就绪交接;当前已建立本地`main`候选提交,本地运行基线就绪且索引结构有效,仍缺远程仓库,样例构建摘要为占位值,十类外部证据均为`PENDING`。 |
| 候选构建身份贯通 | Compose新增统一候选构建参数,后端二进制和后端/Web镜像OCI标签共享版本、完整Git提交及UTC构建时间;远程CI从OpenAPI版本和运行编号形成候选版本,构建后重新读取两个镜像标签并与`GITHUB_SHA`和构建时间逐项比对,再把候选版本、提交、时间和双镜像摘要写入构建元数据。修复了此前Dockerfile虽支持注入但Compose/CI未传参、运行版本仍为`dev/unknown`的问题。 |
+10 -2
View File
@@ -2,7 +2,7 @@
## 1. 目标与边界
`deploy/server/health-monitor.sh`为单机阿里云演示环境提供五分钟一次的轻量健康检查。它用于发现“公网入口不可用、身份签发地址漂移、容器退出、备份过期或损坏、证书接近到期、定时任务停止、磁盘空间不足”等直接影响可用性的问题。
`deploy/server/health-monitor.sh`为单机阿里云演示环境提供五分钟一次的轻量健康检查。它用于发现“公网入口不可用、宿主机出现未授权公网监听端口、身份签发地址漂移、容器退出、备份过期或损坏、证书接近到期、定时任务停止、磁盘空间不足”等直接影响可用性或暴露面的事件。
该脚本不替代Prometheus、Grafana、云监控或真实值班系统。默认只写结构化状态文件和systemd日志;配置外部接收器后,状态发生变化时才发送一次HMAC签名Webhook,避免每五分钟重复通知。
@@ -12,6 +12,7 @@
|---|---|---|
| 公网地址 | `PUBLIC_ORIGIN`为HTTPS | `invalid_https_origin` |
| HTTPS入口 | `/api/v1/version`在10秒内返回成功 | `request_failed` |
| 公网监听端口 | 宿主机非回环TCP监听端口只能属于`22,80,443`;回环地址上的数据库或监控端口不计入公网暴露 | `unexpected_ports=...`、`probe_unavailable`或`invalid_probe_result` |
| OIDC | Discovery中的`issuer`与公网地址完全一致 | `issuer_mismatch`或`request_failed` |
| 容器 | PostgreSQL、Redis、MinIO、Keycloak、API、Callbacks、Worker、Web、Gateway和两个模拟服务均为运行态;有健康检查的容器必须为healthy | `missing`、容器状态或健康状态 |
| 备份 | 最新加密归档不超过36小时且外层SHA-256有效 | `missing`、`age_seconds=...`或`checksum_failed` |
@@ -89,11 +90,12 @@ PLATFORM_HEALTH_REPORT_SECRET_FILE=/etc/data-terminal-cloud/platform-health-call
| `CERTIFICATE_MIN_SECONDS` | `129600` | 证书最小剩余时间,36小时 |
| `DISK_WARNING_PERCENT` | `80` | 磁盘警告阈值 |
| `DISK_CRITICAL_PERCENT` | `90` | 磁盘严重阈值 |
| `EXPECTED_PUBLIC_TCP_PORTS` | `22,80,443` | 宿主机允许绑定非回环地址的TCP端口,逗号分隔 |
| `MONITOR_STATE_DIR` | `/var/lib/data-terminal-cloud-monitor` | 原子状态文件目录 |
| `MONITOR_CONFIG_FILE` | `/etc/data-terminal-cloud/monitor.env` | 可选通知配置 |
| `TLS_CERTIFICATE_FILE` | 根据`PUBLIC_ORIGIN`主机名推导 | 当前活动证书文件 |
阈值只能使用非负整数,磁盘警告阈值必须小于严重阈值。普通调整无需重建镜像,但应记录变更理由并重新执行一次手工检查。
阈值只能使用非负整数,磁盘警告阈值必须小于严重阈值。端口必须位于1—65535;修改允许端口前必须同步复核阿里云安全组,不能用扩大允许列表掩盖错误的数据库或缓存端口映射。普通调整无需重建镜像,但应记录变更理由并重新执行一次手工检查。
## 7. 验收与排障
@@ -114,6 +116,12 @@ MONITOR_STATE_DIR="$test_dir" MONITOR_CONFIG_FILE=/dev/null \
期望退出1,状态为critical,唯一人工注入的严重检查为`backup_freshness`。测试目录验证后应精确删除。
监听策略解析器的正常、回环忽略和异常端口分支可独立验证:
```bash
python3 -B deploy/server/test_public_listener_policy.py
```
常用排障命令:
```bash
+3 -3
View File
@@ -101,7 +101,7 @@ export function OpsDiagnosticsWorkspace({ accessToken, accounts }: { accessToken
{canOperate?<section className="diagnostic-jobs acceptance-snapshot"><div className="section-heading"><div><p className="eyebrow">RELEASE CANDIDATE EVIDENCE</p><h3>发布候选验收快照</h3><p>一次冻结候选构建、迁移、必需依赖、联调问题、可靠任务和严重告警;输出仅是技术预检证据,不代替发布批准。</p></div></div><form className="integration-issue-form" onSubmit={(event)=>void exportAcceptanceSnapshot(event)}><label><span>候选版本</span><input required maxLength={128} pattern="[A-Za-z0-9][A-Za-z0-9._+\-]*" value={acceptanceDraft.candidate_version} onChange={(event)=>setAcceptanceDraft({...acceptanceDraft,candidate_version:event.target.value})} placeholder="如 0.55.0-rc.1" /></label><label><span>构建摘要</span><input required pattern="sha256:[A-Fa-f0-9]{64}" value={acceptanceDraft.build_digest} onChange={(event)=>setAcceptanceDraft({...acceptanceDraft,build_digest:event.target.value})} placeholder="sha256: 后跟64位十六进制" /></label><label><span>目标环境</span><select value={acceptanceDraft.environment} onChange={(event)=>setAcceptanceDraft({...acceptanceDraft,environment:event.target.value as OpsAcceptanceSnapshotRequest["environment"]})}><option value="LAB">实验室</option><option value="STAGING">预发布</option><option value="PRODUCTION">生产</option></select></label><label><span>验收类型</span><select value={acceptanceDraft.run_type} onChange={(event)=>setAcceptanceDraft({...acceptanceDraft,run_type:event.target.value as OpsAcceptanceSnapshotRequest["run_type"]})}><option value="REAL_DEVICE_PROVIDER">真实设备与供应商</option><option value="PILOT">灰度试点</option><option value="PRODUCTION_RELEASE">生产发布</option></select></label><label><span>验收范围</span><textarea required maxLength={500} value={acceptanceDraft.scope} onChange={(event)=>setAcceptanceDraft({...acceptanceDraft,scope:event.target.value})} /></label><label><span>归档原因</span><textarea required maxLength={255} value={acceptanceDraft.reason} onChange={(event)=>setAcceptanceDraft({...acceptanceDraft,reason:event.target.value})} /></label><fieldset><legend>本次必需依赖</legend>{(["oidc_jwks","ark_tls","volc_rtc_tls","content_safety_tls"] as const).map((dependency)=><label className="checkbox-label" key={dependency}><input type="checkbox" checked={acceptanceDraft.required_dependencies.includes(dependency)} onChange={()=>toggleAcceptanceDependency(dependency)} />{externalDependencyLabel(dependency)}</label>)}</fieldset><button type="submit" disabled={busy||!acceptanceDraft.candidate_version.trim()||!acceptanceDraft.build_digest.trim()||!acceptanceDraft.scope.trim()||!acceptanceDraft.reason.trim()||acceptanceDraft.required_dependencies.length===0}>{busy?"生成中…":"生成并下载快照"}</button></form>{acceptanceResult?<div className="acceptance-result"><div className="section-heading"><div><h4>最近生成:{acceptanceResult.candidate_version}</h4><p>{formatTimestamp(acceptanceResult.generated_at)} · 快照 {acceptanceResult.id}</p></div><span className={`status ${acceptanceResult.overall_gate_satisfied?"active":"danger"}`}>{acceptanceResult.overall_gate_satisfied?"技术门禁通过":"技术门禁阻断"}</span></div><div className="issue-summary-grid">{acceptanceResult.gates.map((gate)=><article className={gate.satisfied?"issue-gate-pass":"issue-gate-blocked"} key={gate.code}><span>{acceptanceGateLabel(gate.code)}</span><strong>{gate.satisfied?"通过":"阻断"}</strong><small>{gate.summary}</small></article>)}</div>{acceptanceResult.alerts.blocking_items.length?<div className="diagnostic-results"><h4>阻断告警定位</h4>{acceptanceResult.alerts.blocking_items.map((alert)=><article className="diagnostic-card" key={alert.id}><div className="diagnostic-card__head"><div><strong>{alert.rule_code}</strong><span>告警 {alert.id} · 最近命中 {formatTimestamp(alert.last_detected_at)}</span></div><span className={`status ${alert.severity==="CRITICAL"?"danger":"warning"}`}>{alert.severity}</span></div><dl><div><dt>处置状态</dt><dd>{alert.status}</dd></div><div><dt>关联类型</dt><dd>{alert.resource_type||"聚合队列"}</dd></div><div><dt>关联编号</dt><dd>{alert.resource_id||"—"}</dd></div><div><dt>资源核对</dt><dd>{acceptanceResourceStateLabel(alert.resource_state)}</dd></div></dl></article>)}</div>:<p className="empty-copy">当前没有阻断发布的HIGH或CRITICAL告警。</p>}{acceptanceResult.alerts.blocking_items_truncated?<p className="recording-notice recording-notice--error">阻断告警超过100条,下载文件仅包含最早100条;必须在告警工作台查看完整队列。</p>:null}<p className="empty-copy">{acceptanceResult.disclaimer}</p></div>:null}</section>:null}
{canOperate?<section className="diagnostic-jobs pilot-daily-snapshot"><div className="section-heading"><div><p className="eyebrow">PILOT DAILY OPERATIONS</p><h3>试点运行日报</h3><p>按业务时区自然日汇总设备、通话、供应商、上传、告警和费用;下载内容不含字幕、录音、个人资料和供应商任务编号。</p></div></div><form className="integration-issue-form" onSubmit={(event)=>void exportPilotDailySnapshot(event)}><label><span>业务日期</span><input required type="date" max={localDateInput()} value={pilotDailyDraft.local_date} onChange={(event)=>setPilotDailyDraft({...pilotDailyDraft,local_date:event.target.value})} /></label><label><span>IANA 时区</span><input required maxLength={80} value={pilotDailyDraft.timezone} onChange={(event)=>setPilotDailyDraft({...pilotDailyDraft,timezone:event.target.value})} placeholder="Asia/Shanghai" /></label><label><span>归档原因</span><textarea required maxLength={255} value={pilotDailyDraft.reason} onChange={(event)=>setPilotDailyDraft({...pilotDailyDraft,reason:event.target.value})} /></label><button type="submit" disabled={busy||!pilotDailyDraft.candidate_version.trim()||!pilotDailyDraft.build_digest.trim()||!pilotDailyDraft.local_date||!pilotDailyDraft.timezone.trim()||!pilotDailyDraft.reason.trim()}>{busy?"生成中…":"生成并下载日报"}</button></form>{pilotDailyResult?<div className="acceptance-result"><div className="section-heading"><div><h4>最近生成:{pilotDailyResult.local_date}</h4><p>{pilotDailyResult.timezone} · {formatTimestamp(pilotDailyResult.generated_at)} · 日报 {pilotDailyResult.id}</p></div><span className="status active">已脱敏归档</span></div><div className="issue-summary-grid"><article><span>可用设备</span><strong>{pilotDailyResult.devices.eligible}</strong><small>当前在线 {pilotDailyResult.devices.online}</small></article><article><span>当日通话</span><strong>{pilotDailyResult.conversations.total}</strong><small>失败 {pilotStatusCount(pilotDailyResult,"FAILED")}</small></article><article><span>上传积压</span><strong>{pilotDailyResult.uploads.open_batches}</strong><small>待处理分段 {pilotDailyResult.uploads.pending_segments}</small></article><article className={pilotDailyResult.alerts.high_open+pilotDailyResult.alerts.critical_open?"issue-gate-blocked":"issue-gate-pass"}><span>高严重度告警</span><strong>{pilotDailyResult.alerts.high_open+pilotDailyResult.alerts.critical_open}</strong><small>全部未关闭 {pilotDailyResult.alerts.open}</small></article></div><div className="diagnostic-results"><article className="diagnostic-card"><div className="diagnostic-card__head"><div><strong>供应商运行</strong><span>{pilotDailyResult.providers.length} 个供应商有运行记录</span></div></div><p>{pilotDailyResult.providers.length?pilotDailyResult.providers.map((item)=>`${item.provider}:${item.current_status},失败通知${item.failed_events_during_day},失败通话${item.failed_calls_during_day},积压${item.notification_backlog}`).join(";"):"所选日期和当前状态中没有供应商记录。"}</p></article><article className="diagnostic-card"><div className="diagnostic-card__head"><div><strong>当日费用</strong><span>按币种分列,不做汇率换算</span></div></div><p>{pilotDailyResult.costs.length?pilotDailyResult.costs.map((item)=>`${item.currency} ${(item.cost_micros/1_000_000).toFixed(6)}(${item.line_count}条)`).join(";"):"所选自然日没有费用明细。"}</p></article></div><p className="empty-copy">{pilotDailyResult.disclaimer}</p></div>:null}</section>:null}
{canOperate?<section className="diagnostic-jobs pilot-weekly-snapshot"><div className="section-heading"><div><p className="eyebrow">PILOT WEEKLY REVIEW</p><h3>试点运行周报</h3><p>按周一开始的业务自然周汇总内容审核、记忆、隐私请求、固件和配置差异;投诉仍需附客服或CRM权威证据。</p></div></div><form className="integration-issue-form" onSubmit={(event)=>void exportPilotWeeklySnapshot(event)}><label><span>周开始日期(周一)</span><input required type="date" max={localDateInput()} value={pilotWeeklyDraft.week_start} onChange={(event)=>setPilotWeeklyDraft({...pilotWeeklyDraft,week_start:event.target.value})} /></label><label><span>IANA 时区</span><input required maxLength={80} value={pilotWeeklyDraft.timezone} onChange={(event)=>setPilotWeeklyDraft({...pilotWeeklyDraft,timezone:event.target.value})} placeholder="Asia/Shanghai" /></label><label><span>归档原因</span><textarea required maxLength={255} value={pilotWeeklyDraft.reason} onChange={(event)=>setPilotWeeklyDraft({...pilotWeeklyDraft,reason:event.target.value})} /></label><button type="submit" disabled={busy||!pilotWeeklyDraft.candidate_version.trim()||!pilotWeeklyDraft.build_digest.trim()||!pilotWeeklyDraft.week_start||!pilotWeeklyDraft.timezone.trim()||!pilotWeeklyDraft.reason.trim()}>{busy?"生成中…":"生成并下载周报"}</button></form>{pilotWeeklyResult?<div className="acceptance-result"><div className="section-heading"><div><h4>最近生成:{pilotWeeklyResult.week_start} 起始周</h4><p>{pilotWeeklyResult.timezone} · {formatTimestamp(pilotWeeklyResult.generated_at)} · 周报 {pilotWeeklyResult.id}</p></div><span className={`status ${pilotWeeklyResult.is_complete?"active":"warning"}`}>{pilotWeeklyResult.is_complete?"完整自然周":"周中快照"}</span></div><div className="issue-summary-grid"><article><span>人工修订率</span><strong>{basisPointsLabel(pilotWeeklyResult.content_review.manual_revision_rate_available,pilotWeeklyResult.content_review.manual_revision_rate_basis_points)}</strong><small>{pilotWeeklyResult.content_review.stories_manually_revised}/{pilotWeeklyResult.content_review.stories_review_touched} 个审核故事</small></article><article><span>故事确认率</span><strong>{basisPointsLabel(pilotWeeklyResult.content_review.story_confirmation_rate_available,pilotWeeklyResult.content_review.story_confirmation_rate_basis_points)}</strong><small>确认 {pilotWeeklyResult.content_review.stories_confirmed} · 拒绝 {pilotWeeklyResult.content_review.stories_rejected}</small></article><article><span>隐私请求</span><strong>{pilotWeeklyResult.privacy.requests_created_during_week}</strong><small>当前未完成 {pilotWeeklyResult.privacy.current_open}</small></article><article className={pilotWeeklyResult.config.drifted+pilotWeeklyResult.config.unreported?"issue-gate-blocked":"issue-gate-pass"}><span>配置差异</span><strong>{pilotWeeklyResult.config.drifted+pilotWeeklyResult.config.unreported}</strong><small>一致 {pilotWeeklyResult.config.aligned}/{pilotWeeklyResult.config.eligible_devices}</small></article></div><div className="diagnostic-results"><article className="diagnostic-card"><div className="diagnostic-card__head"><div><strong>记忆确认与索引</strong><span>首版 {pilotWeeklyResult.content_review.memories_created} · 修订 {pilotWeeklyResult.content_review.memory_revisions}</span></div></div><p>当前索引待处理 {pilotWeeklyResult.content_review.current_memory_index_pending},失败 {pilotWeeklyResult.content_review.current_memory_index_failed}。</p></article><article className="diagnostic-card"><div className="diagnostic-card__head"><div><strong>固件分布</strong><span>{pilotWeeklyResult.firmware_versions.reduce((sum,item)=>sum+item.device_count,0)} 台可用设备</span></div></div><p>{pilotWeeklyResult.firmware_versions.length?pilotWeeklyResult.firmware_versions.map((item)=>`${item.model}/${item.firmware_version}:${item.device_count}台`).join(";"):"当前没有可用且已绑定设备。"}</p></article></div>{pilotWeeklyResult.external_evidence.map((item)=><p className="recording-notice recording-notice--error" key={item.code}><strong>外部证据仍需补充:投诉</strong><br />{item.reason}</p>)}<p className="empty-copy">{pilotWeeklyResult.disclaimer}</p></div>:null}</section>:null}
<section className="diagnostic-jobs platform-health"><div className="section-heading"><div><p className="eyebrow">SERVER HEALTH</p><h3>云端主机运行状态</h3><p>主机每五分钟签名上报公网入口、容器、备份、证书、磁盘和定时任务的脱敏结果。</p></div><button type="button" className="button-secondary" disabled={busy} onClick={()=>void loadPlatformHealth()}>刷新状态</button></div>{platformHealth?(()=>{const state=platformHealthState(platformHealth);const critical=platformHealth.checks.filter((item)=>item.status==="critical").length;const warning=platformHealth.checks.filter((item)=>item.status==="warning").length;return <><div className="issue-summary-grid"><article className={state.tone==="active"?"issue-gate-pass":"issue-gate-blocked"}><span>综合状态</span><strong>{state.label}</strong><small>上报 {formatTimestamp(platformHealth.observed_at)}</small></article><article><span>检查项</span><strong>{platformHealth.checks.length}</strong><small>全部脱敏</small></article><article className={critical?"issue-gate-blocked":"issue-gate-pass"}><span>严重异常</span><strong>{critical}</strong><small>需要立即处置</small></article><article className={warning?"issue-gate-blocked":"issue-gate-pass"}><span>预警</span><strong>{warning}</strong><small>需要持续观察</small></article></div><div className="external-dependency-grid">{platformHealth.checks.map((item)=><article className="diagnostic-card" key={item.name}><div className="diagnostic-card__head"><div><strong>{platformHealthCheckLabel(item.name)}</strong><span>{item.name}</span></div><span className={`status ${item.status==="healthy"?"active":item.status==="warning"?"warning":"danger"}`}>{platformHealthCheckStatusLabel(item.status)}</span></div><p>{platformHealthValueLabel(item.name,item.value)}</p></article>)}</div>{state.stale?<p className="recording-notice recording-notice--error">最近上报已超过15分钟,不能继续视为当前状态;请检查健康监测定时器、回调进程和签名配置。</p>:null}</>})():<p className="empty-copy">尚未收到主机健康快照;完成监测回调配置后,首个五分钟周期会自动出现。</p>}</section>
<section className="diagnostic-jobs platform-health"><div className="section-heading"><div><p className="eyebrow">SERVER HEALTH</p><h3>云端主机运行状态</h3><p>主机每五分钟签名上报公网入口、监听端口、容器、备份、证书、磁盘和定时任务的脱敏结果。</p></div><button type="button" className="button-secondary" disabled={busy} onClick={()=>void loadPlatformHealth()}>刷新状态</button></div>{platformHealth?(()=>{const state=platformHealthState(platformHealth);const critical=platformHealth.checks.filter((item)=>item.status==="critical").length;const warning=platformHealth.checks.filter((item)=>item.status==="warning").length;return <><div className="issue-summary-grid"><article className={state.tone==="active"?"issue-gate-pass":"issue-gate-blocked"}><span>综合状态</span><strong>{state.label}</strong><small>上报 {formatTimestamp(platformHealth.observed_at)}</small></article><article><span>检查项</span><strong>{platformHealth.checks.length}</strong><small>全部脱敏</small></article><article className={critical?"issue-gate-blocked":"issue-gate-pass"}><span>严重异常</span><strong>{critical}</strong><small>需要立即处置</small></article><article className={warning?"issue-gate-blocked":"issue-gate-pass"}><span>预警</span><strong>{warning}</strong><small>需要持续观察</small></article></div><div className="external-dependency-grid">{platformHealth.checks.map((item)=><article className="diagnostic-card" key={item.name}><div className="diagnostic-card__head"><div><strong>{platformHealthCheckLabel(item.name)}</strong><span>{item.name}</span></div><span className={`status ${item.status==="healthy"?"active":item.status==="warning"?"warning":"danger"}`}>{platformHealthCheckStatusLabel(item.status)}</span></div><p>{platformHealthValueLabel(item.name,item.value)}</p></article>)}</div>{state.stale?<p className="recording-notice recording-notice--error">最近上报已超过15分钟,不能继续视为当前状态;请检查健康监测定时器、回调进程和签名配置。</p>:null}</>})():<p className="empty-copy">尚未收到主机健康快照;完成监测回调配置后,首个五分钟周期会自动出现。</p>}</section>
<section className="diagnostic-jobs external-dependencies"><div className="section-heading"><div><p className="eyebrow">EXTERNAL PREFLIGHT</p><h3>外部依赖预检</h3><p>API与Worker周期执行不消耗模型或语音额度的连通性检查,仅保存脱敏结果。</p></div><button type="button" className="button-secondary" disabled={busy} onClick={()=>void loadExternalDependencies()}>刷新状态</button></div>{externalDependencies.length?<div className="external-dependency-grid">{externalDependencies.map((item)=>{const state=externalDependencyState(item);return <article className="diagnostic-card" key={`${item.service_name}-${item.dependency}`}><div className="diagnostic-card__head"><div><strong>{externalDependencyLabel(item.dependency)}</strong><span>{serviceLabel(item.service_name)} · 最近检查 {formatTimestamp(item.checked_at)}</span></div><span className={`status ${state.tone}`}>{state.label}</span></div><dl><div><dt>探针耗时</dt><dd>{item.latency_ms}毫秒</dd></div><div><dt>连续失败</dt><dd>{item.consecutive_failures}次</dd></div><div><dt>最近成功</dt><dd>{formatTimestamp(item.last_success_at)}</dd></div><div><dt>最近失败</dt><dd>{formatTimestamp(item.last_failure_at)}</dd></div></dl>{state.stale?<p>该进程已超过三倍探针周期未更新,不能视为当前可用。</p>:item.last_error_code?<p>归一化错误:<strong>{item.last_error_code}</strong></p>:null}</article>})}</div>:<p className="empty-copy">当前进程尚未写入外部探针结果;本地未启用的方舟或RTC依赖不会伪装为已验收。</p>}</section>
<form className="diagnostic-search" onSubmit={(event)=>void search(event)}><label>检索字段<select value={field} onChange={(event)=>setField(event.target.value as typeof field)}>{fields.map(([key,label])=><option value={key} key={key}>{label}</option>)}</select></label><label>精确编号<input value={value} onChange={(event)=>setValue(event.target.value)} placeholder="输入完整编号" /></label><button type="submit" disabled={loading||!value.trim()}>{loading?"查询中…":"查询"}</button>{canOperate?<button type="button" className="button-secondary" disabled={busy||!value.trim()} onClick={openBundleExport}>导出脱敏诊断包</button>:null}</form>
{diagnosticBundleDraft?<form className="sensitive-access-request controlled-operation" aria-label="脱敏诊断包导出" onSubmit={(event)=>void exportBundle(event)}><div className="sensitive-access-request__head"><div><p className="eyebrow">REDACTED DIAGNOSTIC EXPORT</p><h3>导出脱敏诊断包</h3></div><span className="status warning">导出行为写入审计</span></div><p>本次导出固定使用“<strong>{diagnosticFieldLabel(diagnosticBundleDraft.field)}</strong> = <strong>{diagnosticBundleDraft.value}</strong>”的精确检索条件。文件只包含当前脱敏诊断快照,不包含字幕、录音地址、短期凭证或密钥正文。</p><label><span>导出原因</span><textarea autoFocus required maxLength={500} value={diagnosticBundleDraft.reason} onChange={(event)=>setDiagnosticBundleDraft({...diagnosticBundleDraft,reason:event.target.value})} placeholder="说明接收方、联调问题和使用范围;不要粘贴敏感正文" /></label><div className="sensitive-access-request__actions"><button type="button" className="button-secondary" disabled={busy} onClick={()=>setDiagnosticBundleDraft(undefined)}>取消</button><button type="submit" disabled={busy||!diagnosticBundleDraft.reason.trim()}>{busy?"正在生成…":"生成并下载"}</button></div></form>:null}
@@ -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<string,string>={public_origin:"公网地址",public_https:"HTTPS入口",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("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<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 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 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 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]}