]> acesimba.cloud Git - StationAIDemo.git/commitdiff
重新初始化仓库:推送当前最新代码
authorwang.yu(王瑜) <wang.yu@advantech.com>
Mon, 24 Aug 2026 02:23:33 +0000 (10:23 +0800)
committerwang.yu(王瑜) <wang.yu@advantech.com>
Mon, 24 Aug 2026 02:23:33 +0000 (10:23 +0800)
以当前工作树为准重建仓库历史(原分叉历史已废弃)。包含:
- 前端:12 场景框架、侧栏折叠/展开、Header 固定、移动端抽屉菜单与遮罩、index.css 清理、vite-env.d.ts、页脚 ICP 备案号
- 后端:5 个接口(含 /api/mu-period-compare 运行环比)
- 备案信息.md 与 README

17 files changed:
.gitignore [new file with mode: 0644]
README.md [new file with mode: 0644]
backend/.env.example [new file with mode: 0644]
backend/main.py [new file with mode: 0644]
backend/requirements.txt [new file with mode: 0644]
frontend/index.html [new file with mode: 0644]
frontend/package-lock.json [new file with mode: 0644]
frontend/package.json [new file with mode: 0644]
frontend/src/App.tsx [new file with mode: 0644]
frontend/src/api.ts [new file with mode: 0644]
frontend/src/index.css [new file with mode: 0644]
frontend/src/main.tsx [new file with mode: 0644]
frontend/src/vite-env.d.ts [new file with mode: 0644]
frontend/tsconfig.json [new file with mode: 0644]
frontend/tsconfig.node.json [new file with mode: 0644]
frontend/vite.config.ts [new file with mode: 0644]
备案信息.md [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..f86a9fe
--- /dev/null
@@ -0,0 +1,32 @@
+# 依赖
+node_modules/
+frontend/node_modules/
+.pnpm-store/
+backend/.venv/
+backend/venv/
+__pycache__/
+*.pyc
+
+# 构建产物
+frontend/dist/
+*.tsbuildinfo
+# tsc -b 为 vite.config 生成的临时产物(composite 构建副产物)
+frontend/vite.config.js
+frontend/vite.config.d.ts
+
+# 环境 / 密钥
+.env
+backend/.env
+*.local
+
+# 编辑器 / 系统
+.vscode/
+.idea/
+.DS_Store
+
+# 设计源(仅作参考,不部署;如需纳入版本控制可删除此行)
+design-source/
+Station-AI-Copilot-Design-Source.zip
+
+# 损坏仓库备份(勿提交)
+.git.broken/
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..7d6a277
--- /dev/null
+++ b/README.md
@@ -0,0 +1,81 @@
+# StationAIDemo
+
+在 **wise-insightapm.com**(`desk-desk-cluster01.cn.wise-insightapm.com`)站点之上构建的
+**AI 分析 Demo**。硬性约束:**必须使用该站点的真实 API 与真实数据**,禁止凭空造或使用无关数据源。
+
+由设计源 `Station-AI-Copilot`(WISE-IoT 风格)移植而来,部署形态为 **Vite + React 前端 +
+FastAPI 后端(BFF)**,贴合既有 1061/2341 技术栈。AI 分析能力使用 **CodeBuddy 模型(默认 hy3)**。
+
+---
+
+## 架构
+
+```
+浏览器
+   └─ /api/*  →  FastAPI 后端(BFF)
+                    ├─ /api/ai/analyze   调用 CodeBuddy(hy3) 对真实数据分析(未配密钥时本地规则兜底)
+                    ├─ /api/mu-connect   转发 MU 认证并探测真实 APM 端点(cURL/EIToken/Bearer)
+                    └─ /api/mu-period-compare 按资产组计算本月与上月同期真实运行环比
+```
+
+后端代理的真实端点(见 `backend/main.py`):
+- `/api/v1/asset/group/list`
+- `/api/v1/asset/node/root`
+- `/api/v1/event/latest`
+- `/api/v1/event/hist`(运行环比:本月截至当前 vs 上月同期)
+
+目标环境中的实际服务根地址为 `https://portal-apm-<环境域名>/api-apm`,不能把上述路径直接拼接到 Desk 门户地址。
+
+## 目录
+
+```
+StationAIDemo/
+├── frontend/      # Vite + React + TypeScript(UI 与真实数据接入)
+├── backend/       # FastAPI(AI 分析 + MU 代理 BFF)
+├── design-source/ # 设计源(参考,不部署)
+└── README.md
+```
+
+## 本地开发
+
+> 注意:`localhost` 无法直接复用 MU 门户 Cookie。开发环境请在 MU 的 Network 面板复制一条真实
+> `portal-apm-.../api-apm/api/v1/...` 请求为 cURL,粘贴到“MU 数据源配置”;也可以使用
+> EIToken/Bearer 和对应的 `org-id`。凭据只保存在页面内存中。
+
+### 1. 后端(FastAPI,端口 8000)
+```bash
+cd backend
+python -m venv .venv && source .venv/Scripts/activate   # Windows: .venv\Scripts\activate
+pip install -r requirements.txt
+cp .env.example .env      # 填入 CodeBuddy 接入地址与密钥(可选,缺省走本地兜底)
+uvicorn main:app --reload --port 8000
+```
+
+### 2. 前端(Vite,端口 5173)
+```bash
+cd frontend
+npm install
+npm run dev
+```
+访问 http://localhost:5173 。前端 `/api` 已代理到后端 8000。
+
+## 生产部署(云端 Linux + 同域微应用)
+
+1. 构建前端静态产物:`cd frontend && npm install && npm run build`(产物在 `frontend/dist`)。
+2. 用 nginx 等将前端静态文件与后端 `/api` 暴露到统一入口;后端负责访问实际的 APM 服务。
+3. 后端以 `uvicorn main:app --port 8000` 运行(systemd 托管),并由同域反向代理转发 `/api`。
+4. 配置 `.env` 中的 CodeBuddy 接入信息后重启后端。
+
+## AI 分析接线的现状
+
+- **设备问答(QA)** 与 **首页 AI 实时分析** 已接入 `/api/ai/analyze`,把真实设备/告警数据
+  送入 CodeBuddy(hy3) 生成结论;未配置密钥时回退到本地规则分析。
+- 日报 / 诊断 / 健康 / 预测 / 控制等模块的“AI 结论”目前为设计源示例文本,后续按你的
+  **规格** 逐模块替换为真·AI 分析(接线点统一为 `frontend/src/api.ts` 的 `analyzeModule`)。
+
+## 待办(按你的规格推进)
+
+- [ ] 各分析模块的 AI 接线与提示词打磨
+- [ ] CodeBuddy 真实接入地址 / 密钥确认
+- [ ] 云端同域微应用的反代与路径规划
+- [ ] 各模块 UI 在真实数据下的细节校准
diff --git a/backend/.env.example b/backend/.env.example
new file mode 100644 (file)
index 0000000..1ded057
--- /dev/null
@@ -0,0 +1,6 @@
+# CodeBuddy(AI 分析的“脑”)接入配置
+# 由你提供真实接入地址与密钥;未配置时后端自动回退到本地规则分析。
+# 默认模型:hy3(后续按需要切换)
+CODEBUDDY_API_BASE=
+CODEBUDDY_API_KEY=
+CODEBUDDY_MODEL=hy3
diff --git a/backend/main.py b/backend/main.py
new file mode 100644 (file)
index 0000000..506f5ba
--- /dev/null
@@ -0,0 +1,984 @@
+import os
+import re
+import json
+import base64
+import asyncio
+from datetime import datetime, timedelta
+from typing import Any, Optional
+from urllib.parse import unquote, urlparse, urlunparse
+
+from dotenv import load_dotenv
+import httpx
+from fastapi import FastAPI, Request
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+
+load_dotenv()
+
+app = FastAPI(title="Station AI Copilot Demo Backend")
+
+# 开发期允许前端(Vite 5173)跨域调用;生产期建议同域部署,可收紧 allow_origins
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+# ---- CodeBuddy(AI 脑)配置 ----
+CODEBUDDY_API_BASE = os.getenv("CODEBUDDY_API_BASE", "")
+CODEBUDDY_API_KEY = os.getenv("CODEBUDDY_API_KEY", "")
+CODEBUDDY_MODEL = os.getenv("CODEBUDDY_MODEL", "hy3")
+
+# 真实站点 API 契约。设备监控微应用将 APM 请求发送至:
+# https://portal-apm-<环境域名>/api-apm/api/v1/...
+PROBE_ENDPOINTS = [
+    "/api/v1/asset/group/list?count=1000",
+    "/api/v1/asset/node/root",
+    "/api/v1/event/latest",
+]
+
+
+# ---- 请求模型 ----
+class MuConfig(BaseModel):
+    baseUrl: str
+    apiBaseUrl: str = ""
+    authMode: str = "session"
+    username: str = ""
+    tenant: str = ""
+    orgCode: str = ""
+    orgId: str = ""
+    tenantId: str = ""
+    rawCurl: str = ""
+    loginPath: str = "/api/v1/auth/login"
+    usernameField: str = "username"
+    passwordField: str = "password"
+
+
+class MuConnectRequest(BaseModel):
+    config: MuConfig
+    secret: str = ""
+    selectedGroupId: str = ""
+    selectedGroupName: str = ""
+
+
+class MuPeriodCompareRequest(MuConnectRequest):
+    comparisonMode: str = "month"
+
+
+class AnalyzeRequest(BaseModel):
+    module: str = "summary"
+    prompt: str = ""
+    context: dict = {}
+
+
+def extract_payload_rows(value: Any) -> list[dict]:
+    """兼容 MU 各微服务常见包装,并递归寻找真正的行数组。"""
+    if isinstance(value, list):
+        return [item for item in value if isinstance(item, dict)]
+    if not isinstance(value, dict):
+        return []
+    for key in ("e", "data", "result", "list", "rows", "records", "items", "nodes", "nodeList", "content", "elements", "children"):
+        if key not in value:
+            continue
+        rows = extract_payload_rows(value.get(key))
+        if rows:
+            return rows
+    for nested in value.values():
+        rows = extract_payload_rows(nested)
+        if rows:
+            return rows
+    return []
+
+
+# ---- 工具 ----
+def validate_base_url(raw: str) -> str:
+    try:
+        url = urlparse(raw)
+    except Exception:
+        raise ValueError("MU 站点地址格式不正确")
+    if url.scheme != "https":
+        raise ValueError("MU 站点必须使用 HTTPS")
+    if url.username or url.password:
+        raise ValueError("请不要把账号或密码写在 URL 中")
+    host = url.hostname or ""
+    if re.match(r"^\d{1,3}(\.\d{1,3}){3}$", host) or host == "localhost":
+        raise ValueError("为防止内部网络探测,请使用 MU 站点域名,不要填写 IP 地址")
+    return raw.rstrip("/")
+
+
+def extract_curl_url(text: str) -> str:
+    """提取 Copy as cURL 中的首个 HTTPS URL。"""
+    match = re.search(r"https://[^\s'\"`\]\)]+", text or "", re.I)
+    if not match:
+        return ""
+    return match.group(0).rstrip("\\^").replace("\\&", "&").replace("\\_", "_")
+
+
+def resolve_apm_base(config: MuConfig) -> str:
+    """从 Desk 门户地址自动推导设备监控实际使用的 APM API 根地址。"""
+    explicit = (config.apiBaseUrl or "").strip()
+    if explicit:
+        return validate_base_url(explicit)
+
+    curl_url = extract_curl_url(config.rawCurl)
+    source = curl_url or config.baseUrl
+    validated = validate_base_url(source)
+    parsed = urlparse(validated)
+    host = parsed.hostname or ""
+
+    if host.startswith("desk-"):
+        host = "portal-apm-" + host[len("desk-") :]
+    elif host.startswith("portal-m2i-"):
+        host = "portal-apm-" + host[len("portal-m2i-") :]
+
+    netloc = host
+    if parsed.port:
+        netloc += f":{parsed.port}"
+    path = parsed.path or ""
+    marker = "/api-apm"
+    if marker in path:
+        path = path[: path.index(marker) + len(marker)]
+    elif host.startswith("portal-apm-"):
+        path = marker
+    else:
+        path = ""
+    return urlunparse((parsed.scheme, netloc, path.rstrip("/"), "", "", ""))
+
+
+def find_token(value: Any) -> Optional[str]:
+    if not isinstance(value, dict):
+        return None
+    for k in ["access_token", "accessToken", "token", "jwt"]:
+        if isinstance(value.get(k), str):
+            return value[k]
+    for k in ["data", "result"]:
+        t = find_token(value.get(k))
+        if t:
+            return t
+    return None
+
+
+def sanitize_cookie_header(raw_cookie: str) -> str:
+    """只保留 APM 认证需要且可安全作为 HTTP 头发送的 Cookie。"""
+    allowed = {
+        "eitoken",
+        "einame",
+        "tenantid",
+        "orgid",
+        "state",
+        "nonce",
+        "lang",
+        "langclass",
+        "username",
+        "themecolor",
+    }
+    result = []
+    for match in re.finditer(r"(?:^|;\s*)([A-Za-z0-9_-]+)=([^;]*)", raw_cookie or ""):
+        name, value = match.group(1), match.group(2).strip()
+        if name.lower() not in allowed:
+            continue
+        try:
+            value.encode("ascii")
+        except UnicodeEncodeError:
+            continue
+        result.append(f"{name}={value}")
+    return "; ".join(result)
+
+
+def sanitize_forward_headers(headers: dict) -> dict:
+    """Drop browser-only/non-ASCII headers that httpx cannot safely encode."""
+    safe: dict = {}
+    for raw_key, raw_value in headers.items():
+        key, value = str(raw_key).strip(), str(raw_value).strip()
+        if not key or not value:
+            continue
+        try:
+            key.encode("ascii")
+            value.encode("ascii")
+        except UnicodeEncodeError:
+            continue
+        safe[key] = value
+    return safe
+
+
+def parse_curl_headers(text: str) -> dict:
+    """从 curl 命令或原始请求头文本中提取 Key: Value 头,用于原样复刻浏览器请求。"""
+    parsed: dict = {}
+    if not text:
+        return parsed
+    # 1) 尝试解析 curl 的 -H '...' / -H "..." 片段
+    for m in re.finditer(r"-H\s+['\"]([^'\"]+)['\"]", text):
+        kv = m.group(1)
+        if ":" in kv:
+            k, v = kv.split(":", 1)
+            parsed[k.strip()] = v.strip()
+    # 2) 若没有 -H,则按"每行 Key: Value"解析(浏览器复制的原始头)
+    if not parsed:
+        for line in text.splitlines():
+            if ":" in line and not line.strip().startswith(("http", "curl", "{", "}")):
+                k, v = line.split(":", 1)
+                parsed[k.strip()] = v.strip()
+
+    # Chrome 的 Copy as cURL 常把 Cookie 输出为 -b $'...',而不是 -H 'Cookie: ...'。
+    cookie_match = re.search(
+        r"(?:^|\s)(?:-b|--cookie)\s+\$?(['\"])(.*?)\1",
+        text,
+        re.I | re.S,
+    )
+    if cookie_match and not any(key.lower() == "cookie" for key in parsed):
+        parsed["Cookie"] = cookie_match.group(2).strip()
+
+    for key in list(parsed):
+        if key.lower() == "cookie":
+            parsed[key] = sanitize_cookie_header(parsed[key])
+
+    # 只转发认证和租户上下文相关的头,避免 Host、Content-Length 等浏览器头干扰 BFF 请求。
+    allowed = {
+        "accept",
+        "accept-language",
+        "authorization",
+        "cookie",
+        "org-id",
+        "tenant-id",
+        "x-grafana-org-id",
+        "x-requested-with",
+        "x-xsrf-token",
+    }
+    return sanitize_forward_headers(
+        {key: value for key, value in parsed.items() if key.lower() in allowed}
+    )
+
+
+def normalize_authorization(value: str) -> str:
+    token = unquote((value or "").strip())
+    if token.lower().startswith(("bearer ", "basic ")):
+        return token
+    return f"Bearer {token}" if token else ""
+
+
+def cookie_and_authorization(raw_secret: str) -> tuple[str, str]:
+    """兼容整段 Cookie、Cookie: 前缀、EIToken=xxx 和裸 Token。"""
+    raw = (raw_secret or "").strip()
+    if raw.lower().startswith("cookie:"):
+        raw = raw.split(":", 1)[1].strip()
+    if ";" in raw or re.search(r"(?:^|;\s*)EIToken=", raw, re.I):
+        match = re.search(r"(?:^|;\s*)EIToken=([^;]+)", raw, re.I)
+        token = match.group(1).strip() if match else ""
+        return sanitize_cookie_header(raw), normalize_authorization(token)
+    token = raw[len("EIToken=") :].strip() if raw.startswith("EIToken=") else raw
+    authorization = normalize_authorization(token)
+    cookie_token = unquote(token)
+    return f"EIToken={cookie_token}", authorization
+
+
+def build_mu_request_headers(config: MuConfig, secret: str) -> tuple[dict, str]:
+    """为只读分析接口复用 MU 的认证和组织上下文。"""
+    headers = {"Accept": "application/json"}
+    if config.orgId:
+        headers["org-id"] = config.orgId
+    if config.tenantId:
+        headers["Tenant-Id"] = config.tenantId
+
+    raw_mode = bool(config.rawCurl and config.rawCurl.strip())
+    if raw_mode:
+        headers.update(parse_curl_headers(config.rawCurl))
+    elif config.authMode == "eitoken":
+        cookie, authorization = cookie_and_authorization(secret)
+        headers["Cookie"] = cookie
+        if authorization:
+            headers["Authorization"] = authorization
+    elif config.authMode == "bearer":
+        headers["Authorization"] = normalize_authorization(secret)
+    elif config.authMode == "basic":
+        token = base64.b64encode(f"{config.username}:{secret}".encode()).decode()
+        headers["Authorization"] = f"Basic {token}"
+    elif config.authMode == "session":
+        return {}, "localhost 无法复用 MU 浏览器登录态,请使用原始请求、EIToken 或 Bearer"
+    else:
+        return {}, "运行环比暂不支持登录接口模式,请使用原始请求、EIToken 或 Bearer"
+
+    headers = sanitize_forward_headers(headers)
+    if not any(key.lower() in ("cookie", "authorization") for key in headers):
+        return {}, "未取得可用的 Cookie 或 Authorization"
+    return headers, ""
+
+
+def pick_value(row: dict, *keys: str) -> Any:
+    for key in keys:
+        value = row.get(key)
+        if value is not None and value != "":
+            return value
+    return None
+
+
+def timestamp_ms(value: Any) -> Optional[int]:
+    if isinstance(value, (int, float)):
+        number = int(value)
+        return number * 1000 if number < 10_000_000_000 else number
+    if isinstance(value, str) and value.strip():
+        text = value.strip()
+        if text.isdigit():
+            return timestamp_ms(int(text))
+        try:
+            return int(datetime.fromisoformat(text.replace("Z", "+00:00")).timestamp() * 1000)
+        except ValueError:
+            return None
+    return None
+
+
+def month_window() -> tuple[datetime, datetime, datetime, datetime]:
+    """返回本月截至当前与上月同期窗口,避免完整上月和半个月本月直接比较。"""
+    now = datetime.now().astimezone()
+    current_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
+    previous_month_end = current_start
+    previous_start = (current_start - timedelta(days=1)).replace(day=1)
+    elapsed = now - current_start
+    previous_end = min(previous_start + elapsed, previous_month_end)
+    return current_start, now, previous_start, previous_end
+
+
+def event_summary(rows: list[dict], start: datetime, end: datetime, node_count: int) -> dict:
+    duration_ms = 0
+    high_count = 0
+    affected: set[str] = set()
+    for row in rows:
+        node_key = str(pick_value(row, "nodeId", "assetId", "deviceId", "nodeName", "deviceName") or "")
+        if node_key:
+            affected.add(node_key)
+        level = str(pick_value(row, "level", "severity", "priority", "alarmLevel") or "")
+        if re.search(r"P?1|critical|high|严重|紧急", level, re.I):
+            high_count += 1
+        begin = timestamp_ms(pick_value(row, "startTs", "createTs", "occurTs", "eventTs", "ts"))
+        finish = timestamp_ms(pick_value(row, "endTs", "recoverTs", "closeTs", "finishTs", "updateTs"))
+        if begin:
+            capped_start = max(begin, int(start.timestamp() * 1000))
+            capped_end = min(finish or int(end.timestamp() * 1000), int(end.timestamp() * 1000))
+            duration_ms += max(0, capped_end - capped_start)
+    hours = round(duration_ms / 3_600_000, 1)
+    window_hours = max((end - start).total_seconds() / 3600, 0.01)
+    availability = None
+    if node_count:
+        availability = round(max(0.0, 1 - hours / (node_count * window_hours)) * 100, 2)
+    return {
+        "eventCount": len(rows),
+        "affectedNodes": len(affected),
+        "highPriority": high_count,
+        "impactHours": hours,
+        "availabilityEstimate": availability,
+        "windowHours": round(window_hours, 1),
+    }
+
+
+def compare_delta(current: Optional[float], previous: Optional[float]) -> Optional[float]:
+    if current is None or previous in (None, 0):
+        return None
+    return round((current - previous) / abs(previous) * 100, 1)
+
+
+SYSTEM_PROMPT = (
+    "你是 Station AI Copilot,一个面向燃气站、水站、污水处理厂、新能源场站与 AIDC 的场站运营分析助手。"
+    "请从当前场站或资产组角度回答,设备、告警与测点只作为场站结论的证据。用简体中文给出简洁、可落地、可追溯的分析结论。"
+    "不要编造数据中不存在的事实。"
+)
+
+
+async def call_codebuddy(messages: list) -> Optional[str]:
+    """调用 CodeBuddy 模型(默认 hy3)。未配置时返回 None,由调用方回退。"""
+    if not CODEBUDDY_API_BASE or not CODEBUDDY_API_KEY:
+        return None
+    url = CODEBUDDY_API_BASE.rstrip("/") + "/chat/completions"
+    headers = {
+        "Authorization": f"Bearer {CODEBUDDY_API_KEY}",
+        "Content-Type": "application/json",
+    }
+    payload = {"model": CODEBUDDY_MODEL, "messages": messages, "temperature": 0.3}
+    async with httpx.AsyncClient(timeout=30) as client:
+        r = await client.post(url, headers=headers, json=payload)
+        r.raise_for_status()
+        data = r.json()
+        return data["choices"][0]["message"]["content"]
+
+
+def local_analysis(module: str, prompt: str, context: dict) -> str:
+    """未配置 CodeBuddy 密钥时的本地规则兜底分析(仍基于真实数据)。"""
+    devices = context.get("devices") or []
+    if module == "station-qa":
+        stats = context.get("statistics") or {}
+        group = context.get("assetGroup") or {}
+        group_name = str(group.get("name") or "当前资产组")
+        station_name = str(group.get("station") or "当前站点")
+        rows = [item for item in devices if isinstance(item, dict)]
+
+        def names_for(statuses: tuple[str, ...]) -> list[str]:
+            return [
+                f"{item.get('name', '未命名设备')}({item.get('site', station_name)})"
+                for item in rows if str(item.get("status", "")) in statuses
+            ]
+
+        offline_items = names_for(("离线", "offline", "Offline"))
+        fault_items = names_for(("故障", "fault", "Fault"))
+        warning_items = names_for(("告警", "预警", "alarm", "warning"))
+        running_items = names_for(("运行", "正常", "online", "normal", "running"))
+        total = int(stats.get("total") or len(rows))
+        unknown = int(stats.get("unknown") or 0)
+        question = (prompt or "").strip()
+
+        if "离线" in question:
+            detail = "、".join(offline_items[:12]) or "无"
+            return f"{group_name}当前识别到 {len(offline_items)} 台离线设备:{detail}。建议按站点层级先核查通信链路、供电和维护停用状态;当前结果来自资产组节点状态,不等同于未闭环报警。"
+        if "故障" in question or "异常" in question or "告警" in question:
+            fault_detail = "、".join(fault_items[:8]) or "无"
+            warning_detail = "、".join(warning_items[:8]) or "无"
+            return f"{group_name}当前识别到故障 {len(fault_items)} 台、告警 {len(warning_items)} 台。故障证据:{fault_detail};告警证据:{warning_detail}。建议先确认故障对泵站供水连续性的影响,再核查告警持续时间与关联测点。"
+        if "完整" in question or "可信" in question or "发布" in question or "正式" in question:
+            known = max(0, total - unknown)
+            return f"{group_name}当前状态数据覆盖 {known}/{total} 台,仍有 {unknown} 台待补齐。可用于值班预览和异常排查;若要发布正式日报,建议先补齐节点状态,并接通报警闭环、工单和保养数据。"
+        if "在线" in question or "运行" in question:
+            detail = "、".join(running_items[:10]) or "无"
+            return f"{group_name}当前识别到 {len(running_items)} 台处于运行/正常状态:{detail}。该统计基于当前节点状态;状态待补齐设备未计入在线数。"
+
+        return (
+            f"{group_name}({station_name})当前纳管 {total} 台,运行 {len(running_items)} 台、"
+            f"故障 {len(fault_items)} 台、告警 {len(warning_items)} 台、离线 {len(offline_items)} 台,"
+            f"另有 {unknown} 台状态待补齐。建议优先处理故障与集中离线问题;报警闭环、工单和保养尚未接入,因此不对这些数量作推测。"
+        )
+    if module == "station-insight":
+        stats = context.get("statistics") or {}
+        group = context.get("assetGroup") or {}
+        abnormal = context.get("abnormalDevices") or []
+        group_name = group.get("name") or "当前资产组"
+        fault_names = [str(item.get("name")) for item in abnormal if isinstance(item, dict) and item.get("status") == "故障"]
+        warning_names = [str(item.get("name")) for item in abnormal if isinstance(item, dict) and item.get("status") == "告警"]
+        faults = int(stats.get("faults") or 0)
+        warnings = int(stats.get("warnings") or 0)
+        offline = int(stats.get("offline") or 0)
+        unknown = int(stats.get("unknown") or 0)
+        first = (
+            f"{group_name}存在 {faults} 台故障设备({('、'.join(fault_names[:2]) or '详见证据清单')}),可能影响站点运行连续性,建议立即确认影响范围并建立处置任务。"
+            if faults else f"{group_name}当前未识别到故障状态,建议继续关注关键功能系统的运行连续性。"
+        )
+        second = (
+            f"当前有 {warnings} 台告警、{offline} 台离线设备({('、'.join(warning_names[:2]) or '详见证据清单')}),建议按站点层级核查通信、供电和关联过程量。"
+            if warnings or offline else "当前未识别到告警或离线设备,站点状态总体可控。"
+        )
+        third = (
+            f"仍有 {unknown} 台设备状态待补齐,当前结论仅覆盖可追溯数据;建议取得完整节点状态后再发布正式日报。"
+            if unknown else "节点状态数据已完整,可基于当前统计发布正式站点日报。"
+        )
+        return "\n".join([first, second, third])
+    if module == "period-analysis":
+        group = context.get("assetGroup") or {}
+        current = context.get("current") or {}
+        previous = context.get("previous") or {}
+        deltas = context.get("deltas") or {}
+        sites = context.get("siteContributions") or []
+        name = str(group.get("name") or "当前资产组")
+        if not context.get("historyScoped"):
+            return f"{name}的历史事件接口尚未返回可验证的资产组范围,当前不能生成可信环比结论。请检查事件记录中的 nodeId/nodeName 映射。"
+        event_delta = deltas.get("eventCount")
+        impact_delta = deltas.get("impactHours")
+        availability_delta = deltas.get("availabilityEstimate")
+        event_text = "缺少上月基数" if event_delta is None else f"{event_delta:+.1f}%"
+        impact_text = "缺少上月基数" if impact_delta is None else f"{impact_delta:+.1f}%"
+        availability_text = "无法计算" if availability_delta is None else f"{availability_delta:+.2f} 个百分点"
+        top_site = sites[0].get("name") if sites and isinstance(sites[0], dict) else "暂无集中区域"
+        return (
+            f"{name}本月截至当前记录 {current.get('eventCount', 0)} 条场站事件,较上月同期 {event_text};"
+            f"事件影响时长 {current.get('impactHours', 0)} 小时,环比 {impact_text}。"
+            f"事件影响后的运行可用率估算变化 {availability_text},当前事件最集中的层级为“{top_site}”。"
+            f"建议优先核查高频区域及重复事件;该结论基于真实事件历史和资产组节点映射,不包含尚未接入的能耗与工单数据。"
+        )
+    if isinstance(devices, list) and devices:
+        n = len(devices)
+        n_attention = len(
+            [d for d in devices if isinstance(d, dict) and d.get("status") in ("告警", "预警", "故障", "离线", "alarm", "warning", "fault", "offline")]
+        )
+        n_offline = len(
+            [d for d in devices if isinstance(d, dict) and d.get("status") in ("离线", "offline", "Offline")]
+        )
+        top = sorted([d for d in devices if isinstance(d, dict)], key=lambda x: x.get("score", 100))[:3]
+        lines = [f"基于已加载的 {n} 台设备实时数据:需关注 {n_attention} 台,离线 {n_offline} 台。"]
+        if top:
+            lines.append("风险最高的设备:")
+            for d in top:
+                lines.append(f"  - {d.get('site', '')} · {d.get('name', '')}({d.get('status', '')},{d.get('alarm', '')})")
+        lines.append("建议:优先核查上述设备的通信与过程量,必要时按站点 SOP 切换备用设备,并安排点检。")
+        return "\n".join(lines)
+    return "当前为预览数据,未获取到真实设备列表。请在“MU 数据源配置”中完成真实连接后重试。"
+
+
+# ---- 接口 ----
+@app.post("/api/ai/analyze")
+async def analyze(req: AnalyzeRequest):
+    messages = [
+        {"role": "system", "content": SYSTEM_PROMPT},
+        {
+            "role": "user",
+            "content": (
+                f"分析模块:{req.module}\n"
+                f"用户问题:{req.prompt}\n"
+                f"真实数据(JSON):\n{json.dumps(req.context, ensure_ascii=False)}"
+            ),
+        },
+    ]
+    try:
+        text = await call_codebuddy(messages)
+        if text:
+            return {"ok": True, "analysis": text, "model": CODEBUDDY_MODEL, "fallback": False}
+    except Exception:
+        # 调用失败则回退到本地规则分析
+        pass
+    return {"ok": True, "analysis": local_analysis(req.module, req.prompt, req.context), "model": "local-rule", "fallback": True}
+
+
+@app.post("/api/mu-connect")
+async def mu_connect(req: MuConnectRequest):
+    config = req.config
+    secret = req.secret or ""
+    try:
+        portal_base = validate_base_url(config.baseUrl)
+        apm_base = resolve_apm_base(config)
+    except ValueError as e:
+        return {"ok": False, "message": str(e)}
+
+    raw_mode = bool(config.rawCurl and config.rawCurl.strip())
+    if config.authMode == "session" and not raw_mode:
+        return {
+            "ok": False,
+            "message": "localhost 无法读取 MU 门户登录态。请使用“原始请求”或 EIToken/Bearer 方式连接。",
+            "api_base": apm_base,
+        }
+    if not raw_mode and not secret:
+        msg = {"bearer":"请输入访问令牌","eitoken":"请输入 EIToken","basic":"请输入密码","login":"请输入密码"}.get(config.authMode,"请输入密码")
+        return {"ok": False, "message": msg}
+    if not raw_mode and config.authMode in ("basic","login") and not config.username:
+        return {"ok": False, "message": "请输入账号"}
+
+    headers = {"Accept": "application/json"}
+    if config.orgId:
+        headers["org-id"] = config.orgId
+    if config.tenantId:
+        headers["Tenant-Id"] = config.tenantId
+
+    if raw_mode:
+        headers.update(parse_curl_headers(config.rawCurl))
+    elif config.authMode == "basic":
+        token = base64.b64encode(f"{config.username}:{secret}".encode()).decode()
+        headers["Authorization"] = f"Basic {token}"
+    elif config.authMode == "bearer":
+        headers["Authorization"] = normalize_authorization(secret)
+    elif config.authMode == "eitoken":
+        cookie, authorization = cookie_and_authorization(secret)
+        headers["Cookie"] = cookie
+        if authorization:
+            headers["Authorization"] = authorization
+    elif config.authMode == "login":
+        if (urlparse(portal_base).hostname or "").endswith("wise-insightapm.com"):
+            return {
+                "ok": False,
+                "message": "该站点使用门户 SSO,不能通过通用账号密码接口登录。请复制真实请求 cURL,或使用 EIToken/Bearer。",
+                "api_base": apm_base,
+            }
+        try:
+            async with httpx.AsyncClient(base_url=portal_base, timeout=15, follow_redirects=False) as login_client:
+                login_response = await login_client.post(
+                    config.loginPath,
+                    json={config.usernameField: config.username, config.passwordField: secret},
+                )
+                if login_response.status_code >= 400:
+                    return {"ok": False, "message": f"登录接口返回 {login_response.status_code}"}
+                try:
+                    login_payload = login_response.json()
+                except Exception:
+                    login_payload = {}
+                token = find_token(login_payload)
+                if token:
+                    headers["Authorization"] = normalize_authorization(token)
+                if login_client.cookies:
+                    headers["Cookie"] = "; ".join(f"{key}={value}" for key, value in login_client.cookies.items())
+                if not token and not login_client.cookies:
+                    return {"ok": False, "message": "登录成功,但未找到会话 Cookie 或访问令牌"}
+        except Exception as e:
+            return {"ok": False, "message": f"登录请求失败:{e}"}
+
+    headers = sanitize_forward_headers(headers)
+    if raw_mode and not any(key.lower() in ("cookie", "authorization") for key in headers):
+        return {
+            "ok": False,
+            "message": "原始请求中没有可用的 Cookie 或 Authorization。请从浏览器开发者工具重新 Copy as cURL (bash)。",
+            "api_base": apm_base,
+        }
+
+    async with httpx.AsyncClient(base_url=apm_base, headers=headers, timeout=20, follow_redirects=False) as client:
+        async def probe(path: str) -> dict:
+            try:
+                response = await client.get(path)
+                response_text = response.text or ""
+                try:
+                    payload = json.loads(response_text) if response_text else None
+                except Exception:
+                    payload = {"text": response_text[:800]}
+                return {
+                    "path": path,
+                    "status": response.status_code,
+                    "ok": 200 <= response.status_code < 300,
+                    "payload": payload,
+                    "snippet": json.dumps(payload, ensure_ascii=False)[:500] if isinstance(payload, (dict, list)) else response_text[:500],
+                }
+            except Exception as e:
+                return {"path": path, "status": 0, "ok": False, "error": str(e)[:200]}
+
+        results = await asyncio.gather(*(probe(path) for path in PROBE_ENDPOINTS))
+
+        # 资产组列表是已验证可用的入口。连接成功后继续按当前选择拉取节点清单,
+        # 让前端日报拿到真正属于该资产组的数据,而不是把“资产组行”误当作设备。
+        group_result = next(
+            (item for item in results if item.get("path", "").startswith("/api/v1/asset/group/list") and item.get("ok")),
+            None,
+        )
+        asset_groups = extract_payload_rows(group_result.get("payload") if group_result else None)
+
+        selected_group = None
+        if req.selectedGroupId:
+            selected_group = next(
+                (item for item in asset_groups if str(item.get("id", "")) == str(req.selectedGroupId)),
+                None,
+            )
+        if selected_group is None and req.selectedGroupName:
+            selected_group = next(
+                (item for item in asset_groups if str(item.get("name", "")) == req.selectedGroupName),
+                None,
+            )
+        if selected_group is None:
+            selected_group = next(
+                (item for item in asset_groups if "水务" in str(item.get("name", ""))),
+                asset_groups[0] if asset_groups else None,
+            )
+
+        nodes_payload = None
+        node_rows_count = 0
+        if selected_group:
+            group_id = str(selected_group.get("id", ""))
+            group_name = str(selected_group.get("name", ""))
+            node_path = "/api/v1/asset/node/list"
+            # 源码确认的是同一个 node/list 接口;不同 MU 版本对 groupId/groupName
+            # 的必填组合不一致,因此只在该真实接口上依次尝试三种参数组合。
+            node_queries = [
+                {"groupId": group_id, "groupName": group_name, "index": 0, "count": 10000},
+                {"groupId": group_id, "index": 0, "count": 10000},
+                {"groupName": group_name, "index": 0, "count": 10000},
+            ]
+            for params in node_queries:
+                query_label = "&".join(f"{key}={value}" for key, value in params.items())
+                try:
+                    node_response = await client.get(node_path, params=params)
+                    node_text = node_response.text or ""
+                    try:
+                        candidate_payload = json.loads(node_text) if node_text else None
+                    except Exception:
+                        candidate_payload = {"text": node_text[:800]}
+                    candidate_rows = extract_payload_rows(candidate_payload)
+                    candidate_count = len(candidate_rows)
+                    if nodes_payload is None or candidate_count > node_rows_count:
+                        nodes_payload = candidate_payload
+                        node_rows_count = candidate_count
+                    results.append({
+                        "path": f"{node_path}?{query_label}",
+                        "status": node_response.status_code,
+                        "ok": 200 <= node_response.status_code < 300,
+                        "payload": candidate_payload,
+                        "row_count": candidate_count,
+                        "snippet": json.dumps(candidate_payload, ensure_ascii=False)[:500] if isinstance(candidate_payload, (dict, list)) else node_text[:500],
+                    })
+                    if 200 <= node_response.status_code < 300 and candidate_count:
+                        break
+                except Exception as e:
+                    results.append({"path": f"{node_path}?{query_label}", "status": 0, "ok": False, "error": str(e)[:200]})
+        sent_headers = [
+            key for key in client.headers.keys() if key.lower() not in ("cookie", "authorization")
+        ]
+        for header_name in ("Authorization", "Cookie"):
+            if header_name.lower() in client.headers:
+                sent_headers.append(header_name)
+
+        successful = [r for r in results if r["ok"]]
+        if not successful:
+            status_text = ";".join(f"{r['path']} -> {r.get('status') or r.get('error','连接失败')}" for r in results)
+            snippets = " ".join(str(r.get("snippet", "")) for r in results).lower()
+            auth_rejected = any(r.get("status") in (401, 403) for r in results) or "no auth" in snippets or "token lack" in snippets
+            wrong_base = all(r.get("status") == 404 for r in results)
+            if auth_rejected:
+                reason = "已到达正确的 APM 服务,但 Token/Cookie 未被接受"
+            elif wrong_base:
+                reason = "APM API 地址不正确"
+            else:
+                reason = "APM 服务已到达,但接口未返回可用数据"
+            return {
+                "ok": False,
+                "message": f"{reason}:{status_text}",
+                "endpoints": results,
+                "sent_headers": sent_headers,
+                "api_base": apm_base,
+            }
+        return {
+            "ok": True,
+            "message": f"已连接 {apm_base},通过 {len(successful)}/{len(results)} 个接口验证",
+            "payloads": [r.get("payload") for r in successful if r.get("payload") is not None],
+            "asset_groups": asset_groups,
+            "selected_group": selected_group,
+            "nodes_payload": nodes_payload,
+            "node_rows_count": node_rows_count,
+            "endpoints": results,
+            "sent_headers": sent_headers,
+            "api_base": apm_base,
+        }
+
+
+class MuFetchRequest(BaseModel):
+    config: MuConfig
+    secret: str = ""
+    path: str = ""
+
+
+@app.post("/api/mu-fetch")
+async def mu_fetch(req: MuFetchRequest):
+    """透传鉴权并拉取指定路径,供前端在连接成功后获取真实设备/告警数据。"""
+    config = req.config
+    secret = req.secret or ""
+    path = (req.path or "").strip()
+    if not path:
+        return {"ok": False, "message": "缺少 path"}
+    if not path.startswith("/"):
+        return {"ok": False, "message": "path 必须以 / 开头"}
+    try:
+        apm_base = resolve_apm_base(config)
+    except ValueError as e:
+        return {"ok": False, "message": str(e)}
+
+    headers = {"Accept": "application/json"}
+    if config.orgId:
+        headers["org-id"] = config.orgId
+    if config.tenantId:
+        headers["Tenant-Id"] = config.tenantId
+
+    raw_mode = bool(config.rawCurl and config.rawCurl.strip())
+    if config.authMode == "session" and not raw_mode:
+        return {"ok": False, "message": "localhost 无法复用 MU 浏览器登录态,请使用原始请求或 Token"}
+    if raw_mode:
+        headers.update(parse_curl_headers(config.rawCurl))
+    elif config.authMode == "eitoken":
+        cookie, authorization = cookie_and_authorization(secret)
+        headers["Cookie"] = cookie
+        if authorization:
+            headers["Authorization"] = authorization
+    elif config.authMode == "bearer":
+        headers["Authorization"] = normalize_authorization(secret)
+    elif config.authMode == "basic":
+        token = base64.b64encode(f"{config.username}:{secret}".encode()).decode()
+        headers["Authorization"] = f"Basic {token}"
+
+    headers = sanitize_forward_headers(headers)
+    if raw_mode and not any(key.lower() in ("cookie", "authorization") for key in headers):
+        return {"ok": False, "message": "原始请求中没有可用的 Cookie 或 Authorization"}
+
+    async with httpx.AsyncClient(base_url=apm_base, headers=headers, timeout=20, follow_redirects=False) as client:
+        try:
+            r = await client.get(path)
+        except Exception as e:
+            return {"ok": False, "message": f"请求失败:{e}"}
+        text = r.text or ""
+        try:
+            data = json.loads(text) if text else None
+        except Exception:
+            data = {"text": text[:1000]}
+        return {
+            "ok": 200 <= r.status_code < 300,
+            "status": r.status_code,
+            "data": data,
+            "snippet": (json.dumps(data, ensure_ascii=False)[:800] if isinstance(data, (dict, list)) else text[:800]),
+            "api_base": apm_base,
+        }
+
+
+@app.post("/api/mu-period-compare")
+async def mu_period_compare(req: MuPeriodCompareRequest):
+    """按资产组计算本月与上月同期的真实事件运行环比。"""
+    try:
+        apm_base = resolve_apm_base(req.config)
+    except ValueError as e:
+        return {"ok": False, "message": str(e)}
+    headers, auth_error = build_mu_request_headers(req.config, req.secret or "")
+    if auth_error:
+        return {"ok": False, "message": auth_error, "api_base": apm_base}
+
+    current_start, current_end, previous_start, previous_end = month_window()
+    endpoints: list[dict] = []
+
+    async with httpx.AsyncClient(base_url=apm_base, headers=headers, timeout=25, follow_redirects=False) as client:
+        async def get_json(path: str, params: Optional[dict] = None) -> tuple[Optional[Any], dict]:
+            try:
+                response = await client.get(path, params=params)
+                text = response.text or ""
+                try:
+                    payload = json.loads(text) if text else None
+                except Exception:
+                    payload = {"text": text[:800]}
+                trace = {
+                    "path": str(response.request.url).replace(apm_base, ""),
+                    "status": response.status_code,
+                    "ok": 200 <= response.status_code < 300,
+                    "rowCount": len(extract_payload_rows(payload)),
+                }
+                endpoints.append(trace)
+                return payload, trace
+            except Exception as e:
+                trace = {"path": path, "status": 0, "ok": False, "error": str(e)[:200], "rowCount": 0}
+                endpoints.append(trace)
+                return None, trace
+
+        group_payload, group_trace = await get_json("/api/v1/asset/group/list", {"count": 1000})
+        groups = extract_payload_rows(group_payload)
+        selected_group = None
+        if req.selectedGroupId:
+            selected_group = next((row for row in groups if str(pick_value(row, "id", "groupId") or "") == str(req.selectedGroupId)), None)
+        if selected_group is None and req.selectedGroupName:
+            selected_group = next((row for row in groups if str(pick_value(row, "name", "groupName") or "") == req.selectedGroupName), None)
+        if selected_group is None:
+            if not group_trace.get("ok"):
+                status = group_trace.get("status") or "连接失败"
+                return {
+                    "ok": False,
+                    "message": f"真实资产组接口返回 {status},Cookie / Token 可能已失效",
+                    "endpoints": endpoints,
+                    "api_base": apm_base,
+                }
+            return {
+                "ok": False,
+                "message": f"资产组“{req.selectedGroupName or req.selectedGroupId}”未在真实资产组接口中找到",
+                "endpoints": endpoints,
+                "api_base": apm_base,
+            }
+
+        group_id = str(pick_value(selected_group, "id", "groupId") or "")
+        group_name = str(pick_value(selected_group, "name", "groupName") or req.selectedGroupName)
+        nodes_payload = None
+        nodes: list[dict] = []
+        for params in (
+            {"groupId": group_id, "groupName": group_name, "index": 0, "count": 10000},
+            {"groupId": group_id, "index": 0, "count": 10000},
+            {"groupName": group_name, "index": 0, "count": 10000},
+        ):
+            candidate, trace = await get_json("/api/v1/asset/node/list", params)
+            candidate_rows = extract_payload_rows(candidate)
+            if candidate_rows:
+                nodes_payload, nodes = candidate, candidate_rows
+                break
+            if nodes_payload is None and trace.get("ok"):
+                nodes_payload = candidate
+
+        node_ids = {str(pick_value(row, "id", "nodeId", "deviceId", "assetId")) for row in nodes if pick_value(row, "id", "nodeId", "deviceId", "assetId") is not None}
+        node_names = {str(pick_value(row, "name", "nodeName", "deviceName", "assetName")) for row in nodes if pick_value(row, "name", "nodeName", "deviceName", "assetName") is not None}
+        node_count = int(pick_value(selected_group, "deviceCount", "nodeCount", "assetCount") or len(nodes))
+
+        async def get_events(start: datetime, end: datetime) -> tuple[list[dict], bool]:
+            payload, trace = await get_json(
+                "/api/v1/event/hist",
+                {
+                    "startTs": int(start.timestamp() * 1000),
+                    "endTs": int(end.timestamp() * 1000),
+                    "groupId": group_id,
+                    "groupName": group_name,
+                    "index": 0,
+                    "count": 10000,
+                    "sortType": "desc",
+                },
+            )
+            rows = extract_payload_rows(payload)
+            if not trace.get("ok"):
+                return [], False
+            rows_with_identity = [
+                row for row in rows
+                if pick_value(row, "nodeId", "assetId", "deviceId", "nodeName", "deviceName", "assetName") is not None
+            ]
+            if rows_with_identity and (node_ids or node_names):
+                scoped = []
+                for row in rows_with_identity:
+                    row_id = str(pick_value(row, "nodeId", "assetId", "deviceId") or "")
+                    row_name = str(pick_value(row, "nodeName", "deviceName", "assetName") or "")
+                    if row_id in node_ids or row_name in node_names:
+                        scoped.append(row)
+                return scoped, True
+            # 空结果是有效历史结果;非空但没有节点身份时不能证明属于当前资产组。
+            return ([], True) if not rows else ([], False)
+
+        (current_events, current_scoped), (previous_events, previous_scoped) = await asyncio.gather(
+            get_events(current_start, current_end),
+            get_events(previous_start, previous_end),
+        )
+
+        current_summary = event_summary(current_events, current_start, current_end, node_count)
+        previous_summary = event_summary(previous_events, previous_start, previous_end, node_count)
+
+        node_lookup: dict[str, dict] = {}
+        for row in nodes:
+            row_id = str(pick_value(row, "id", "nodeId", "deviceId", "assetId") or "")
+            row_name = str(pick_value(row, "name", "nodeName", "deviceName", "assetName") or "")
+            if row_id:
+                node_lookup[row_id] = row
+            if row_name:
+                node_lookup[row_name] = row
+
+        def event_breakdown(rows: list[dict]) -> tuple[list[dict], list[dict]]:
+            sites: dict[str, int] = {}
+            devices: dict[str, dict] = {}
+            for event in rows:
+                identity = str(pick_value(event, "nodeId", "assetId", "deviceId", "nodeName", "deviceName", "assetName") or "未映射节点")
+                node = node_lookup.get(identity, {})
+                name = str(pick_value(node, "name", "nodeName", "deviceName", "assetName") or pick_value(event, "nodeName", "deviceName", "assetName") or identity)
+                site = str(pick_value(node, "pathName", "fullPathName", "parentName", "siteName", "stationName") or "当前资产组")
+                sites[site] = sites.get(site, 0) + 1
+                device = devices.setdefault(identity, {"id": identity, "name": name, "site": site, "eventCount": 0, "highPriority": 0})
+                device["eventCount"] += 1
+                level = str(pick_value(event, "level", "severity", "priority", "alarmLevel") or "")
+                if re.search(r"P?1|critical|high|严重|紧急", level, re.I):
+                    device["highPriority"] += 1
+            site_rows = [{"name": name, "eventCount": count} for name, count in sorted(sites.items(), key=lambda item: item[1], reverse=True)]
+            device_rows = sorted(devices.values(), key=lambda item: (item["eventCount"], item["highPriority"]), reverse=True)
+            return site_rows[:8], device_rows[:12]
+
+        site_rows, device_rows = event_breakdown(current_events)
+        return {
+            "ok": True,
+            "message": "已按当前资产组计算本月与上月同期运行环比" if current_scoped and previous_scoped else "接口已返回,但历史事件缺少可验证的资产组节点身份",
+            "api_base": apm_base,
+            "assetGroup": {"id": group_id, "name": group_name, "deviceCount": node_count},
+            "periods": {
+                "current": {"start": current_start.isoformat(), "end": current_end.isoformat(), "label": f"{current_start:%Y-%m-%d} 至 {current_end:%Y-%m-%d}"},
+                "previous": {"start": previous_start.isoformat(), "end": previous_end.isoformat(), "label": f"{previous_start:%Y-%m-%d} 至 {previous_end:%Y-%m-%d}"},
+            },
+            "current": current_summary,
+            "previous": previous_summary,
+            "deltas": {
+                "eventCount": compare_delta(current_summary["eventCount"], previous_summary["eventCount"]),
+                "affectedNodes": compare_delta(current_summary["affectedNodes"], previous_summary["affectedNodes"]),
+                "impactHours": compare_delta(current_summary["impactHours"], previous_summary["impactHours"]),
+                "availabilityEstimate": None if current_summary["availabilityEstimate"] is None or previous_summary["availabilityEstimate"] is None else round(current_summary["availabilityEstimate"] - previous_summary["availabilityEstimate"], 2),
+            },
+            "siteContributions": site_rows,
+            "topDevices": device_rows,
+            "historyScoped": current_scoped and previous_scoped,
+            "nodesPayload": nodes_payload,
+            "endpoints": endpoints,
+        }
+
+
+@app.get("/api/health")
+async def health():
+    return {
+        "ok": True,
+        "connection_version": "apm-bff-v2",
+        "ai_model": CODEBUDDY_MODEL,
+        "ai_configured": bool(CODEBUDDY_API_BASE and CODEBUDDY_API_KEY),
+    }
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644 (file)
index 0000000..9d3c140
--- /dev/null
@@ -0,0 +1,4 @@
+fastapi
+uvicorn[standard]
+httpx
+python-dotenv
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644 (file)
index 0000000..047f8e9
--- /dev/null
@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Station AI Copilot · Demo</title>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644 (file)
index 0000000..a975e3c
--- /dev/null
@@ -0,0 +1,1750 @@
+{
+  "name": "station-ai-demo-frontend",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "station-ai-demo-frontend",
+      "version": "0.1.0",
+      "dependencies": {
+        "react": "^18.3.1",
+        "react-dom": "^18.3.1"
+      },
+      "devDependencies": {
+        "@types/react": "^18.3.12",
+        "@types/react-dom": "^18.3.1",
+        "@vitejs/plugin-react": "^4.3.4",
+        "typescript": "^5.6.3",
+        "vite": "^5.4.11"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+      "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+      "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+      "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.7",
+        "@babel/helper-compilation-targets": "^7.29.7",
+        "@babel/helper-module-transforms": "^7.29.7",
+        "@babel/helpers": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/template": "^7.29.7",
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+      "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.8",
+        "@babel/types": "^7.29.8",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+      "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.29.7",
+        "@babel/helper-validator-option": "^7.29.7",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+      "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+      "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+      "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "@babel/traverse": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+      "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+      "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+      "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+      "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.8"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-self": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+      "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-source": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+      "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+      "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+      "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.8",
+        "@babel/helper-globals": "^7.29.7",
+        "@babel/parser": "^7.29.8",
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.8",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+      "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+      "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^22.20 || ^24.12 || >=25"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.27",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+      "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.5.tgz",
+      "integrity": "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.5.tgz",
+      "integrity": "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.5.tgz",
+      "integrity": "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz",
+      "integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.5.tgz",
+      "integrity": "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.5.tgz",
+      "integrity": "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.5.tgz",
+      "integrity": "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.5.tgz",
+      "integrity": "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.5.tgz",
+      "integrity": "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.5.tgz",
+      "integrity": "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.5.tgz",
+      "integrity": "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.5.tgz",
+      "integrity": "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.5.tgz",
+      "integrity": "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.5.tgz",
+      "integrity": "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.5.tgz",
+      "integrity": "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.5.tgz",
+      "integrity": "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.5.tgz",
+      "integrity": "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.5.tgz",
+      "integrity": "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.5.tgz",
+      "integrity": "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.5.tgz",
+      "integrity": "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.5.tgz",
+      "integrity": "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.5.tgz",
+      "integrity": "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.5.tgz",
+      "integrity": "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.5.tgz",
+      "integrity": "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz",
+      "integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.2"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/prop-types": {
+      "version": "15.7.15",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+      "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/react": {
+      "version": "18.3.31",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+      "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/prop-types": "*",
+        "csstype": "^3.2.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "18.3.7",
+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+      "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "^18.0.0"
+      }
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+      "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.28.0",
+        "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+        "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+        "@rolldown/pluginutils": "1.0.0-beta.27",
+        "@types/babel__core": "^7.20.5",
+        "react-refresh": "^0.17.0"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.11.16",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.16.tgz",
+      "integrity": "sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.8",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
+      "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.11.12",
+        "caniuse-lite": "^1.0.30001809",
+        "electron-to-chromium": "^1.5.402",
+        "node-releases": "^2.0.53",
+        "update-browserslist-db": "^1.3.0"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001809",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
+      "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.412",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.412.tgz",
+      "integrity": "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.18",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+      "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.53",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
+      "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/postcss": {
+      "version": "8.5.26",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+      "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.17",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/react": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "scheduler": "^0.23.2"
+      },
+      "peerDependencies": {
+        "react": "^18.3.1"
+      }
+    },
+    "node_modules/react-refresh": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+      "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.62.5",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.5.tgz",
+      "integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.9"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+        "@rollup/rollup-android-arm-eabi": "4.62.5",
+        "@rollup/rollup-android-arm64": "4.62.5",
+        "@rollup/rollup-darwin-arm64": "4.62.5",
+        "@rollup/rollup-darwin-x64": "4.62.5",
+        "@rollup/rollup-freebsd-arm64": "4.62.5",
+        "@rollup/rollup-freebsd-x64": "4.62.5",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.62.5",
+        "@rollup/rollup-linux-arm-musleabihf": "4.62.5",
+        "@rollup/rollup-linux-arm64-gnu": "4.62.5",
+        "@rollup/rollup-linux-arm64-musl": "4.62.5",
+        "@rollup/rollup-linux-loong64-gnu": "4.62.5",
+        "@rollup/rollup-linux-loong64-musl": "4.62.5",
+        "@rollup/rollup-linux-ppc64-gnu": "4.62.5",
+        "@rollup/rollup-linux-ppc64-musl": "4.62.5",
+        "@rollup/rollup-linux-riscv64-gnu": "4.62.5",
+        "@rollup/rollup-linux-riscv64-musl": "4.62.5",
+        "@rollup/rollup-linux-s390x-gnu": "4.62.5",
+        "@rollup/rollup-linux-x64-gnu": "4.62.5",
+        "@rollup/rollup-linux-x64-musl": "4.62.5",
+        "@rollup/rollup-openbsd-x64": "4.62.5",
+        "@rollup/rollup-openharmony-arm64": "4.62.5",
+        "@rollup/rollup-win32-arm64-msvc": "4.62.5",
+        "@rollup/rollup-win32-ia32-msvc": "4.62.5",
+        "@rollup/rollup-win32-x64-gnu": "4.62.5",
+        "@rollup/rollup-win32-x64-msvc": "4.62.5",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.23.2",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      }
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz",
+      "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "5.4.21",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+      "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    }
+  }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644 (file)
index 0000000..fd2485d
--- /dev/null
@@ -0,0 +1,22 @@
+{
+  "name": "station-ai-demo-frontend",
+  "private": true,
+  "version": "0.1.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc -b && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "react": "^18.3.1",
+    "react-dom": "^18.3.1"
+  },
+  "devDependencies": {
+    "@types/react": "^18.3.12",
+    "@types/react-dom": "^18.3.1",
+    "@vitejs/plugin-react": "^4.3.4",
+    "typescript": "^5.6.3",
+    "vite": "^5.4.11"
+  }
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644 (file)
index 0000000..ae901af
--- /dev/null
@@ -0,0 +1,1351 @@
+import { useEffect, useMemo, useState, type CSSProperties } from "react";
+import { analyzeChat, analyzeModule, analyzeSummary, loadPeriodComparison, type PeriodComparisonResponse } from "./api";
+
+type ScenarioKey = "daily" | "qa" | "period" | "benchmark" | "maintenance" | "health" | "prediction" | "alarm" | "efficiency" | "capacity" | "strategy" | "emergency";
+type DesignSectionKey = ScenarioKey | "architecture";
+type SkillKey = ScenarioKey | "settings" | "design";
+type ConnectionState = "connecting" | "live" | "preview";
+type Device = { id: string; name: string; site: string; type: string; status: string; score: number; alarm: string; metric: string };
+type AssetGroup = { id: string; name: string; deviceCount?: number; orgName?: string; source: "api" | "snapshot" };
+type AuthMode = "session" | "basic" | "bearer" | "eitoken" | "login";
+type MuConfig = {
+  baseUrl: string;
+  apiBaseUrl: string;
+  authMode: AuthMode;
+  username: string;
+  tenant: string;
+  orgCode: string;
+  orgId: string;
+  tenantId: string;
+  rawCurl: string;
+  loginPath: string;
+  usernameField: string;
+  passwordField: string;
+};
+
+const API_BASE = "https://desk-desk-cluster01.cn.wise-insightapm.com";
+const DEFAULT_MU_CONFIG: MuConfig = {
+  baseUrl: API_BASE,
+  apiBaseUrl: "",
+  authMode: "session",
+  username: "",
+  tenant: "",
+  orgCode: "",
+  orgId: "",
+  tenantId: "",
+  rawCurl: "",
+  loginPath: "/api/v1/auth/login",
+  usernameField: "username",
+  passwordField: "password",
+};
+// 路径由后端拼接到自动推导出的 portal-apm-.../api-apm 服务根地址。
+const API_ENDPOINTS = [
+  "/api/v1/asset/group/list?count=1000",
+  "/api/v1/asset/node/root",
+  "/api/v1/event/latest",
+];
+
+const fallbackDevices: Device[] = [
+  { id:"a1", name:"冷冻水泵 P-03", site:"AIDC-01", type:"冷冻水泵", status:"告警", score:66, alarm:"冷冻水供水温度高", metric:"10.8 °C" },
+  { id:"w1", name:"1# 鼓风机", site:"东区污水处理厂", type:"曝气鼓风机", status:"预警", score:61, alarm:"轴承振动持续上升", metric:"7.2 mm/s" },
+  { id:"g1", name:"入口压力 PT-101", site:"北区燃气调压站", type:"压力变送器", status:"离线", score:72, alarm:"通信中断", metric:"—" },
+  { id:"s1", name:"逆变器 INV-08", site:"光伏场站 A", type:"逆变器", status:"正常", score:78, alarm:"效率偏离同组基线", metric:"96.1 %" },
+  { id:"p1", name:"加压泵 P-02", site:"城南供水站", type:"变频泵", status:"正常", score:87, alarm:"无", metric:"0.43 MPa" },
+];
+
+type ScenarioDefinition = {
+  key: ScenarioKey;
+  section: "场站运营" | "运营分析" | "风险与策略";
+  icon: string;
+  title: string;
+  short: string;
+  tone: string;
+  summary: string;
+  metrics: Array<{label:string;value:string;delta:string;tone?:"good"|"warn"|"bad"}>;
+  bars: number[];
+  insights: string[];
+  sources: string[];
+  formula: string;
+  maturity: string;
+  visual: "report" | "qa" | "compare" | "operations" | "health" | "forecast" | "alarm" | "efficiency" | "capacity" | "strategy" | "emergency";
+};
+
+type ApiStatus = "verified" | "source" | "external";
+type ApiKey = "groups" | "nodes" | "nodeStatus" | "properties" | "latest" | "history" | "events" | "alarmEvents" | "alarmProcess" | "workOrders" | "maintenance" | "stationParams" | "controlLogs" | "schedules" | "execution" | "external";
+type ApiDefinition = { label:string; method:string; path:string; status:ApiStatus; note:string };
+type ScenarioDesign = {
+  feasibility:string;
+  sourceText:string;
+  apiKeys:ApiKey[];
+  gaps:string;
+  kpiCalcs:string[];
+  visualPanel:string;
+  visualCalc:string;
+  insightCalc:string;
+  summaryCalc:string;
+};
+type PageNarrative = { audience:string; value:string; whyAI:string; feasibilityEvidence:string };
+type PanelSpec = { name:string; kind:string; source:string; apis:ApiDefinition[]; calc:string };
+
+const skills: ScenarioDefinition[] = [
+  {key:"daily",section:"场站运营",icon:"报",title:"场站智能日报",short:"基于当前资产组节点状态生成可追溯日结",tone:"cyan",visual:"report",summary:"日报按当前资产组重新统计纳管、在线、离线、告警、故障及状态完整度;未接入的数据域明确显示为待补齐。",metrics:[{label:"纳管设备",value:"动态",delta:"资产组节点总数"},{label:"当前在线",value:"动态",delta:"按当前节点状态统计",tone:"good"},{label:"当前离线",value:"动态",delta:"不沿用其他资产组数据",tone:"warn"},{label:"告警 / 故障",value:"动态",delta:"设备当前状态口径",tone:"warn"}],bars:[0,0,0,0,0,0,0],insights:["按故障、告警、离线和数据完整度生成关注事项","设备名称只作为场站结论的证据","报警闭环、工单与保养未接入时不推测"],sources:["资产组清单","节点清单与当前状态","报警闭环(待接入)","工单与保养(待接入)"],formula:"当前资产组节点确定性统计 + 可编辑提示词的 AI 表述",maturity:"首版已实现 · 运维域待接入"},
+  {key:"qa",section:"场站运营",icon:"问",title:"场站问答",short:"围绕当前资产组的节点现状进行证据化问答",tone:"blue",visual:"qa",summary:"问答始终锁定当前资产组,当前可回答资产组范围、节点数量、运行状态、故障、告警、离线和数据完整度。",metrics:[{label:"当前资产组",value:"动态",delta:"组织与场站范围"},{label:"已加载证据",value:"动态",delta:"节点状态记录",tone:"good"},{label:"可回答数据域",value:"2 / 5",delta:"资产组、节点状态"},{label:"数据完整度",value:"动态",delta:"已知状态 / 纳管总数",tone:"warn"}],bars:[0,0,0,0,0,0,0],insights:["本站当前有哪些故障和告警设备?","哪些设备离线,集中在哪个层级?","当前数据能否发布正式日报?"],sources:["资产组清单","节点清单与当前状态","报警流程/工单/保养(待接入)","SOP 知识库(待接入)"],formula:"问题识别 → 当前资产组证据筛选 → 确定性统计 → AI/本地规则回答",maturity:"节点状态问答首版已实现"},
+  {key:"period",section:"运营分析",icon:"环",title:"运行环比",short:"按真实历史事件比较本月截至当前与上月同期",tone:"violet",visual:"compare",summary:"页面只在历史事件成功返回、可映射到当前资产组节点且时间字段有效时生成环比;否则显示数据缺口,不展示示例结论。",metrics:[{label:"事件影响可用率(估算)",value:"动态",delta:"同长度周期"},{label:"历史事件",value:"动态",delta:"当前资产组事件"},{label:"影响节点",value:"动态",delta:"按节点去重"},{label:"事件影响时长",value:"动态",delta:"事件持续时间合计"}],bars:[0,0,0,0,0,0,0],insights:["历史事件必须与当前资产组节点身份匹配","本月截至当前与上月同期采用相同窗口时长","能耗、工单和保养未接入,不参与本页环比"],sources:["资产组与节点范围","历史事件","事件持续时间","资产层级映射"],formula:"同长度窗口 + 资产组节点过滤 + 事件确定性统计 + AI 解释",maturity:"框架已实现 · 历史事件待有效认证实测"},
+  {key:"benchmark",section:"运营分析",icon:"标",title:"同类场站对标",short:"按容量、负荷和场景归一化横向比较",tone:"blue",visual:"compare",summary:"本站综合表现位于同类 AIDC 场站前 35%,可用率领先,但制冷能效与运维响应仍有改进空间。",metrics:[{label:"同类排名",value:"7/20",delta:"前 35%",tone:"good"},{label:"可用率分位",value:"P82",delta:"领先同类",tone:"good"},{label:"能效分位",value:"P46",delta:"低于标杆",tone:"warn"},{label:"运维分位",value:"P61",delta:"中上水平"}],bars:[68,76,55,82,63,71,77],insights:["以 IT 负载、气候区和设计等级筛选同类站","标杆站制冷系统单位能耗低 8.6%","避免用设备绝对数量直接排名"],sources:["多场站快照","设计容量","负荷/处理量","天气与场站标签"],formula:"同类型分组 + 单位产出归一化 + 分位数排名",maturity:"需统一主数据口径"},
+  {key:"maintenance",section:"运营分析",icon:"维",title:"运维履约",short:"比较计划、响应、关闭和维护后效果",tone:"orange",visual:"operations",summary:"今日计划任务完成率 80%,两张工单存在延期风险;近 30 日一次修复率较上期提升 6%。",metrics:[{label:"计划完成率",value:"80%",delta:"8 / 10",tone:"warn"},{label:"SLA 按时率",value:"92%",delta:"较上期 +4%",tone:"good"},{label:"一次修复率",value:"84%",delta:"较上期 +6%",tone:"good"},{label:"延期风险",value:"2",delta:"22:00 前到期",tone:"bad"}],bars:[58,66,73,69,78,84,92],insights:["WO-20260822-17 距 SLA 仅剩 42 分钟","维护后 24 小时报警下降 31%","夜班巡检还有 2 项未提交"],sources:["工单流转","巡检计划","保养记录","关联报警"],formula:"响应/到场/修复/关闭时长 + 维护前后效果",maturity:"接入运维模块即可落地"},
+  {key:"health",section:"风险与策略",icon:"健",title:"场站运营健康度",short:"六维评分并解释场站级扣分来源",tone:"green",visual:"health",summary:"本站健康度 86 分,运行可用性良好;主要扣分来自能效偏离、未闭环事件和两项逾期风险。",metrics:[{label:"综合健康度",value:"86",delta:"等级:良好",tone:"good"},{label:"运行可用性",value:"94",delta:"权重 30%",tone:"good"},{label:"报警风险",value:"78",delta:"权重 20%",tone:"warn"},{label:"运维履约",value:"88",delta:"权重 20%",tone:"good"}],bars:[91,89,87,84,82,85,86],insights:["能效与产出维度扣 5.4 分","报警风险维度扣 4.1 分","评分均可下钻至系统和原始证据"],sources:["运行可用性","报警风险","运维履约","能效与数据质量"],formula:"六维规则评分,按场站类型配置权重",maturity:"可先规则化上线"},
+  {key:"prediction",section:"风险与策略",icon:"预",title:"场站风险预测",short:"预测未来 24—72 小时的运行影响",tone:"violet",visual:"forecast",summary:"未来 24 小时整体风险可控;明日 14:00—17:00 高负荷时段,冷却能力裕度可能降至 12%。",metrics:[{label:"24h 综合风险",value:"中低",delta:"置信度 82%",tone:"good"},{label:"高风险窗口",value:"1",delta:"明日 14:00—17:00",tone:"warn"},{label:"能力最低裕度",value:"12%",delta:"建议阈值 15%",tone:"bad"},{label:"数据完整度",value:"96%",delta:"模型输入良好",tone:"good"}],bars:[28,31,36,48,63,71,54,42],insights:["高温与 IT 负荷共同压缩制冷裕度","未完成保养使风险概率增加 7%","建议提前确认备用冷机可用状态"],sources:["历史时序","天气/负荷","报警密度","未完工单"],formula:"趋势基线 + 时序预测 + 约束规则联合判断",maturity:"需积累 1—3 个月"},
+  {key:"alarm",section:"风险与策略",icon:"警",title:"报警事件治理",short:"将报警风暴归并为可处置的场站事件",tone:"red",visual:"alarm",summary:"过去 2 小时的 27 条报警已归并为 3 个场站事件,压缩率 88.9%;其中 1 个事件尚未闭环。",metrics:[{label:"原始报警",value:"27",delta:"过去 2 小时"},{label:"归并事件",value:"3",delta:"压缩 88.9%",tone:"good"},{label:"未闭环事件",value:"1",delta:"优先级 P2",tone:"warn"},{label:"平均确认时长",value:"6m",delta:"较昨日 -3m",tone:"good"}],bars:[22,48,83,61,34,27,18],insights:["冷却水能力下降:关联 16 条报警","通信质量波动:关联 8 条报警","环境温度短时越限:关联 3 条报警"],sources:["报警历史","资产拓扑","测点相关性","确认/关闭记录"],formula:"时间窗口 + 拓扑关系 + 因果顺序进行事件聚类",maturity:"规则归并可立即实施"},
+  {key:"efficiency",section:"运营分析",icon:"效",title:"资源与能效",short:"用单位产出指标发现场站级优化空间",tone:"green",visual:"efficiency",summary:"本站单位 IT 负荷制冷能耗较基线高 6.2%,冷却塔和二次泵合计贡献了 71% 的偏差。",metrics:[{label:"综合能效指数",value:"93.8",delta:"低于基线 6.2%",tone:"warn"},{label:"今日 PUE",value:"1.36",delta:"目标 1.32",tone:"warn"},{label:"可节约空间",value:"4.1%",delta:"约 860 kWh/日",tone:"good"},{label:"数据覆盖率",value:"91%",delta:"能源表计完整",tone:"good"}],bars:[74,79,83,88,92,86,81],insights:["二次泵频率在低负荷时段偏高","冷却塔逼近温度较基线增加 1.4℃","优化需保持温度、冗余和启停次数约束"],sources:["能源表计","负荷/处理量","天气数据","运行模式"],formula:"总资源消耗 ÷ 有效产出,并按工况建立基线",maturity:"需接入产出与能源数据"},
+  {key:"capacity",section:"风险与策略",icon:"荷",title:"负荷与能力预测",short:"预测需求、产出和场站运行裕度",tone:"blue",visual:"capacity",summary:"明日峰值负荷预计达到设计能力的 84%,总体容量充足;高温时段建议预留至少一套冗余系统。",metrics:[{label:"明日峰值负荷",value:"84%",delta:"14:30 预计峰值",tone:"warn"},{label:"当前能力裕度",value:"23%",delta:"满足 N+1",tone:"good"},{label:"预测误差",value:"±4.8%",delta:"近 30 日回测",tone:"good"},{label:"受限时段",value:"3h",delta:"高温重载窗口",tone:"warn"}],bars:[48,55,61,70,84,78,66,54],insights:["天气升温预计带来 9% 冷量需求增量","峰时能力仍满足安全约束","建议 13:30 前完成备用系统检查"],sources:["历史负荷","设计能力","天气/日历","上下游需求"],formula:"多变量负荷预测 + 容量与冗余约束",maturity:"三个月数据可形成初版"},
+  {key:"strategy",section:"风险与策略",icon:"策",title:"场站运行策略建议",short:"从协同运行角度生成可审批的优化方案",tone:"cyan",visual:"strategy",summary:"建议在低负荷窗口调整冷源组合并小步提高供水设定值,预计节能 3.2%,同时维持冗余与温度边界。",metrics:[{label:"预计节能",value:"3.2%",delta:"约 670 kWh/日",tone:"good"},{label:"风险等级",value:"低",delta:"约束校验通过",tone:"good"},{label:"建议窗口",value:"22:00",delta:"低负荷时段"},{label:"审批状态",value:"待审批",delta:"仅展示,不执行",tone:"warn"}],bars:[82,78,70,63,58,55,57],insights:["建议对象是场站运行组合,不是单设备控制","执行链路:建议→仿真→审批→分步执行→回退","燃气站首期仅提供建议,不进入自动执行"],sources:["实时测点","运行边界","控制状态","指令审计"],formula:"约束优化/仿真产生方案,AI 负责解释收益与风险",maturity:"建议模式可先落地"},
+  {key:"emergency",section:"风险与策略",icon:"应",title:"应急辅助与复盘",short:"事件影响、SOP、处置进度与复盘闭环",tone:"red",visual:"emergency",summary:"当前无一级应急事件。最近一次事件持续 38 分钟,影响冷却冗余但未影响核心业务,处置过程形成 3 项改进任务。",metrics:[{label:"当前应急事件",value:"0",delta:"运行平稳",tone:"good"},{label:"最近事件时长",value:"38m",delta:"已闭环"},{label:"SOP 执行率",value:"100%",delta:"8 / 8 步",tone:"good"},{label:"改进任务",value:"3",delta:"1 项待完成",tone:"warn"}],bars:[18,62,88,74,49,31,20],insights:["10:15 事件触发并完成影响分析","10:21 启用备用运行路径","10:53 指标恢复并进入观察期"],sources:["事件时间线","SOP 知识库","控制记录","工单与人员"],formula:"拓扑影响分析 + SOP 检索 + 处置过程自动归档",maturity:"检索与摘要可先上线"},
+];
+
+// 2026-08-22 从用户已登录的 Demo Org / 水务资产组页面读取的可见设备快照。
+// 源页面显示总计 25 台;首屏可追溯到以下 20 台,剩余 5 台由连接后的节点 API 自动补齐。
+const waterGroupSnapshot: Device[] = [
+  {id:"water-01",name:"水泵1",site:"良泉水厂 / 一号泵站",type:"水泵",status:"告警",score:68,alarm:"源站当前状态:告警",metric:"—"},
+  {id:"water-02",name:"水泵2",site:"良泉水厂 / 一号泵站",type:"水泵",status:"告警",score:68,alarm:"源站当前状态:告警",metric:"—"},
+  {id:"water-03",name:"水箱1",site:"良泉水厂 / 一号泵站",type:"水箱",status:"运行",score:92,alarm:"无",metric:"—"},
+  {id:"water-04",name:"水箱2",site:"良泉水厂 / 一号泵站",type:"水箱",status:"运行",score:92,alarm:"无",metric:"—"},
+  {id:"water-05",name:"水阀1",site:"良泉水厂 / 一号泵站",type:"阀门",status:"运行",score:91,alarm:"无",metric:"—"},
+  {id:"water-06",name:"水阀2",site:"良泉水厂 / 一号泵站",type:"阀门",status:"运行",score:91,alarm:"无",metric:"—"},
+  {id:"water-07",name:"压力表1",site:"良泉水厂 / 一号泵站",type:"仪表",status:"告警",score:70,alarm:"源站当前状态:告警",metric:"—"},
+  {id:"water-08",name:"气罐1",site:"良泉水厂 / 一号泵站",type:"气罐",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"water-09",name:"流量计1",site:"良泉水厂 / 一号泵站",type:"仪表",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"water-10",name:"控制柜1",site:"良泉水厂 / 一号泵站",type:"控制柜",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"water-11",name:"水质分析仪1",site:"良泉水厂 / 一号泵站",type:"水质仪表",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"water-12",name:"配电柜",site:"良泉水厂 / 一号泵站",type:"配电",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"water-13",name:"水表",site:"良泉水厂 / 一号泵站",type:"仪表",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"water-14",name:"电表",site:"良泉水厂 / 一号泵站",type:"仪表",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"water-15",name:"水泵3",site:"良泉水厂 / 一号泵站",type:"水泵",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"water-16",name:"水泵3_1ikvuvsvk",site:"良泉水厂 / 一号泵站",type:"水泵",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"water-17",name:"machine",site:"良泉水厂 / 一号泵站",type:"其他",status:"未知",score:75,alarm:"源站未显示状态",metric:"—"},
+  {id:"water-18",name:"水箱1_1j69i1emc",site:"良泉水厂 / 一号泵站",type:"水箱",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"water-19",name:"1#卧式泵",site:"良泉水厂 / 二号泵站",type:"水泵",status:"故障",score:42,alarm:"源站当前状态:故障",metric:"—"},
+  {id:"water-20",name:"2#卧式泵",site:"良泉水厂 / 二号泵站",type:"水泵",status:"运行",score:90,alarm:"无",metric:"—"},
+];
+
+// 2026-08-22 从用户已登录的 Demo Org / SMT车间页面逐行核对的 20 台设备。
+const smtGroupSnapshot: Device[] = [
+  {id:"smt-01",name:"上板机",site:"SMT车间 / 1#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-02",name:"锡膏印刷机",site:"SMT车间 / 1#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-03",name:"高速贴片机FUJI",site:"SMT车间 / 1#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-04",name:"高速贴片机_FUJJ",site:"SMT车间 / 1#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-05",name:"回流焊炉(报警分析)",site:"SMT车间 / 1#产线",type:"SMT设备",status:"告警",score:68,alarm:"源站当前状态:告警",metric:"—"},
+  {id:"smt-06",name:"AOI",site:"SMT车间 / 1#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-07",name:"下板机",site:"SMT车间 / 1#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-08",name:"双轨上板机",site:"SMT车间 / 2#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-09",name:"专用印刷机",site:"SMT车间 / 2#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-10",name:"微米贴片机",site:"SMT车间 / 2#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-11",name:"真空回流焊炉",site:"SMT车间 / 2#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-12",name:"高分辨率AOI",site:"SMT车间 / 2#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-13",name:"X-Ray检测机",site:"SMT车间 / 2#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-14",name:"自动上板机-1",site:"SMT车间 / 3#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-15",name:"自动上板机-2",site:"SMT车间 / 3#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-16",name:"双轨印刷机",site:"SMT车间 / 3#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-17",name:"双通道回流焊炉",site:"SMT车间 / 3#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-18",name:"高速AOI-1",site:"SMT车间 / 3#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-19",name:"高速AOI-2",site:"SMT车间 / 3#产线",type:"SMT设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"smt-20",name:"轨迹模拟",site:"SMT车间",type:"站点节点",status:"运行",score:90,alarm:"无",metric:"—"},
+];
+
+// PMS 源页确认共 61 台;以下为当前第一页可追溯的 20 台,剩余 41 台由节点 API 补齐。
+const pmsGroupSnapshot: Device[] = [
+  {id:"滚压机1-3_1ie3beb4p2",name:"滚压机1-3",site:"A部门 / A五产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"滚压机1-4_1ie3beb4p1",name:"滚压机1-4",site:"A部门 / A五产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"machine_1i9qo1tmi",name:"滚压机1-1",site:"A部门 / 一产线",type:"PMS设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"machine_1i9qo22",name:"滚压机1-2",site:"A部门 / 一产线",type:"PMS设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"machine_1i9qo2596",name:"滚压机1-3",site:"A部门 / 一产线",type:"PMS设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"machine_1i9qo281j",name:"滚压机1-4",site:"A部门 / 一产线",type:"PMS设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"machine_1i9qo29b5",name:"滚压机1-5",site:"A部门 / 一产线",type:"PMS设备",status:"运行",score:90,alarm:"无",metric:"—"},
+  {id:"滚压机1-3_1ie3bdqko4",name:"滚压机1-3",site:"A部门 / A四产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"滚压机1-4_1ie3bdqko3",name:"滚压机1-4",site:"A部门 / A四产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"滚压机1-5_1ie3bdqko2",name:"滚压机1-5",site:"A部门 / A四产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"滚压机1-1_1ie3bdqko1",name:"滚压机1-1",site:"A部门 / A四产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"滚压机1-2_1ie3bdqko0",name:"滚压机1-2",site:"A部门 / A四产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"machine_1i9qo5lje",name:"刨渣机2-1",site:"A部门 / 二产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"刨渣机2-21i9qo7bjc",name:"刨渣机2-2",site:"A部门 / 二产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"刨渣机2-31i9qo7jkd",name:"刨渣机2-3",site:"A部门 / 二产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"滚压机1-3_1ie3bcv3e2",name:"滚压机1-3",site:"A部门 / 三产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"滚压机1-4_1ie3bcv3e1",name:"滚压机1-4",site:"A部门 / 三产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"滚压机1-5_1ie3bcv3e0",name:"滚压机1-5",site:"A部门 / 三产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"滚压机1-1_1ie3bcv3e4",name:"滚压机1-1",site:"A部门 / 三产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+  {id:"滚压机1-2_1ie3bcv3e3",name:"滚压机1-2",site:"A部门 / 三产线",type:"PMS设备",status:"离线",score:55,alarm:"通信/在线状态异常",metric:"—"},
+];
+
+const snapshotAssetGroups: AssetGroup[] = [
+  {id:"snapshot-smt",name:"SMT车间",deviceCount:20,orgName:"Demo Org",source:"snapshot"},
+  {id:"snapshot-pms",name:"PMS",deviceCount:61,orgName:"Demo Org",source:"snapshot"},
+  {id:"snapshot-water",name:"水务资产组",deviceCount:25,orgName:"良泉水厂",source:"snapshot"},
+];
+
+function snapshotDevicesForGroup(groupName?:string): Device[] {
+  if (groupName === "SMT车间") return smtGroupSnapshot;
+  if (groupName === "PMS") return pmsGroupSnapshot;
+  if (groupName === "水务资产组") return waterGroupSnapshot;
+  return [];
+}
+
+const apiCatalog: Record<ApiKey,ApiDefinition> = {
+  groups:{label:"资产组 / 场站清单",method:"GET",path:"{APM}/api/v1/asset/group/list?count=1000",status:"verified",note:"Demo 连接实测 HTTP 200;资产组作为场站主键和查询入口。"},
+  nodes:{label:"场站节点清单",method:"GET",path:"{APM}/api/v1/asset/node/list",status:"source",note:"源码确认参数 groupName、groupId、index、count;需按已选资产组发起请求。"},
+  nodeStatus:{label:"节点当前状态",method:"GET",path:"{APM}/api/v1/asset/node/status/ids",status:"source",note:"源码确认按节点 ID 查询;状态枚举需和 MU 页面口径映射。"},
+  properties:{label:"节点属性与测点",method:"GET",path:"{APM}/api/v1/node/property/byid/list",status:"source",note:"源码确认支持 nodeIds、componentName、name、queryType、分页参数。"},
+  latest:{label:"实时测点",method:"POST",path:"{APM}/api/v1/latest/data",status:"source",note:"源码确认;请求体需从源站成功请求中固化测点 ID、聚合和时间口径。"},
+  history:{label:"历史测点",method:"POST",path:"{APM}/api/v1/hist/raw/data 或 /api/v1/hist/span/data",status:"source",note:"源码确认原始值与时间聚合两类接口;区间、步长和缺失值规则待映射。"},
+  events:{label:"报警历史 / 当前事件",method:"GET",path:"{APM}/api/v1/event/hist 或 /api/v1/event/latest",status:"source",note:"路径与查询参数已从源模块确认;运行环比当前调用 hist,但仍需在有效认证下验证 HTTP 成功、返回结构、时间字段及资产组节点映射。"},
+  alarmEvents:{label:"报警流程事件列表",method:"POST",path:"{alarm-process}/api/v1/event/list",status:"source",note:"源码确认;用于事件状态、级别、确认与闭环时间。网关基址待抓包映射。"},
+  alarmProcess:{label:"报警处置日志",method:"GET/POST",path:"{alarm-process}/api/v1/event/process/log",status:"source",note:"源码确认单条/批量日志及 process 接口;用于还原处置链。"},
+  workOrders:{label:"工单列表与状态",method:"POST",path:"{api-ticket}/api/v1/tickets",status:"source",note:"源码确认;服务网关地址、工单状态和 SLA 字段待从成功请求映射。"},
+  maintenance:{label:"保养计划关联工单",method:"POST",path:"{api-ticket}/api/v1.0/audit/maintenance/schedules/tickets",status:"source",note:"源码确认;用于计划、到期、完成和维护效果关联。"},
+  stationParams:{label:"场站关键参数",method:"GET",path:"{m2i}/asset/station/keyparameters/byids",status:"source",note:"源码确认;用于配置容量、边界、关键测点及场站口径。"},
+  controlLogs:{label:"控制指令与状态日志",method:"POST",path:"{rest-bridge}/api/v1/reversecontrol/log",status:"source",note:"源码确认 log、status、exec、batch;首期只读,用于审计和效果验证。"},
+  schedules:{label:"智能控制排程",method:"POST",path:"{api-m2icore}/api/v1.0/intelli-control/schedules/list",status:"source",note:"源码确认排程列表、状态和详情接口;不代表 Demo 获得执行权限。"},
+  execution:{label:"控制执行记录",method:"POST",path:"{api-m2icore}/api/v1.0/intelli-control/execution-records",status:"source",note:"源码确认;用于建议、审批、执行与回退的闭环审计。"},
+  external:{label:"外部与经营主数据",method:"待接入",path:"天气 / 电价 / 处理量或负荷 / 设计容量 / 班次人员 / SOP 知识库",status:"external",note:"当前 MU 已确认接口不能完整提供,需由场站侧数据平台或主数据服务补齐。"},
+};
+
+const scenarioDesigns: Record<ScenarioKey,ScenarioDesign> = {
+  daily:{feasibility:"首版已经实现。资产组可切换,节点清单返回后会重新统计当前范围;水务、SMT、PMS 的源站可见快照只用于接口未返回明细时保留已确认事实,并明确标注数据模式。",sourceText:"以当前资产组作为场站边界,只使用该组节点清单和节点当前状态计算纳管、在线、离线、告警、故障及状态完整度;不会跨资产组复用数据。",apiKeys:["groups","nodes","nodeStatus"],gaps:"报警流程闭环、工单、保养、关键测点和按日历史尚未接入,因此当前日报不生成未处理报警数、工单数、保养数和趋势结论。",kpiCalcs:["优先取当前资产组节点清单长度;若接口明细未返回,只显示源站已确认的资产组设备总数并标注快照模式。","在线数 = 当前资产组中除离线及未知外的节点数;同时单列明确为运行/正常的节点数和状态覆盖数。","离线数 = 当前资产组节点状态映射为“离线”的记录数,不把未知状态计为离线。","分别统计当前节点状态为“告警”和“故障”的记录;该值不是报警流程事件条数。"],visualPanel:"站点智能日报与异常证据",visualCalc:"以当前节点状态生成今日结论、运行健康估算和状态分布;异常清单按故障→告警→离线排序,最多展示 12 条可追溯节点证据。",insightCalc:"将确定性统计与异常节点作为上下文,按事实、场站影响、建议动作生成三条关注事项;提示词可查看、编辑并重新生成。",summaryCalc:"汇总纳管、运行、告警、故障、离线和未知状态,并明确列出尚未接入的数据域;模型不可补写缺失数字。"},
+  qa:{feasibility:"节点状态问答首版已经实现。切换资产组后,问题、回答和证据会清空并重新绑定当前范围;回答可由 AI 服务生成,也可回退到本地确定性规则。",sourceText:"问题始终绑定当前资产组 id/name;当前只把资产组、节点清单、节点状态统计和最多 8 条相关节点作为回答证据。",apiKeys:["groups","nodes","nodeStatus"],gaps:"报警流程闭环、工单、保养、历史趋势、测点值和 SOP 尚未接入,遇到这些问题应明确说明数据不足,不能把源站演示页面内容当作答案。",kpiCalcs:["使用资产组 id/name 锁定全部上下文;资产组切换时清空上一组的问答状态。","已加载证据 = 当前资产组实际载入的节点记录数;纳管总数单独显示。","当前可回答域为资产组与节点状态,共 2/5;其余域在上下文中标记为 unavailableDomains。","数据完整度 = (纳管总数 − 未知状态数) ÷ 纳管总数 × 100%;无纳管基数时显示缺口。"],visualPanel:"向当前场站提问",visualCalc:"识别故障、告警、离线、运行和日报可发布性等意图,筛选当前资产组节点并形成带数据模式、API 路径和节点清单的回答。",insightCalc:"推荐问题只覆盖当前已具备的节点状态域;相关证据按问题关键词过滤,最多展示 8 条。",summaryCalc:"回答包含场站范围、确定性统计、证据节点、数据来源和未接入域;AI 服务不可用时使用同一证据生成本地规则答案。"},
+  period:{feasibility:"真实数据框架已经实现,但正式环比结论是有条件的:必须取得有效认证,历史事件请求成功,返回记录可映射到当前资产组节点,且具备可解析的开始/结束时间。当前不以源站“综合数据分析”的演示内容作为可行性证据。",sourceText:"先用资产组和节点清单建立当前场站边界,再分别查询本月截至当前与上月同期的历史事件,只保留可归属于当前资产组节点的记录。",apiKeys:["groups","nodes","events"],gaps:"历史事件接口仍需在有效认证下完成实测和字段校准;事件重叠尚未去重。能耗、产出、工单、保养及真实运行时长均未接入,不参与当前页面。",kpiCalcs:["事件影响可用率(估算) = 1 − 事件影响时长 ÷(纳管节点数 × 比较窗口小时);本月值减上月值显示百分点变化。","分别统计两个同长度窗口内、可归属当前资产组的历史事件记录数;事件量环比 = (本期 − 基期) ÷ 基期。","影响节点数 = 当前周期出现历史事件的节点 ID 去重数;无法映射的事件不得归入当前资产组。","事件影响时长 = 每条事件结束时间减开始时间后求和;仍未结束的事件截至窗口结束,重叠事件未去重时结果可能偏保守。"],visualPanel:"本月与上月同期事件对比",visualCalc:"展示事件影响可用率估算、历史事件、影响节点、影响时长四项本期/基期数据;无有效记录时显示数据缺口而不是示例值。",insightCalc:"按本月事件数量汇总场站层级贡献,并列出高频影响节点;只有通过资产组过滤的事件才能进入排名。",summaryCalc:"将两个窗口、四项确定性指标、层级贡献和高频节点交给 AI 解释;请求失败或映射不足时停止生成结论并展示请求轨迹。"},
+  benchmark:{feasibility:"有条件可做。MU 可提供站内数据,但横向对标还需要多场站统一主数据、设计容量、负荷和场站标签。",sourceText:"从多个资产组抽取同口径指标,按场站类型、容量、气候区和运行模式分群后再比较。",apiKeys:["groups","nodes","history","events","workOrders","stationParams","external"],gaps:"需要建立跨场站指标版本、容量/等级标签和数据授权;样本太少时不展示排名。",kpiCalcs:["本站综合标准分在同类有效样本中的降序排名;同时显示样本量。","以可用率在同类分布中的百分位 P 值表示,检修时段按统一规则处理。","以单位产出能耗反向计算百分位;越低越优,需控制气候与负荷差异。","由 SLA 按时率、一次修复率、逾期率加权后计算同类百分位。"],visualPanel:"同类场站标准化对比",visualCalc:"先按场站标签筛同类样本,再把四类指标转为分位数或标准分,不直接比较设备数量。",insightCalc:"计算本站与 P75 标杆的差距,并定位最有改进空间的系统和运营环节。",summaryCalc:"解释同类样本选择、本站位置、主要优势和差距,禁止在样本不足时生成虚假排名。"},
+  maintenance:{feasibility:"接口层面可做。工单、流程和保养计划路径已确认,仍需映射部署网关与状态字段。",sourceText:"以工单和保养计划为主数据,关联报警事件及维护前后关键测点,评价计划、响应、处置、验证四段履约。",apiKeys:["workOrders","maintenance","alarmEvents","alarmProcess","history","nodes"],gaps:"需取得 api-ticket 网关地址,确认计划时间、响应、到场、完成、关闭和责任班组字段。",kpiCalcs:["今日已完成计划任务数 ÷ 今日应完成计划任务数;取消任务单列。","在 SLA 截止前达到指定流程状态的工单数 ÷ 应考核工单数。","一次处置后在观察窗内未重开/未重复报警的工单数 ÷ 已完成工单数。","根据剩余 SLA、当前状态、历史同类耗时和责任资源预测未来 24 小时逾期数。"],visualPanel:"今日运维履约漏斗",visualCalc:"按计划→响应→处置→效果验证统计同一批任务的留存数,并展示每一步流失原因。",insightCalc:"优先提示临近 SLA、缺责任人、处置后复发和巡检未提交的任务。",summaryCalc:"汇总完成率、SLA、一次修复和高风险任务,给出责任环节而非归责到单台设备。"},
+  health:{feasibility:"规则版可做。已有资产、状态、报警、工单和测点即可形成首版;能效与安全维度需补场站参数。",sourceText:"从场站运行可用性、报警风险、运维履约、能效产出、数据可信度、安全合规六个维度评分。",apiKeys:["nodes","nodeStatus","latest","history","alarmEvents","workOrders","maintenance","stationParams","external"],gaps:"权重、阈值和红线需按燃气站、水站、污水、新能源、AIDC 分类型配置并由业务确认。",kpiCalcs:["六维得分按场站类型权重加权;任一安全红线触发时总等级封顶。","在线时长、关键系统冗余满足率和计划停机规则组合评分。","以未闭环级别、持续时间、影响范围和重复率计算 0—100 反向得分。","由 SLA 按时率、计划完成率、一次修复率和逾期率合成。"],visualPanel:"六维运营健康度",visualCalc:"每维 0—100 分,展示权重、扣分项和原始证据;不以模型黑盒分数替代规则。",insightCalc:"按总分扣分贡献排序,并把设备证据归并成场站系统级问题。",summaryCalc:"解释总等级、前三项扣分来源及可改善动作,同时输出数据完整性。"},
+  prediction:{feasibility:"需要数据积累后可做。当前接口具备时序、事件和工单输入,但至少需 1—3 个月连续数据和故障/影响标签。",sourceText:"融合关键测点趋势、报警密度、未完工单、天气与负荷预测未来 24—72 小时场站影响。",apiKeys:["history","latest","events","workOrders","maintenance","stationParams","external"],gaps:"需沉淀高风险窗口标签、场站能力边界、天气/需求预测和历史干预结果;先规则预警再训练模型。",kpiCalcs:["对未来 24 小时每个时间片计算风险概率,再按最高影响等级映射为低/中/高。","连续超过风险阈值且持续达到最小时长的时间段合并为窗口。","(可用能力-预测需求)÷可用能力;考虑检修、冗余和环境降额。","模型所需有效时间点数 ÷ 应有时间点数,并对关键变量设最低覆盖要求。"],visualPanel:"未来 24—72 小时风险曲线",visualCalc:"规则阈值、时序预测和场站约束联合输出风险区间,并显示置信带与关键驱动因素。",insightCalc:"按风险增量贡献排序天气、负荷、工单和测点趋势,给出可验证的提前动作。",summaryCalc:"描述风险窗口、影响系统、置信度和建议,不把预测概率表述为确定故障。"},
+  alarm:{feasibility:"规则归并可先落地。报警历史、流程事件和资产关系均有对应接口;因果模型需后续数据验证。",sourceText:"在当前场站内按时间窗口、资产拓扑、相同根因特征和恢复顺序,把重复报警归并为可处置事件。",apiKeys:["events","alarmEvents","alarmProcess","nodes","properties","history"],gaps:"需映射 alarm-process 网关、事件状态及关联规则;首期规则结果应允许人工拆分/合并。",kpiCalcs:["统计窗口内原始报警记录数,按消息唯一键去重。","对原始报警执行时间+拓扑+规则聚类后的事件簇数量。","状态未关闭且仍有影响的事件簇数量,不等同于未恢复报警条数。","已确认事件的 ackTs-createTs 平均值,并同时保留 P50/P90。"],visualPanel:"报警归并为场站事件",visualCalc:"按 5—15 分钟滑动窗口建立关联图,再结合上下游拓扑与状态传播顺序形成事件簇。",insightCalc:"按影响系统、事件等级、持续时间和未闭环状态计算优先级。",summaryCalc:"以事件为单位说明影响、可能关联和处置进度,不对尚无证据的根因作确定判断。"},
+  efficiency:{feasibility:"有条件可做。历史测点可读取,但必须明确能源表计、有效产出/负荷、天气和运行模式。",sourceText:"聚合场站能源表计,并以供水量、处理量、发电量、输气量或 IT 负荷作为有效产出建立同工况基线。",apiKeys:["properties","history","latest","stationParams","external"],gaps:"需建立能源表与产出表的测点映射、计量边界、倍率、缺数补齐和气候/负荷归一化规则。",kpiCalcs:["实际单位产出能耗 ÷ 同工况基线 ×100 的反向指数;页面中 100 为基准。","AIDC:场站总耗电 ÷ IT 负载耗电;其他站型替换为对应行业核心能效指标。","在安全与产出约束下,基线能耗减优化模拟能耗,再除以基线能耗。","有有效质量标记的能源与产出采样点 ÷ 应有采样点。"],visualPanel:"单位产出能耗与偏差贡献",visualCalc:"用可解释基线估算期望能耗,实际与期望之差按系统分摊贡献,不跨计量边界重复累计。",insightCalc:"按可节约量、持续时间和操作风险排序场站系统级优化空间。",summaryCalc:"说明偏差、主要贡献系统、潜在收益及约束条件;节能值标注为估算而非结算。"},
+  capacity:{feasibility:"预测原型可做,但正式结论依赖设计容量、运行降额、需求/产出和天气等外部数据。",sourceText:"使用历史负荷和日历天气预测需求,再与场站设计能力、当前可用能力和冗余要求比较。",apiKeys:["history","latest","nodeStatus","stationParams","maintenance","external"],gaps:"需配置设计能力曲线、N+1/N+2 规则、环境降额、计划检修和上下游需求数据。",kpiCalcs:["未来 24 小时预测需求最大值 ÷ 对应时刻可用能力,显示峰值时间。","(当前可用能力-当前需求-必需冗余)÷当前可用能力。","近 30 日滚动回测 MAPE;低负荷和零值场景使用 WAPE 辅助。","预测需求超过安全能力阈值的连续时段总小时数。"],visualPanel:"需求与能力裕度预测",visualCalc:"多变量预测产生需求曲线,叠加可用能力、降额和冗余线,识别受限窗口。",insightCalc:"量化天气、计划检修和需求变化对最小裕度的贡献,并提示提前准备时间。",summaryCalc:"说明峰值、最小裕度、受限窗口、误差和建议,不将设计容量等同实时可用能力。"},
+  strategy:{feasibility:"建议模式可以先做,自动执行暂不建议。实时/历史测点和控制日志可支撑方案计算与事后验证。",sourceText:"围绕场站级协同目标生成运行组合建议,读取控制状态和边界;经仿真、审批后才可进入执行网关。",apiKeys:["latest","history","stationParams","controlLogs","schedules","execution","external"],gaps:"需建立控制点白名单、联锁/回退条件、变更窗口、审批责任和数字仿真;燃气站首期仅建议。",kpiCalcs:["基线能耗减策略仿真能耗,再除以基线能耗;同时校验产出和安全约束。","取约束违规概率、模型不确定性和回退可用性中的最不利等级。","在预测低负荷、无计划变更、数据完整且人员可用的时段中选最早窗口。","读取建议审批流程状态;没有明确授权时始终保持“待审批/仅展示”。"],visualPanel:"场站协同运行建议",visualCalc:"约束优化生成候选组合,依次通过收益预测、边界校验、仿真、人工审批和执行验证。",insightCalc:"比较候选策略的收益、风险、影响范围和可回退性,输出推荐顺序。",summaryCalc:"解释场站级收益与边界,不突出单设备优化;任何执行动作都必须有独立授权和审计。"},
+  emergency:{feasibility:"应急检索与复盘可先做;实时指挥必须由人员负责。事件、处置日志、控制记录和工单可还原时间线。",sourceText:"把场站事件、报警处置、控制审计、SOP 步骤和后续工单按统一时间轴关联。",apiKeys:["alarmEvents","alarmProcess","events","controlLogs","execution","workOrders","external"],gaps:"需接入正式 SOP、应急等级、岗位通讯录和人工确认记录;AI 不替代应急指挥与联锁。",kpiCalcs:["当前状态为处置中/观察中且达到应急等级阈值的事件数。","最近一次已闭环事件的 closeTs-startTs,暂停/观察时段按制度定义。","有完成证据且顺序符合要求的 SOP 步骤数 ÷ 应执行步骤数。","事件关闭后创建的复盘改进工单数,并按待办/进行中/完成分组。"],visualPanel:"场站事件处置时间线",visualCalc:"按时间戳合并报警、人工确认、控制动作、指标恢复和工单,形成可审计时间线。",insightCalc:"识别响应间隔过长、步骤缺失、指标反复和未落实改进项。",summaryCalc:"生成影响范围、关键处置、恢复依据和改进任务摘要;未记录动作不做推断。"},
+};
+
+const pageNarratives: Record<ScenarioKey,PageNarrative> = {
+  daily:{audience:"场站负责人、值班主管、区域运营负责人和需要快速查看场站现状的企业管理者。",value:"把当前资产组的纳管规模、运行状态、异常节点和数据完整度整理成一页可追溯日结,减少手工点查和口头汇报;未接入内容会明确显示,不制造完整但不可靠的日报。",whyAI:"节点统计可以由规则完成,但管理者更需要场站影响、优先顺序和建议动作。AI 用于把已确认事实转成不同角色易读的摘要,且允许查看并人工优化提示词后重新生成。",feasibilityEvidence:"当前页面已经能切换水务、SMT、PMS 等资产组,并按当前组加载节点清单/状态或源站已确认快照,实时计算数量与异常证据。现阶段可形成节点状态版日报;报警闭环、工单、保养和历史趋势尚未接入,设计说明不把这些域列为已实现。"},
+  qa:{audience:"值班人员、场站负责人、区域支持团队和希望快速了解本站现状的企业管理者。",value:"把资产组筛选、节点状态统计和异常查找变成自然语言问答;用户可以快速知道本站有多少节点、哪些故障/告警/离线,以及当前数据能否支持正式日报。",whyAI:"同一事实会被不同角色用不同问法提出。AI 负责理解问题、组织场站级答案和解释证据;数量、状态分布与数据完整度仍由确定性规则计算,AI 服务不可用时可回退本地回答。",feasibilityEvidence:"当前页面已经实现资产组切换、节点证据加载、快捷问题、答案生成、相关节点筛选和 API 证据展示。可行性来自已经接线的资产组与节点数据,不来自尚未验证的历史、报警流程、工单或 SOP 页面。"},
+  period:{audience:"场站负责人、区域运营经理和需要比较场站运行变化的运维团队负责人。",value:"在相同时间长度和相同资产组边界下比较事件影响,帮助判断本月异常是否增多、影响范围是否扩大,以及问题集中在哪个场站层级或节点。",whyAI:"周期指标必须由规则计算,但层级贡献与变化原因较难快速阅读。AI 只对已经计算并通过范围校验的结果做解释;没有有效历史记录时不生成“看似合理”的结论。",feasibilityEvidence:"当前页面已经具备资产组切换、节点范围建立、双周期请求、事件指标计算、层级贡献、高频节点、AI 解读和请求轨迹的实现框架。可行性仍取决于 `/api/v1/event/hist` 在有效认证下返回可映射、带时间字段的真实记录;源站“综合数据分析”菜单为演示数据,已明确排除,不能作为历史积累证据。"},
+  benchmark:{audience:"集团或区域管理者、多站点运营负责人、能效负责人和投资规划团队。",value:"识别本站在同类场站中的真实位置,发现可复制的标杆做法,避免只看绝对设备数量或单一能耗造成错误判断。",whyAI:"横向比较需要自动完成同类筛选、容量与负荷归一化、异常样本识别和差距解释,AI 能把复杂统计转成管理者可行动的结论。",feasibilityEvidence:"源站以资产组管理多个场站,并已积累各场站的状态、历史测点和事件数据;这为多站点指标抽取提供基础,但正式排名仍需补充统一容量、气候区和运行等级主数据。"},
+  maintenance:{audience:"运维团队负责人、场站负责人、值班主管、服务商经理和 SLA 管理人员。",value:"把计划、响应、处置、关闭和维护后效果放在同一视图,及时发现延期风险、责任卡点和重复故障,提升履约率与一次修复率。",whyAI:"工单流程、报警复发和维护前后测点分散在不同记录中。AI 能自动关联同一事件链、预测逾期并生成班组层面的改善建议。",feasibilityEvidence:"源模块中已确认工单列表、工作流、保养计划关联工单、报警流程日志和历史测点路径,具备计算响应时长、完成率及维护效果的原始记录;尚需映射 api-ticket 网关和状态字段。"},
+  health:{audience:"企业管理者、区域运营负责人、场站负责人、风险与安全管理人员。",value:"用一个可解释的场站级健康度统一呈现运行、报警、运维、能效、数据和安全状态,使管理者快速比较、排序和追踪改善。",whyAI:"单一分数本身价值有限,关键是解释扣分来源、影响范围和改善路径。AI 适合把规则评分与底层证据组织成面向不同角色的解释。",feasibilityEvidence:"源站已具备设备状态、报警事件、历史测点、工单保养和场站参数等健康度输入,能够先上线透明规则版评分;各站型权重与安全红线需由业务配置。"},
+  prediction:{audience:"场站负责人、值班主管、区域风险管理团队、运维计划人员和应急准备人员。",value:"把被动告警前移为未来 24—72 小时的风险窗口,使人员、备用能力和维护工作提前准备,减少场站能力受损和突发处置。",whyAI:"风险通常由负荷、环境、趋势、报警密度和未完工单共同驱动,AI/时序模型适合识别非线性组合并解释主要驱动因素。",feasibilityEvidence:"源站已经长期保存历史曲线、趋势、事件和工单记录,具备形成时序特征的基础;初期可用规则预警,积累 1—3 个月连续数据及影响标签后再训练预测模型。"},
+  alarm:{audience:"值班人员、报警中心、运维负责人、场站负责人和区域事件治理团队。",value:"把大量重复报警压缩为少数场站事件,降低报警疲劳,突出真正影响场站能力的事项,并缩短确认和闭环时间。",whyAI:"报警关联涉及时间、拓扑、状态传播和文本描述。AI 可以辅助事件聚类与摘要,让人员围绕事件处置,而不是逐条阅读报警。",feasibilityEvidence:"源站已提供报警历史、当前事件、报警流程日志和资产拓扑相关接口,且页面本身具备报警查看与处置能力;首期可采用可解释规则归并并允许人工拆分、合并。"},
+  efficiency:{audience:"能源管理人员、场站负责人、企业运营管理者、成本负责人和节能项目团队。",value:"将总耗能转成单位产出指标,定位系统级偏差与可节约空间,为运行优化、节能项目和成本复盘提供量化依据。",whyAI:"能效受负荷、天气、运行模式和设备组合共同影响。AI 能建立同工况基线、解释偏差贡献,并生成带安全约束的节能机会。",feasibilityEvidence:"源站已经具备历史曲线、用量分析、关键测点和场站参数功能,可提供能源与运行数据积累;需要进一步配置能源表计、有效产出测点及天气等外部数据。"},
+  capacity:{audience:"场站负责人、运行调度、规划建设团队、企业管理者和业务容量管理人员。",value:"提前看到需求峰值、场站可用能力和冗余裕度,支持排程、备用系统准备、扩容决策及高负荷风险控制。",whyAI:"需求会受到天气、日历、业务负荷和检修计划共同影响,AI 适合进行多变量预测,并将预测结果与能力约束自动比对。",feasibilityEvidence:"源站已积累历史负荷、实时状态、维护计划和关键参数,能够形成预测原型;正式能力结论还需补齐设计能力曲线、环境降额和上下游需求数据。"},
+  strategy:{audience:"场站负责人、能源优化团队、值班主管、运行策略审批人和区域运营负责人。",value:"从场站整体协同角度提出可审批的运行组合建议,在保证安全、冗余和产出的前提下获得节能或能力收益。",whyAI:"候选策略需要同时考虑多系统边界、收益、风险和回退条件。AI/优化算法适合生成候选方案并把复杂约束解释成人员可审核的建议。",feasibilityEvidence:"源站已经具备实时/历史测点、控制管理、快速控制、排程计划、控制模板和执行记录等功能,说明控制状态与审计数据基础存在;首期仅做建议与仿真,不自动执行。"},
+  emergency:{audience:"场站负责人、应急指挥人员、值班团队、EHS/安全人员和企业管理层。",value:"在事件期间快速汇总影响、进度和 SOP,在事件后自动形成完整时间线与改进任务,减少信息遗漏并提升复盘质量。",whyAI:"应急信息来自报警、人工确认、控制动作、指标恢复和工单等多条时间线。AI 适合检索 SOP、合并记录和生成复盘初稿,但不替代指挥决策。",feasibilityEvidence:"源站已经积累报警事件、处置日志、控制记录、执行记录和工单,能够还原大部分事件时间线;接入正式 SOP、应急等级和人员确认记录后即可形成完整闭环。"},
+};
+
+type ArchitectureLayer = {id:string;index:string;title:string;role:string;items:string[];tone:string};
+type ArchitectureItem = {code:string;name:string;purpose:string;details:string;phase:string};
+
+const architectureLayers:ArchitectureLayer[] = [
+  {id:"experience",index:"01",title:"场站应用与体验层",role:"面向管理者和场站团队,呈现 12 个业务页面,不暴露源系统复杂度。",items:["资产组切换","证据下钻","提示词工作台","Word / MD 导出"],tone:"cyan"},
+  {id:"orchestration",index:"02",title:"Agent 编排与治理层",role:"识别意图、选择 Skill、拆解任务、合并结果,并执行范围、质量和安全校验。",items:["总控编排","数据落地","指标计算","解释生成","安全审计"],tone:"violet"},
+  {id:"skills",index:"03",title:"场站 Skill 能力层",role:"沉淀可版本化的场站业务流程、指标口径、提示模板和降级规则。",items:["12 个场景 Skill","场站上下文","证据引用","报告导出"],tone:"blue"},
+  {id:"mcp",index:"04",title:"MCP 工具与领域服务层",role:"公司 API 全部按领域封装为 MCP Tool,并遵循公司既有 MCP 建设与治理规范。",items:["资产与时序","事件与报警","运维工单","知识与外部数据","控制能力"],tone:"green"},
+  {id:"source",index:"05",title:"MU 与企业数据源层",role:"保留现有系统作为事实源;AI 不直接抓页面,也不把演示页面数值作为生产数据。",items:["MU / APM API","Alarm Process","Ticket / Maintenance","Control Core","SOP / 天气 / 电价"],tone:"orange"},
+];
+
+const requiredSkills:ArchitectureItem[] = [
+  {code:"SK-00",name:"场站上下文与权限",purpose:"统一 tenant、org、assetGroup、时间范围和站型,阻止跨场站取数。",details:"所有业务 Skill 的前置依赖;输出标准 StationContext 和权限范围。",phase:"P0"},
+  {code:"SK-01",name:"证据与数据质量",purpose:"给每项结论附接口、时间、范围、记录 ID、完整度和缺口。",details:"负责“可回答/不可回答”判断,禁止使用演示值补空。",phase:"P0"},
+  {code:"SK-02",name:"场站智能日报",purpose:"生成节点状态版日报,并逐步接入报警闭环、工单、保养和关键测点。",details:"对应场站智能日报页面;支持提示词编辑和重新生成。",phase:"P0"},
+  {code:"SK-03",name:"场站问答",purpose:"将问题绑定当前资产组,编排工具并生成带证据回答。",details:"先覆盖资产与状态,再扩展事件、运维、趋势和 SOP。",phase:"P0"},
+  {code:"SK-04",name:"运行环比",purpose:"按同长度窗口和相同场站边界计算事件及后续运营指标变化。",details:"当前先用历史事件;无有效记录时输出数据缺口。",phase:"P0"},
+  {code:"SK-05",name:"同类场站对标",purpose:"按站型、容量、气候和运行模式筛选可比样本并归一化。",details:"要求跨站统一指标版本、主数据和授权。",phase:"P2"},
+  {code:"SK-06",name:"运维履约分析",purpose:"计算计划完成、SLA、一次修复、逾期风险和维护后效果。",details:"关联工单、保养、报警流程和维护前后测点。",phase:"P1"},
+  {code:"SK-07",name:"场站运营健康度",purpose:"按站型配置透明权重和红线,形成可解释的六维评分。",details:"分数必须能下钻到确定性指标和原始证据。",phase:"P2"},
+  {code:"SK-08",name:"场站风险预测",purpose:"预测未来 24—72 小时的场站影响窗口。",details:"先规则预警,积累连续数据和影响标签后再训练模型。",phase:"P2"},
+  {code:"SK-09",name:"报警事件治理",purpose:"按时间、拓扑和状态传播把报警归并为场站事件。",details:"首期采用可解释规则,并允许人工拆分与合并。",phase:"P1"},
+  {code:"SK-10",name:"资源与能效",purpose:"建立单位产出能耗和同工况基线,解释系统级偏差。",details:"需要能源表计、产出/负荷、天气及计量边界。",phase:"P2"},
+  {code:"SK-11",name:"负荷与能力预测",purpose:"比较预测需求、实时可用能力和冗余裕度。",details:"需要设计能力曲线、环境降额、检修和外部需求。",phase:"P2"},
+  {code:"SK-12",name:"场站运行策略建议",purpose:"在安全、产出和冗余约束内生成可审批的运行组合。",details:"只生成建议和仿真结果;控制执行必须独立授权。",phase:"P3"},
+  {code:"SK-13",name:"应急辅助与复盘",purpose:"检索 SOP、组织处置时间线并生成复盘与改进任务。",details:"AI 不替代应急指挥、保护联锁和人工确认。",phase:"P1"},
+  {code:"SK-14",name:"文档与交付物生成",purpose:"将场站报告、证据、口径和数据缺口导出为 Word/MD。",details:"所有导出复用页面确定性结果,保留版本和生成时间。",phase:"P0"},
+];
+
+const requiredMcps:ArchitectureItem[] = [
+  {code:"MCP-01",name:"mu-context-mcp",purpose:"封装租户、组织、资产组和站点上下文。",details:"Tools:list_asset_groups、get_station_context;映射 group/list 等接口。当前优先级最高。",phase:"P0"},
+  {code:"MCP-02",name:"mu-asset-telemetry-mcp",purpose:"封装节点、状态、属性、实时值与历史时序。",details:"Tools:list_nodes、get_node_status、list_properties、get_latest_values、query_history;映射 asset/node、node/property、latest、hist 系列接口。",phase:"P0"},
+  {code:"MCP-03",name:"mu-event-alarm-mcp",purpose:"封装当前/历史事件、报警流程和处置日志。",details:"Tools:query_events、get_open_events、get_event_process、correlate_event_evidence;映射 event/hist、event/latest 与 alarm-process。",phase:"P0 / P1"},
+  {code:"MCP-04",name:"mu-om-mcp",purpose:"封装工单、巡检、保养计划、SLA 和责任信息。",details:"Tools:list_work_orders、get_work_order、list_maintenance_tasks、get_sla_status;映射 api-ticket 与 maintenance。",phase:"P1"},
+  {code:"MCP-05",name:"station-masterdata-mcp",purpose:"提供站型、容量、关键参数、层级映射和指标版本。",details:"Tools:get_station_profile、get_capacity_rules、resolve_asset_hierarchy、get_metric_dictionary;汇集 station/keyparameters 和企业主数据。",phase:"P1"},
+  {code:"MCP-06",name:"station-knowledge-mcp",purpose:"检索 SOP、操作规程、应急预案、维护手册和历史案例。",details:"Tools:search_sop、get_procedure、search_incident_case;返回文档版本、段落引用和适用站型。",phase:"P1"},
+  {code:"MCP-07",name:"external-context-mcp",purpose:"封装天气、电价、负荷/处理量、业务日历等外部上下文。",details:"Tools:get_weather、get_tariff、get_business_load、get_calendar_context;缺失时业务 Skill 必须降级。",phase:"P2"},
+  {code:"MCP-08",name:"mu-control-audit-mcp",purpose:"隔离控制状态、排程、执行记录及未来的受控下发。",details:"Tools:list_control_logs、list_schedules、simulate_strategy、request_approval;默认只读,写操作需审批、白名单、联锁校验和全量审计。",phase:"P3"},
+];
+
+const requiredAgents:ArchitectureItem[] = [
+  {code:"AG-00",name:"Station Copilot Orchestrator",purpose:"总控 Agent:识别场景、选择 Skill、拆解步骤、调用其他 Agent 并合并结果。",details:"强制携带 StationContext;负责超时、重试、降级和最终响应结构。",phase:"P0"},
+  {code:"AG-01",name:"Data Grounding Agent",purpose:"将业务问题转换为 MCP Tool 调用并形成可追溯证据包。",details:"只取数不写结论;校验资产组范围、时间窗、字段映射和数据新鲜度。",phase:"P0"},
+  {code:"AG-02",name:"Metric & Rule Agent",purpose:"执行日报、环比、健康度、SLA 等确定性计算。",details:"公式版本化、可单元测试;大模型不得代替基础数值计算。",phase:"P0"},
+  {code:"AG-03",name:"Station QA Agent",purpose:"理解自然语言问题,选择所需证据并生成带引用答案。",details:"遇到未接入数据域时明确拒答或降级,不跨资产组推断。",phase:"P0"},
+  {code:"AG-04",name:"Report & Insight Agent",purpose:"把确定性结果转成日报、管理摘要、洞察和 Word/MD 交付物。",details:"支持可编辑提示词;输出必须引用指标和数据缺口。",phase:"P0"},
+  {code:"AG-05",name:"Event Diagnosis Agent",purpose:"归并报警、梳理事件链、关联工单与维护效果。",details:"区分“相关性”和“已确认根因”,保留人工拆分/合并结果。",phase:"P1"},
+  {code:"AG-06",name:"Forecast Agent",purpose:"承担风险、负荷、能力和能效基线预测。",details:"输出置信度、回测指标、输入覆盖率和失效条件。",phase:"P2"},
+  {code:"AG-07",name:"Strategy Advisor Agent",purpose:"生成场站级运行组合、收益预测、仿真和回退建议。",details:"不直接控制设备;所有执行动作转交审批与控制安全链。",phase:"P3"},
+  {code:"AG-08",name:"Governance & Audit Agent",purpose:"在发布前检查证据覆盖、权限、敏感信息、口径版本和控制边界。",details:"可阻断无来源结论、过期数据、跨站数据和未经审批的控制请求。",phase:"P0"},
+];
+
+const deliveryPhases = [
+  {phase:"P0 · 事实层闭环",goal:"完成现有三页生产化",scope:"Context/Asset/Event MCP;总控、数据、计算、问答、报告、审计 Agent;SK-00—04、SK-14。"},
+  {phase:"P1 · 运维与知识闭环",goal:"补足日报与事件处置",scope:"O&M、Masterdata、Knowledge MCP;事件诊断 Agent;运维履约、报警治理、应急复盘 Skill。"},
+  {phase:"P2 · 跨站与预测",goal:"形成管理分析和预测能力",scope:"External Context MCP;预测 Agent;对标、健康度、风险、能效、容量 Skill。"},
+  {phase:"P3 · 建议到受控执行",goal:"先建议、仿真、审批,再考虑执行",scope:"Control Audit MCP;策略 Agent;白名单、联锁、回退、双人审批和审计。"},
+];
+
+const apiStatusLabel:Record<ApiStatus,string> = {verified:"已验证",source:"源码确认",external:"待外部接入"};
+
+function getPanelItems(scenario:ScenarioDefinition,design:ScenarioDesign):PanelSpec[] {
+  if (scenario.key === "daily") return [
+    {name:"资产组切换与数据状态",kind:"公共 Panel",source:"当前组织下的资产组清单,以及所选资产组的节点清单/源站已确认快照。",apis:[apiCatalog.groups,apiCatalog.nodes],calc:"资产组 id/name 作为页面唯一场站边界;切换后重新加载节点,不保留上一资产组统计。数据状态明确区分节点 API、源站可见快照和仅有总数。"},
+    {name:"KPI · 纳管设备",kind:"指标 Panel",source:"当前资产组节点清单;接口未返回节点时可显示源站已确认的资产组总数。",apis:[apiCatalog.groups,apiCatalog.nodes],calc:design.kpiCalcs[0]},
+    {name:"KPI · 当前在线",kind:"指标 Panel",source:"当前资产组节点当前状态。",apis:[apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.kpiCalcs[1]},
+    {name:"KPI · 当前离线",kind:"指标 Panel",source:"当前资产组节点当前状态。",apis:[apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.kpiCalcs[2]},
+    {name:"KPI · 告警 / 故障",kind:"指标 Panel",source:"当前资产组节点状态枚举;不是报警流程事件。",apis:[apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.kpiCalcs[3]},
+    {name:"站点智能日报",kind:"主报告 Panel",source:"本页四项 KPI、状态覆盖数和当前资产组名称。",apis:[apiCatalog.groups,apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.visualCalc},
+    {name:"异常设备证据清单",kind:"证据 Panel",source:"当前资产组中状态为故障、告警或离线的节点记录。",apis:[apiCatalog.nodes,apiCatalog.nodeStatus],calc:"按故障→告警→离线排序,最多展示 12 条;设备只作为场站结论的证据,不把页面转为设备管理。"},
+    {name:"AI 场站洞察与提示词",kind:"AI Panel",source:"确定性状态统计、异常节点证据、当前资产组和 unavailableDomains。",apis:[apiCatalog.groups,apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.insightCalc},
+    {name:"场站运营摘要与数据边界",kind:"摘要 / 质量 Panel",source:"当前资产组确定性统计及接口就绪状态。",apis:[apiCatalog.groups,apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.summaryCalc},
+  ];
+  if (scenario.key === "qa") return [
+    {name:"资产组切换与问答上下文",kind:"公共 Panel",source:"当前组织的资产组清单、所选资产组节点及数据模式。",apis:[apiCatalog.groups,apiCatalog.nodes],calc:"锁定资产组 id/name;切换资产组时清空旧问题、答案和证据,随后重新加载当前组节点。"},
+    {name:"KPI · 当前资产组",kind:"指标 Panel",source:"资产组及其组织/站点名称。",apis:[apiCatalog.groups],calc:design.kpiCalcs[0]},
+    {name:"KPI · 已加载证据",kind:"指标 Panel",source:"当前资产组实际载入的节点记录。",apis:[apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.kpiCalcs[1]},
+    {name:"KPI · 可回答数据域",kind:"指标 Panel",source:"当前已接线的数据域清单和 unavailableDomains。",apis:[apiCatalog.groups,apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.kpiCalcs[2]},
+    {name:"KPI · 数据完整度",kind:"指标 Panel",source:"资产组纳管总数与有明确状态的节点数。",apis:[apiCatalog.groups,apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.kpiCalcs[3]},
+    {name:"向当前场站提问",kind:"问答 Panel",source:"问题文本、当前资产组统计、节点清单、接口路径及未接入域。",apis:[apiCatalog.groups,apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.visualCalc},
+    {name:"相关节点证据",kind:"证据 Panel",source:"当前资产组节点状态;依据问题中的故障、告警、离线或运行关键词筛选。",apis:[apiCatalog.nodes,apiCatalog.nodeStatus],calc:"最多展示 8 条相关节点,保留名称、层级、类型、状态和数据模式;不存在证据时明确显示空状态。"},
+    {name:"答案来源与接口依据",kind:"质量 Panel",source:"本次回答使用的数据模式、资产组过滤条件和实际接口路径。",apis:[apiCatalog.groups,apiCatalog.nodes],calc:"显示 AI 模型或本地规则模式,并列出可追溯 API;未接入域不能成为回答依据。"},
+    {name:"场站运营摘要",kind:"摘要 Panel",source:"当前资产组与已加载节点状态证据。",apis:[apiCatalog.groups,apiCatalog.nodes,apiCatalog.nodeStatus],calc:design.summaryCalc},
+  ];
+  if (scenario.key === "period") return [
+    {name:"资产组切换与比较范围",kind:"公共 Panel",source:"资产组和节点清单用于建立当前场站的节点身份集合。",apis:[apiCatalog.groups,apiCatalog.nodes],calc:"资产组切换后重新发起双周期查询;本期为本月 1 日至当前,基期为上月 1 日起相同已过去时长。"},
+    {name:"真实数据校验门",kind:"质量 Panel",source:"历史事件接口状态、返回行数、时间字段及节点映射结果。",apis:[apiCatalog.events,apiCatalog.nodes],calc:"只有请求成功、存在记录、时间可解析且能映射到当前资产组节点时才进入计算;否则展示 DATA GAP 并停止生成结论。"},
+    {name:"本月与上月同期",kind:"主对比 Panel",source:"两个同长度窗口内、通过当前资产组节点过滤的历史事件。",apis:[apiCatalog.groups,apiCatalog.nodes,apiCatalog.events],calc:design.visualCalc},
+    {name:"指标 · 事件影响可用率(估算)",kind:"指标 Panel",source:"纳管节点数、窗口小时数和事件影响时长。",apis:[apiCatalog.nodes,apiCatalog.events],calc:design.kpiCalcs[0]},
+    {name:"指标 · 历史事件",kind:"指标 Panel",source:"当前资产组双周期历史事件记录。",apis:[apiCatalog.events,apiCatalog.nodes],calc:design.kpiCalcs[1]},
+    {name:"指标 · 影响节点",kind:"指标 Panel",source:"可映射到当前资产组的事件节点身份。",apis:[apiCatalog.events,apiCatalog.nodes],calc:design.kpiCalcs[2]},
+    {name:"指标 · 事件影响时长",kind:"指标 Panel",source:"历史事件开始、结束时间及比较窗口边界。",apis:[apiCatalog.events],calc:design.kpiCalcs[3]},
+    {name:"本月事件贡献层级",kind:"贡献 Panel",source:"本月当前资产组内事件的场站层级字段。",apis:[apiCatalog.events,apiCatalog.nodes],calc:"按层级分组统计事件数并降序排列;无法确认层级或节点归属的记录不进入排名。"},
+    {name:"高频影响节点",kind:"证据 Panel",source:"本月当前资产组内可映射到节点的历史事件。",apis:[apiCatalog.events,apiCatalog.nodes],calc:"按节点汇总事件数和高优先级事件数,降序展示,作为场站变化解释的下钻证据。"},
+    {name:"运行变化解读",kind:"AI Panel",source:"双周期四项指标、层级贡献、高频节点、窗口口径和数据质量。",apis:[apiCatalog.events,apiCatalog.nodes],calc:design.summaryCalc},
+    {name:"本次真实请求",kind:"审计 Panel",source:"前端调用后端代理时记录的接口路径、HTTP 状态和返回行数。",apis:[apiCatalog.groups,apiCatalog.nodes,apiCatalog.events],calc:"逐接口展示成功/失败、状态码和行数;该轨迹是判断是否可以发布环比结论的直接依据。"},
+  ];
+  const apiItems = design.apiKeys.map(key=>apiCatalog[key]);
+  return [
+    {name:"场站上下文栏",kind:"公共 Panel",source:"资产组确定当前场站;节点、场站关键参数和最近成功数据时间确定范围、模式与数据状态。",apis:[apiCatalog.groups,apiCatalog.nodes,apiCatalog.stationParams],calc:"以资产组 ID 作为所有请求的场站过滤条件;统计范围使用页面选择值;数据时点取关键请求最后成功时间。"},
+    ...scenario.metrics.map((metric,index)=>({name:`KPI · ${metric.label}`,kind:"指标 Panel",source:design.sourceText,apis:apiItems,calc:design.kpiCalcs[index]})),
+    {name:design.visualPanel,kind:"主分析 Panel",source:design.sourceText,apis:apiItems,calc:design.visualCalc},
+    {name:"需要关注的场站事项",kind:"AI 洞察 Panel",source:"只使用本页面已经计算完成的场站指标、事件、趋势与待办,不让大模型直接计算基础数值。",apis:apiItems,calc:design.insightCalc},
+    {name:"场站运营摘要",kind:"AI 摘要 Panel",source:"将确定性计算结果、异常证据 ID、数据时点和数据缺口组成结构化上下文,再交给模型生成说明。",apis:apiItems,calc:design.summaryCalc},
+    {name:"数据来源与实施成熟度",kind:"质量 Panel",source:"汇总本页面各接口的连通、字段映射、数据完整性和刷新时间,展示真实成熟度。",apis:apiItems,calc:"已验证仅表示当前环境真实请求成功;源码确认表示方法与路径已找到;待外部接入表示 MU 当前数据域不足。"},
+  ];
+}
+
+function downloadTextFile(content:string,fileName:string,mime:string) {
+  const blob = new Blob([content],{type:mime});
+  const url = URL.createObjectURL(blob);
+  const anchor = document.createElement("a");
+  anchor.href=url; anchor.download=fileName; document.body.appendChild(anchor); anchor.click(); anchor.remove();
+  window.setTimeout(()=>URL.revokeObjectURL(url),1000);
+  return blob.size;
+}
+
+function escapeHtml(value:string) {
+  return value.replace(/[&<>\"]/g,char=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[char] ?? char));
+}
+
+function buildArchitectureMarkdownLines() {
+  const lines = [
+    "# 整体架构",
+    "",
+    "## 总体架构图",
+    "",
+    "```mermaid",
+    "flowchart TB",
+    "  UI[场站应用与体验层\\n12 个业务页面 / 资产组切换 / 证据 / 导出]",
+    "  ORCH[Station Copilot Orchestrator\\n意图识别 / Skill 选择 / 任务编排 / 降级]",
+    "  AG[专业 Agent\\n数据落地 / 指标规则 / 问答 / 报告 / 诊断 / 预测 / 策略 / 审计]",
+    "  SK[场站 Skill 能力层\\n业务流程 / 指标口径 / 提示模板 / 数据要求 / 降级规则]",
+    "  MCP[MCP 工具与领域服务层\\n公司 API 的领域工具封装]",
+    "  SRC[MU 与企业数据源\\nAPM / Alarm / Ticket / Control / SOP / 外部数据]",
+    "  UI --> ORCH",
+    "  ORCH --> AG",
+    "  ORCH --> SK",
+    "  AG --> SK",
+    "  AG --> MCP",
+    "  MCP --> SRC",
+    "  AUDIT[治理护栏\\nTenant/Org/AssetGroup 隔离 · 证据 · 权限 · 控制审批] -.贯穿.-> ORCH",
+    "  AUDIT -.贯穿.-> MCP",
+    "```",
+    "",
+    "Skill 定义‘如何完成一个场站任务’,Agent 负责‘理解、计划与协作’,MCP 负责‘以受控方式访问事实和动作’。浏览器页面不保存生产凭据,也不直接调用公司 API。",
+    "",
+    "## 分层说明",
+    "",
+  ];
+  architectureLayers.forEach(layer=>lines.push(`### ${layer.index}. ${layer.title}`,"",layer.role,"",...layer.items.map(item=>`- ${item}`),""));
+  lines.push("## 需要建设的 Skills","","> Skill 是可版本化的业务能力包,包含适用场景、输入输出、MCP 依赖、确定性计算、提示模板、缺口处理和验收样例。","");
+  requiredSkills.forEach(item=>lines.push(`### ${item.code} · ${item.name}`,"",`- 目的:${item.purpose}`,`- 实施:${item.details}`,`- 阶段:${item.phase}`,""));
+  lines.push("## 需要建设的 MCP","","> 按领域建设 MCP Server,不按单个 API 拆服务;每个公司 API 只通过相应 MCP Tool 暴露给 Agent。","");
+  requiredMcps.forEach(item=>lines.push(`### ${item.code} · ${item.name}`,"",`- 目的:${item.purpose}`,`- Tools / API:${item.details}`,`- 阶段:${item.phase}`,""));
+  lines.push("## 需要建设的 Agents","","> Agent 是逻辑职责。P0 可部署在一个 Agent Runtime 中,通过清晰角色和工具权限隔离;规模、性能或合规要求出现后再拆成独立服务。","");
+  requiredAgents.forEach(item=>lines.push(`### ${item.code} · ${item.name}`,"",`- 职责:${item.purpose}`,`- 边界:${item.details}`,`- 阶段:${item.phase}`,""));
+  lines.push("## 分阶段实施","",...deliveryPhases.flatMap(item=>[`### ${item.phase} — ${item.goal}`,"",item.scope,""]));
+  return lines;
+}
+
+function buildArchitectureWordHtml() {
+  const layerHtml = architectureLayers.map(layer=>`<div class="arch-layer"><b>${escapeHtml(layer.index)}</b><div><h3>${escapeHtml(layer.title)}</h3><p>${escapeHtml(layer.role)}</p><small>${layer.items.map(escapeHtml).join(" · ")}</small></div></div>`).join("<div class=\"arch-arrow\">↓</div>");
+  const table = (title:string,intro:string,items:ArchitectureItem[])=>`<h2>${escapeHtml(title)}</h2><p>${escapeHtml(intro)}</p><table><thead><tr><th>编号</th><th>名称</th><th>目的</th><th>实施与边界</th><th>阶段</th></tr></thead><tbody>${items.map(item=>`<tr><td>${escapeHtml(item.code)}</td><td><b>${escapeHtml(item.name)}</b></td><td>${escapeHtml(item.purpose)}</td><td>${escapeHtml(item.details)}</td><td>${escapeHtml(item.phase)}</td></tr>`).join("")}</tbody></table>`;
+  return `<article class="architecture page-break"><div class="section-tag">AI ENGINEERING ARCHITECTURE</div><h1>整体架构</h1><p>Skill 定义“如何完成任务”,Agent 负责“理解、计划与协作”,MCP 负责“访问事实和动作”;具体 MCP 建设与治理遵循公司既有规范。</p><h2>总体架构图</h2><div class="arch-diagram">${layerHtml}</div><h2>分层说明</h2>${architectureLayers.map(layer=>`<h3>${escapeHtml(layer.index)}. ${escapeHtml(layer.title)}</h3><p>${escapeHtml(layer.role)}</p><ul>${layer.items.map(item=>`<li>${escapeHtml(item)}</li>`).join("")}</ul>`).join("")}${table("需要建设的 Skills","Skill 是版本化业务能力包,包含输入输出、MCP 依赖、计算口径、提示模板、降级规则和验收样例。",requiredSkills)}${table("需要建设的 MCP","按领域建设 MCP Server;每个公司 API 只通过相应 MCP Tool 暴露给 Agent,具体规范沿用公司要求。",requiredMcps)}${table("需要建设的 Agents","P0 可在一个 Agent Runtime 中按逻辑角色隔离,后续再按性能和合规要求拆服务。",requiredAgents)}<h2>分阶段实施</h2>${deliveryPhases.map(item=>`<h3>${escapeHtml(item.phase)} — ${escapeHtml(item.goal)}</h3><p>${escapeHtml(item.scope)}</p>`).join("")}</article>`;
+}
+
+function buildDesignMarkdown() {
+  const lines=["# 场站 AI 页面与数据口径设计说明","",`- 口径版本:V0.5 · 2026-08-22`,`- 覆盖范围:整体架构 + 12 个场站 AI 页面全部 Panel`,`- 当前实现基线:场站智能日报、场站问答、运行环比`,`- 接口状态:已验证 / 源码确认 / 待外部接入`,"","## 统一实施原则","","所有数字先由 API 数据和确定性规则计算,再由 AI 解释;设备、测点只作为场站结论的证据下钻。源站“综合数据分析”演示页面不作为真实数据或可行性证据;控制相关接口首期只读,不自动下发指令。","",...buildArchitectureMarkdownLines(),"","---",""];
+  skills.forEach((scenario,index)=>{
+    const design=scenarioDesigns[scenario.key], narrative=pageNarratives[scenario.key], panels=getPanelItems(scenario,design);
+    lines.push(`# ${index+1}. ${scenario.title}`,"",`**目标客户:** ${narrative.audience}`,"",`**页面价值:** ${narrative.value}`,"",`**为什么使用 AI:** ${narrative.whyAI}`,"",`**为什么可行:** ${narrative.feasibilityEvidence}`,"",`**可实现性结论:** ${design.feasibility}`,"",`**场站级数据组织:** ${design.sourceText}`,"",`**当前数据缺口:** ${design.gaps}`,"","## Panel 数据说明","");
+    panels.forEach((panel,panelIndex)=>{
+      lines.push(`### ${panelIndex+1}. ${panel.name}`,"",`- Panel 类型:${panel.kind}`,`- 数据来源:${panel.source}`,`- 计算方法:${panel.calc}`,"","对应 API:","");
+      panel.apis.forEach(api=>lines.push(`- [${apiStatusLabel[api.status]}] \`${api.method} ${api.path}\` — ${api.note}`));
+      lines.push("");
+    });
+    lines.push("---","");
+  });
+  return lines.join("\n");
+}
+
+function buildDesignWordHtml() {
+  const architectureHtml=buildArchitectureWordHtml();
+  const pages=skills.map((scenario,index)=>{
+    const design=scenarioDesigns[scenario.key], narrative=pageNarratives[scenario.key], panels=getPanelItems(scenario,design);
+    const panelHtml=panels.map((panel,panelIndex)=>`<section class="panel"><h3>${panelIndex+1}. ${escapeHtml(panel.name)}</h3><p><b>Panel 类型:</b>${escapeHtml(panel.kind)}</p><p><b>数据来源:</b>${escapeHtml(panel.source)}</p><p><b>计算方法:</b>${escapeHtml(panel.calc)}</p><h4>对应 API</h4><ul>${panel.apis.map(api=>`<li><b>[${apiStatusLabel[api.status]}]</b> <code>${escapeHtml(api.method)} ${escapeHtml(api.path)}</code><br>${escapeHtml(api.note)}</li>`).join("")}</ul></section>`).join("");
+    return `<article class="scenario ${index?"page-break":""}"><div class="section-tag">${escapeHtml(scenario.section)} · 页面 ${index+1}/12</div><h1>${escapeHtml(scenario.title)}</h1><div class="lead"><p><b>目标客户:</b>${escapeHtml(narrative.audience)}</p><p><b>页面价值:</b>${escapeHtml(narrative.value)}</p><p><b>为什么使用 AI:</b>${escapeHtml(narrative.whyAI)}</p><p><b>为什么可行:</b>${escapeHtml(narrative.feasibilityEvidence)}</p></div><h2>数据与实施口径</h2><p><b>可实现性结论:</b>${escapeHtml(design.feasibility)}</p><p><b>场站级数据组织:</b>${escapeHtml(design.sourceText)}</p><p><b>当前数据缺口:</b>${escapeHtml(design.gaps)}</p><h2>Panel 数据说明</h2>${panelHtml}</article>`;
+  }).join("");
+  return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>场站AI页面与数据口径设计说明</title><style>@page{margin:22mm 18mm}body{font-family:"Microsoft YaHei",Arial,sans-serif;color:#26323b;font-size:12pt;line-height:1.6}h1{font-size:24pt;color:#005b80;border-bottom:3px solid #008cd6;padding-bottom:8pt}h2{font-size:17pt;color:#005b80;margin-top:22pt}h3{font-size:14pt;margin-bottom:6pt}h4{font-size:12pt;color:#087ca8}.cover{padding:50pt 0 30pt}.cover h1{font-size:28pt}.lead{background:#eef7fb;border-left:5px solid #008cd6;padding:10pt 14pt}.panel{border-top:1px solid #ccd6dc;padding-top:8pt;margin-top:14pt;page-break-inside:avoid}.section-tag{color:#008cd6;font-weight:bold}code{font-family:Consolas,monospace;font-size:10pt;color:#234f68}li{margin-bottom:5pt}.page-break{page-break-before:always}.note{background:#eef9f4;border-left:5px solid #20a379;padding:10pt 14pt}.arch-diagram{background:#f2f7fa;border:1px solid #cbd7de;padding:12pt}.arch-layer{display:flex;gap:10pt;background:#fff;border-left:5px solid #008cd6;padding:8pt 10pt}.arch-layer>b{color:#008cd6}.arch-layer h3,.arch-layer p{margin:0}.arch-layer small{color:#53707f}.arch-arrow{text-align:center;color:#008cd6;font-weight:bold}table{width:100%;border-collapse:collapse;font-size:9.5pt;page-break-inside:auto}th,td{border:1px solid #cbd3d8;padding:5pt;vertical-align:top}th{background:#eaf4f8;color:#005b80}tr{page-break-inside:avoid}</style></head><body><section class="cover"><div class="section-tag">WISE-IoT · Station AI Copilot</div><h1>场站 AI 页面与数据口径设计说明</h1><p>口径版本:V0.5 · 2026-08-22</p><p>覆盖范围:整体架构 + 12 个场站 AI 页面全部 Panel</p><p>当前实现基线:场站智能日报、场站问答、运行环比</p><div class="note"><b>统一实施原则</b><br>所有数字先由 API 数据和确定性规则计算,再由 AI 解释;设备、测点只作为场站结论的证据下钻。源站“综合数据分析”演示页面不作为真实数据或可行性证据;控制相关接口首期只读,不自动下发指令。</div></section>${architectureHtml}${pages}</body></html>`;
+}
+
+const siteControl = {
+  "AIDC": { title:"冷却系统节能建议", device:"冷水机组、冷冻/冷却水泵、冷却塔、CRAH/CRAC", suggestion:"冷冻水供水设定值 7.0°C → 7.5°C", result:"预计节能 2.8%—4.1%,机柜进风温度最高 25.6°C", avoid:"消防/应急状态、冗余不足、传感器异常、UPS/BMS 或保护系统告警" },
+  "燃气站": { title:"压缩机与调压运行建议", device:"压缩机、调压器、流量/压力/温度变送器、可燃气体探测器", suggestion:"按来气量预测调整压缩机启停顺序", result:"降低空载能耗并减少频繁启停,保持出口压力约束", avoid:"泄漏报警、ESD、放散/泄压或消防联锁动作期间" },
+  "水站": { title:"泵组调度与压力优化", device:"取水泵、提升泵、加压泵、变频器、清水池与流量压力仪表", suggestion:"低谷时段优先补水,联动水位与末端压力调整频率", result:"降低峰时功率并维持最小供水压力与 N+1 冗余", avoid:"水质异常、低液位、最小频率或启停次数即将越限" },
+  "污水处理厂": { title:"曝气与回流优化建议", device:"鼓风机、曝气系统、提升泵、回流泵、加药设备、DO/ORP/pH/氨氮仪表", suggestion:"根据 DO 与进水负荷小步调整鼓风机频率", result:"目标是在出水约束内降低曝气能耗,单步变化受限", avoid:"水质仪表失准、负荷突变、工艺约束或应急旁路状态" },
+  "新能源场站": { title:"功率与储能协同建议", device:"光伏逆变器、风机、箱变、PCS/BMS/EMS、气象站与汇流箱", suggestion:"结合功率预测优化储能充放电和无功补偿时段", result:"减少弃电与偏差考核,维持 SOC 和并网约束", avoid:"电网调度指令、保护定值、紧急停机和电池保护动作" },
+};
+
+function extractArray(value: unknown): unknown[] {
+  if (Array.isArray(value)) return value;
+  if (!value || typeof value !== "object") return [];
+  const obj = value as Record<string, unknown>;
+  for (const key of ["e","data","list","rows","result","records","items","nodes","nodeList","content","elements","children"]) {
+    const next = obj[key];
+    if (Array.isArray(next)) return next;
+    const nested = extractArray(next);
+    if (nested.length) return nested;
+  }
+  return [];
+}
+
+function extractNodeRows(value: unknown): Record<string,unknown>[] {
+  const result: Record<string,unknown>[] = [];
+  const visit = (current:unknown) => {
+    if (Array.isArray(current)) {
+      current.forEach(visit);
+      return;
+    }
+    if (!current || typeof current !== "object") return;
+    const row = current as Record<string,unknown>;
+    const id = row.id ?? row.nodeId ?? row.deviceId ?? row.assetId;
+    const name = row.name ?? row.nodeName ?? row.deviceName ?? row.assetName;
+    const childList = [row.children,row.nodes,row.nodeList].find(Array.isArray) as unknown[]|undefined;
+    const hasDeviceSignal = ["status","statusName","onlineStatus","deviceStatus","state","typeName","deviceType","templateName","parentName","pathName"].some(key=>row[key] != null);
+    if (id != null && name != null && (!childList?.length || hasDeviceSignal)) result.push(row);
+    Object.values(row).forEach(visit);
+  };
+  visit(value);
+  return result;
+}
+
+function normalizeDevices(payloads: unknown[]): Device[] {
+  const rows = payloads.flatMap(extractNodeRows);
+  return rows.slice(0,10000).map((row,index) => {
+    const pick = (...keys:string[]) => keys.map(k=>row[k]).find(v=>typeof v === "string" || typeof v === "number");
+    const rawStatus = String(pick("statusName","status","onlineStatus","deviceStatus","state") ?? "未知");
+    const status = /fault|error|故障/i.test(rawStatus) ? "故障" : /offline|^0$|离线/i.test(rawStatus) ? "离线" : /alarm|warn|告警|预警/i.test(rawStatus) ? "告警" : /online|normal|running|运行|正常|^1$/i.test(rawStatus) ? "运行" : "未知";
+    return {
+      id:String(pick("id","nodeId","deviceId","assetId") ?? `live-${index}`),
+      name:String(pick("name","nodeName","deviceName","assetName") ?? `设备 ${index+1}`),
+      site:String(pick("pathName","fullPathName","parentName","stationName","siteName","orgName","groupName") ?? "当前资产组"),
+      type:String(pick("typeName","deviceTypeName","deviceType","categoryName","templateName") ?? "站点设备"),
+      status, score: status === "运行" ? 90 : status === "离线" ? 55 : status === "故障" ? 42 : status === "告警" ? 68 : 75,
+      alarm:status === "运行" ? "无" : status === "未知" ? "状态字段待映射" : `源站当前状态:${status}`,
+      metric:String(pick("value","latestValue","metricValue") ?? "实时"),
+    };
+  }).filter((item,index,self)=>self.findIndex(x=>x.id===item.id)===index);
+}
+
+function normalizeAssetGroups(value: unknown): AssetGroup[] {
+  return extractArray(value).filter((x):x is Record<string,unknown>=>!!x&&typeof x==="object").map((row,index)=>{
+    const name = String(row.name ?? row.groupName ?? `资产组 ${index+1}`);
+    const known = snapshotAssetGroups.find(group=>group.name===name);
+    return {
+      id:String(row.id ?? row.groupId ?? `group-${index}`),
+      name,
+      deviceCount:Number(row.deviceCount ?? row.nodeCount ?? row.assetCount ?? 0) || known?.deviceCount,
+      orgName:String(row.orgName ?? row.organizationName ?? "") || known?.orgName,
+      source:"api" as const,
+    };
+  }).filter((item,index,self)=>item.name&&self.findIndex(x=>x.id===item.id)===index);
+}
+
+function parseInsightText(text: string): string[] {
+  return text.split(/\r?\n/)
+    .map(line=>line.trim().replace(/^[-•*]\s*/,"").replace(/^\d+[.、)]\s*/,""))
+    .filter(line=>line.length>3&&!/^风险最高的设备[::]?$/.test(line))
+    .slice(0,5);
+}
+
+function buildStationQaFallback(question:string,devices:Device[],group:AssetGroup,total:number,unknown:number):string {
+  const pick=(statuses:string[])=>devices.filter(device=>statuses.includes(device.status));
+  const names=(items:Device[],limit=10)=>items.slice(0,limit).map(device=>`${device.name}(${device.site})`).join("、")||"无";
+  const offline=pick(["离线"]), faults=pick(["故障"]), warnings=pick(["告警"]), running=pick(["运行","正常"]);
+  if(question.includes("离线")) return `${group.name}当前识别到 ${offline.length} 台离线设备:${names(offline,12)}。建议按泵站层级先核查通信链路、供电和维护停用状态;该结果来自节点当前状态,不等同于未闭环报警。`;
+  if(/故障|异常|告警/.test(question)) return `${group.name}当前识别到故障 ${faults.length} 台、告警 ${warnings.length} 台。故障证据:${names(faults,8)};告警证据:${names(warnings,8)}。建议先确认故障对场站连续运行的影响,再核查告警持续时间与关联测点。`;
+  if(/完整|可信|发布|正式/.test(question)) return `${group.name}当前状态数据覆盖 ${Math.max(0,total-unknown)}/${total||0} 台,仍有 ${unknown} 台待补齐。可用于值班预览和异常排查;若要发布正式日报,建议先补齐节点状态,并接通报警闭环、工单和保养数据。`;
+  if(/在线|运行/.test(question)) return `${group.name}当前识别到 ${running.length} 台处于运行/正常状态:${names(running)}。状态待补齐设备未计入在线数。`;
+  return `${group.name}当前纳管 ${total||0} 台,运行 ${running.length} 台、故障 ${faults.length} 台、告警 ${warnings.length} 台、离线 ${offline.length} 台,另有 ${unknown} 台状态待补齐。建议优先处理故障和集中离线问题;未接入的报警闭环、工单和保养数量不作推测。`;
+}
+
+function MiniBars({values=[38,42,45,51,57,64,78,86],tone="violet"}:{values?:number[];tone?:string}) {
+  return <div className="mini-bars" aria-label="趋势图">{values.map((v,i)=><span key={i} className={tone} style={{height:`${v}%`}} />)}</div>;
+}
+
+export default function App() {
+  const [active,setActive] = useState<SkillKey>("daily");
+  const [navCollapsed,setNavCollapsed] = useState(false);
+  const [mobileMenuOpen,setMobileMenuOpen] = useState(false);
+  // 手机端切换场景后自动关闭菜单
+  useEffect(()=>{ if(mobileMenuOpen) setMobileMenuOpen(false); },[active]);
+  const [connection,setConnection] = useState<ConnectionState>("connecting");
+  const [devices,setDevices] = useState<Device[]>(waterGroupSnapshot);
+  const [deviceDataMode,setDeviceDataMode] = useState<"api"|"snapshot">("snapshot");
+  const [assetGroups,setAssetGroups] = useState<AssetGroup[]>(snapshotAssetGroups);
+  const [selectedGroupId,setSelectedGroupId] = useState("snapshot-water");
+  const [updatedAt,setUpdatedAt] = useState("—");
+  const [query,setQuery] = useState("列出过去 2 小时离线且伴随高优先级告警的设备");
+  const [asked,setAsked] = useState(false);
+  const [controlSite,setControlSite] = useState<keyof typeof siteControl>("AIDC");
+  const [approval,setApproval] = useState("待审批");
+  const [muConfig,setMuConfig] = useState<MuConfig>(DEFAULT_MU_CONFIG);
+  const [muSecret,setMuSecret] = useState("");
+  const [connectionDetail,setConnectionDetail] = useState("尚未完成真实数据连接");
+  const [connectionEndpoints,setConnectionEndpoints] = useState<Array<{path:string;status:number;ok:boolean;snippet?:string;error?:string}>>([]);
+  const [connectionSentHeaders,setConnectionSentHeaders] = useState<string[]>([]);
+  const [qaAnswer,setQaAnswer] = useState("");
+  const [qaLoading,setQaLoading] = useState(false);
+  const [summary,setSummary] = useState("");
+  const [summaryLoading,setSummaryLoading] = useState(false);
+
+  async function connectData(config: MuConfig = muConfig, secret: string = muSecret, groupId: string = selectedGroupId) {
+    setConnection("connecting");
+    const requestedGroup = assetGroups.find(group=>group.id===groupId) ?? snapshotAssetGroups.find(group=>group.id===groupId);
+    const controller = new AbortController();
+    const timeout = window.setTimeout(()=>controller.abort(),30000);
+    try {
+      const response = await fetch("/api/mu-connect",{
+        method:"POST",
+        signal:controller.signal,
+        headers:{"Content-Type":"application/json"},
+        body:JSON.stringify({
+          config,
+          secret,
+          selectedGroupId:requestedGroup?.source==="api"?requestedGroup.id:"",
+          selectedGroupName:requestedGroup?.name ?? "水务资产组",
+        }),
+      });
+      const responseText = await response.text();
+      let result: {ok?:boolean;payloads?:unknown[];asset_groups?:unknown;selected_group?:Record<string,unknown>;nodes_payload?:unknown;message?:string;endpoints?:Array<{path:string;status:number;ok:boolean;snippet?:string;error?:string}>;sent_headers?:string[]};
+      try {
+        result = JSON.parse(responseText);
+      } catch {
+        throw new Error(`后端服务返回异常(HTTP ${response.status}),请稍后重试或查看后端日志`);
+      }
+      setConnectionEndpoints(result.endpoints ?? []);
+      setConnectionSentHeaders(result.sent_headers ?? []);
+      if (!response.ok || !result.ok) throw new Error(result.message || `连接失败(${response.status})`);
+      const detail = result.message ?? "认证成功";
+      const liveGroups = normalizeAssetGroups(result.asset_groups);
+      if (liveGroups.length) setAssetGroups(liveGroups);
+      const apiSelectedId = result.selected_group ? String(result.selected_group.id ?? result.selected_group.groupId ?? "") : "";
+      const apiSelectedName = result.selected_group ? String(result.selected_group.name ?? result.selected_group.groupName ?? "") : "";
+      if (apiSelectedId) setSelectedGroupId(apiSelectedId);
+      const normalized = normalizeDevices(result.nodes_payload == null ? [] : [result.nodes_payload]);
+      const resolvedGroupName = apiSelectedName || requestedGroup?.name;
+      if (normalized.length) {
+        setDevices(normalized);
+        setDeviceDataMode("api");
+      } else {
+        setDevices(snapshotDevicesForGroup(resolvedGroupName));
+        setDeviceDataMode("snapshot");
+      }
+      setConnection("live");
+      setConnectionDetail(normalized.length ? `${detail};已加载 ${normalized.length} 台节点` : `${detail};节点清单暂未返回,当前显示源站可见快照`);
+      setUpdatedAt(new Date().toLocaleTimeString("zh-CN",{hour:"2-digit",minute:"2-digit",second:"2-digit"}));
+    } catch (error) {
+      setDevices(snapshotDevicesForGroup(requestedGroup?.name ?? "水务资产组"));
+      setDeviceDataMode("snapshot");
+      setConnection("preview");
+      setConnectionDetail(error instanceof Error && error.message !== "cors-or-auth" ? error.message : "已连接服务,但没有返回可用的设备数据");
+      setUpdatedAt("预览数据");
+    } finally { window.clearTimeout(timeout); }
+  }
+
+  async function runQa() {
+    setQaLoading(true);
+    try {
+      const data = await analyzeChat(query, devices, alarms);
+      setQaAnswer(data?.analysis || "(AI 未返回内容)");
+    } catch (e) {
+      setQaAnswer("AI 分析调用失败:" + (e instanceof Error ? e.message : String(e)));
+    } finally { setQaLoading(false); }
+  }
+
+  async function runSummary() {
+    setSummaryLoading(true);
+    try {
+      const data = await analyzeSummary(devices, alarms);
+      setSummary(data?.analysis || "(AI 未返回内容)");
+    } catch (e) {
+      setSummary("AI 分析调用失败:" + (e instanceof Error ? e.message : String(e)));
+    } finally { setSummaryLoading(false); }
+  }
+
+  useEffect(()=>{
+    const saved = window.sessionStorage.getItem("station-ai-mu-config");
+    if (saved) {
+      try {
+        const restored = {...DEFAULT_MU_CONFIG,...JSON.parse(saved)} as MuConfig;
+        setMuConfig(restored);
+        connectData(restored,"");
+        return;
+      } catch { window.sessionStorage.removeItem("station-ai-mu-config"); }
+    }
+    connectData(DEFAULT_MU_CONFIG,"");
+  },[]);
+
+  function saveMuConfig() {
+    window.sessionStorage.setItem("station-ai-mu-config",JSON.stringify(muConfig));
+    setConnectionDetail("配置已保存到当前浏览器会话;密码或令牌不会保存");
+  }
+  const alarms = useMemo(()=>devices.filter(d=>d.status==="告警"||d.status==="故障"),[devices]);
+  const activeSkill = skills.find(s=>s.key===active) ?? skills[0];
+  const selectedGroup = assetGroups.find(group=>group.id===selectedGroupId) ?? snapshotAssetGroups.find(group=>group.id===selectedGroupId) ?? snapshotAssetGroups[2];
+
+  function changeAssetGroup(nextId:string) {
+    const next = assetGroups.find(group=>group.id===nextId) ?? snapshotAssetGroups.find(group=>group.id===nextId);
+    setSelectedGroupId(nextId);
+    setDevices(snapshotDevicesForGroup(next?.name));
+    setDeviceDataMode("snapshot");
+    connectData(muConfig,muSecret,nextId);
+  }
+
+  return (
+    <><main className={`app-shell ${navCollapsed ? "nav-collapsed" : ""}`}>
+      <header className="topbar">
+        <div className="header-left">
+          <button className="brand" onClick={()=>setActive("daily")}>
+            <span className="wise-wordmark">WISE-IoT</span>
+            <span className="brand-divider"/>
+            <span className="portal-title">场站运营智能中心</span>
+          </button>
+          <nav className="product-tabs" aria-label="主导航">
+            <button className={active!=="settings"&&active!=="design"?"active":""} onClick={()=>setActive("daily")}>场站工作台</button>
+            <a href={`${muConfig.baseUrl.replace(/\/$/,"")}/#/internal-ortbhmwj-1751254748436654509/all_operate/all_PIV2_67/all_PIV2_116`} target="_blank" rel="noreferrer">原场站</a>
+            <button className={active==="settings"||active==="design"?"active":""} onClick={()=>setActive("settings")}>数据连接</button>
+          </nav>
+        </div>
+        <div className="header-actions">
+          <button className="mobile-menu-btn" onClick={()=>setMobileMenuOpen(!mobileMenuOpen)} aria-label="菜单">{mobileMenuOpen?"✕":"☰"}</button>
+          <button className={`live-pill ${connection}`} onClick={()=>connectData()}><span className="dot" />{connection==="connecting"?"正在连接设备监控…":connection==="live"?`已连接 · ${updatedAt}`:"预览数据 · 点击重连"}</button>
+          <span className="header-separator"/>
+          <button className="icon-button" aria-label="配置" onClick={()=>setActive("settings")}>⚙</button>
+          <span className="user-avatar" aria-label="用户">●</span>
+        </div>
+      </header>
+      {mobileMenuOpen && <div className="mobile-menu-overlay show" onClick={()=>setMobileMenuOpen(false)} aria-hidden="true" />}
+      <aside className={`left-navigation${mobileMenuOpen?" mobile-open":""}`} aria-label="站点 AI 功能导航">
+        <nav className="module-rail" aria-label="模块导航">
+          <button className={active!=="settings"&&active!=="design"?"active":""} onClick={()=>setActive("daily")} title="场站工作台">
+            <span className="rail-icon">▦</span><small>场站运营</small>
+          </button>
+          {navCollapsed && <button className="rail-expand" onClick={()=>setNavCollapsed(false)} title="展开功能菜单" aria-label="展开功能菜单"><span className="rail-arrow">›</span><small>展开</small></button>}
+          <a href={`${muConfig.baseUrl.replace(/\/$/,"")}/#/internal-ortbhmwj-1751254748436654509/all_operate/all_PIV2_67/all_PIV2_116`} target="_blank" rel="noreferrer" title="打开原场站">
+            <span className="rail-icon">▣</span><small>原场站</small>
+          </a>
+          <button className={active==="settings"||active==="design"?"active":""} onClick={()=>setActive("settings")} title="MU 数据源">
+            <span className="rail-icon">⚙</span><small>数据源</small>
+          </button>
+        </nav>
+        <nav className="function-sidebar" aria-label="场站 AI 场景菜单">
+          <div className="sidebar-panel-title">
+            <strong>场站 AI 场景</strong>
+            <button className="sidebar-collapse" onClick={()=>setNavCollapsed(true)} title="收起功能菜单" aria-label="收起功能菜单">‹</button>
+          </div>
+          {(["场站运营","运营分析","风险与策略"] as const).map(section=><div className="scenario-menu-group" key={section}>
+            <div className="sidebar-section-label">{section}</div>
+            {skills.filter(skill=>skill.section===section).map(skill=><button key={skill.key} className={active===skill.key?"active":""} onClick={()=>setActive(skill.key)}><span className={`scenario-menu-icon ${skill.tone}`}>{skill.icon}</span><span>{skill.title}</span></button>)}
+          </div>)}
+          <div className="sidebar-section-label">系统连接</div>
+          <button className={active==="settings"?"active":""} onClick={()=>setActive("settings")}><span>数据源配置</span></button>
+          <button className={active==="design"?"active":""} onClick={()=>setActive("design")}><span>设计说明</span></button>
+        </nav>
+      </aside>
+
+      {active==="settings" ? <MuSettings config={muConfig} setConfig={setMuConfig} secret={muSecret} setSecret={setMuSecret} connection={connection} detail={connectionDetail} endpoints={connectionEndpoints} sentHeaders={connectionSentHeaders} onTest={()=>connectData(muConfig,muSecret)} onSave={saveMuConfig} onReset={()=>{setMuConfig(DEFAULT_MU_CONFIG);setMuSecret("");window.sessionStorage.removeItem("station-ai-mu-config");setConnectionDetail("已恢复默认配置");setConnectionEndpoints([]);setConnectionSentHeaders([])}} /> : active==="design" ?
+      <DesignNotesPage /> : <ScenarioWorkspace scenario={activeSkill} connection={connection} devices={devices} alarms={alarms.length} updatedAt={updatedAt} assetGroups={assetGroups} selectedGroup={selectedGroup} deviceDataMode={deviceDataMode} muConfig={muConfig} muSecret={muSecret} onGroupChange={changeAssetGroup} onRefresh={()=>connectData(muConfig,muSecret,selectedGroupId)} />}
+    </main>
+    <footer className="site-footer">
+      <span className="footer-item">StationAIDemo · 场站 AI 分析</span>
+      <span className="footer-sep">·</span>
+      <span className="footer-item">ICP 备案号:{import.meta.env.VITE_BEIAN ?? "待备案"}</span>
+      <span className="footer-sep">·</span>
+      <a className="footer-item" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">ICP 备案查询</a>
+    </footer>
+  </>);
+}
+
+function ScenarioWorkspace({scenario,connection,devices,alarms,updatedAt,assetGroups,selectedGroup,deviceDataMode,muConfig,muSecret,onGroupChange,onRefresh}:{scenario:ScenarioDefinition;connection:ConnectionState;devices:Device[];alarms:number;updatedAt:string;assetGroups:AssetGroup[];selectedGroup:AssetGroup;deviceDataMode:"api"|"snapshot";muConfig:MuConfig;muSecret:string;onGroupChange:(id:string)=>void;onRefresh:()=>void}) {
+  const [generatedAt,setGeneratedAt] = useState(()=>new Date().toLocaleTimeString("zh-CN",{hour:"2-digit",minute:"2-digit"}));
+  const [promptOpen,setPromptOpen] = useState(false);
+  const [insightPrompt,setInsightPrompt] = useState("");
+  const [generatedInsights,setGeneratedInsights] = useState<string[]|null>(null);
+  const [insightLoading,setInsightLoading] = useState(false);
+  const [insightMeta,setInsightMeta] = useState("基于当前数据自动生成");
+  const isDaily = scenario.key === "daily";
+  const isQa = scenario.key === "qa";
+  const isPeriod = scenario.key === "period";
+  const isSourceSnapshot = deviceDataMode === "snapshot";
+  const total = selectedGroup.deviceCount || devices.length;
+  const running = devices.filter(device=>device.status==="运行"||device.status==="正常").length;
+  const offline = devices.filter(device=>device.status==="离线").length;
+  const faults = devices.filter(device=>device.status==="故障").length;
+  const warnings = devices.filter(device=>device.status==="告警").length;
+  const known = devices.filter(device=>device.status!=="未知").length;
+  const unknown = Math.max(0,total-known);
+  const online = Math.max(0,total-offline-unknown);
+  const health = total ? Math.round((running*100+warnings*60+faults*20+offline*15+unknown*50)/total) : 0;
+  const dailyMetrics = [
+    {label:"纳管设备",value:String(total||"—"),delta:deviceDataMode==="api"?"节点清单实时统计":total?`源站显示共计 ${total} 台`:"等待节点 API 返回",tone:total?"good" as const:"warn" as const},
+    {label:"当前在线",value:devices.length?(isSourceSnapshot&&known<total?`${online}+`:String(online)):"—",delta:total?`运行 ${running} · 状态完整 ${known}/${total}`:"切换后未沿用上一资产组数据",tone:online>0?"good" as const:"warn" as const},
+    {label:"当前离线",value:String(offline),delta:"按节点当前状态统计",tone:offline?"bad" as const:"good" as const},
+    {label:"告警 / 故障",value:`${warnings} / ${faults}`,delta:"当前设备状态,非报警流程条数",tone:(warnings+faults)?"warn" as const:"good" as const},
+  ];
+  const qaMetrics = [
+    {label:"当前资产组",value:selectedGroup.name,delta:`Demo Org · ${selectedGroup.orgName||(selectedGroup.name==="水务资产组"?"良泉水厂":"按资产组范围")}`,tone:"good" as const},
+    {label:"已加载证据",value:String(devices.length),delta:`节点状态 · 纳管 ${total||"—"} 台`,tone:devices.length?"good" as const:"warn" as const},
+    {label:"可回答数据域",value:"2 / 5",delta:"资产组、节点状态已就绪",tone:"warn" as const},
+    {label:"数据完整度",value:total?`${Math.round(Math.max(0,total-unknown)/total*100)}%`:"—",delta:`状态覆盖 ${Math.max(0,total-unknown)}/${total||0}`,tone:unknown?"warn" as const:"good" as const},
+  ];
+  const dailyInsights = total ? [
+    faults ? `${faults} 台故障设备需优先处置:${devices.filter(d=>d.status==="故障").slice(0,2).map(d=>d.name).join("、")}` : "当前未识别到故障状态设备",
+    offline ? `${offline} 台设备离线,建议先核查一号泵站的通信链路与供电` : "当前未识别到离线设备",
+    unknown ? `仍有 ${unknown} 台设备状态待节点 API 补齐,日报已显式标注数据完整度` : `已取得 ${total} 台设备完整状态,可直接生成正式日报`,
+  ] : ["已切换至新的资产组,正在等待节点清单","不会沿用水务资产组的设备与状态统计","连接成功后将自动生成该资产组的独立日报"];
+  const displayMetrics = isDaily ? dailyMetrics : isQa ? qaMetrics : scenario.metrics;
+  const baseInsights = isDaily ? dailyInsights : scenario.insights;
+  const dailySummary = total ? `${selectedGroup.name}当前纳管 ${total} 台设备;已识别运行 ${running} 台、告警 ${warnings} 台、故障 ${faults} 台、离线 ${offline} 台。${unknown?`尚有 ${unknown} 台状态待接口补齐。`:"节点状态完整。"} 当前日报只陈述源站可追溯事实,未接通的报警闭环、工单与保养数据不做推测。` : `${selectedGroup.name}已选中,暂未取得节点数据。请刷新数据或在“数据源配置”检查认证信息。`;
+  const defaultInsightPrompt = `你是面向场站运营管理者的 AI 分析助手。请仅分析当前资产组“${selectedGroup.name}”,不要扩展到工厂或其他资产组。\n\n已确认数据:纳管 ${total} 台;运行 ${running} 台;告警 ${warnings} 台;故障 ${faults} 台;离线 ${offline} 台;状态待补齐 ${unknown} 台。\n\n生成要求:\n1. 输出 3 条最值得关注的场站级事项,每行一条;\n2. 按影响范围和紧迫度排序,先故障,再告警、离线和数据完整性;\n3. 设备名称只作为场站结论的证据,不把页面写成设备管理页面;\n4. 不推测未接入的报警闭环、工单和保养数字;\n5. 每条包含“事实 + 场站影响 + 建议动作”,语言简洁。`;
+  useEffect(()=>{
+    setInsightPrompt(defaultInsightPrompt);
+    setGeneratedInsights(null);
+    setInsightMeta("基于当前数据自动生成");
+  },[defaultInsightPrompt]);
+  const displayInsights = generatedInsights ?? baseInsights;
+
+  async function regenerateInsights() {
+    if (!insightPrompt.trim()) {
+      setInsightMeta("提示词不能为空");
+      return;
+    }
+    setInsightLoading(true);
+    setInsightMeta("正在根据优化后的提示词生成…");
+    try {
+      const result = await analyzeModule("station-insight",insightPrompt,{
+        assetGroup:{id:selectedGroup.id,name:selectedGroup.name,organization:"Demo Org",station:selectedGroup.orgName||"按资产组范围"},
+        statistics:{total,running,warnings,faults,offline,unknown,health,dataMode:deviceDataMode},
+        abnormalDevices:devices.filter(device=>["故障","告警","离线"].includes(device.status)).slice(0,20),
+        unavailableDomains:["报警流程闭环","工单","保养"],
+      });
+      const analysisText = result.analysis||"";
+      const parsed = parseInsightText(analysisText);
+      const staleBackendFallback = result.fallback && /当前为预览数据|未获取到真实设备/.test(analysisText);
+      setGeneratedInsights(parsed.length&&!staleBackendFallback?parsed:baseInsights);
+      setInsightMeta(result.fallback?"已重新生成 · 本地规则模式":"已重新生成 · AI 模型");
+      setGeneratedAt(new Date().toLocaleTimeString("zh-CN",{hour:"2-digit",minute:"2-digit"}));
+    } catch (error) {
+      setGeneratedInsights(baseInsights);
+      setInsightMeta(`AI 服务暂不可用,已采用确定性规则 · ${error instanceof Error?error.message:"请求失败"}`);
+    } finally {
+      setInsightLoading(false);
+    }
+  }
+  return <section className={`scenario-workspace scenario-${scenario.visual}`}>
+    <header className="scenario-header">
+      <div className={`scenario-title-icon ${scenario.tone}`}>{scenario.icon}</div>
+      <div>
+        <span>{scenario.section} / {scenario.title}</span>
+        <h1>{scenario.title}</h1>
+        <p>{scenario.short}</p>
+      </div>
+      <div className="scenario-header-actions">
+        <button onClick={onRefresh} disabled={connection==="connecting"}>{connection==="connecting"?"正在同步":"刷新源数据"}</button>
+        <button className="primary" onClick={()=>setGeneratedAt(new Date().toLocaleTimeString("zh-CN",{hour:"2-digit",minute:"2-digit"}))}>{isDaily?"重新生成日报":"更新分析"}</button>
+      </div>
+    </header>
+    <div className="station-context-bar">
+      <div className="asset-group-context"><small>当前资产组</small><select aria-label="切换资产组" value={selectedGroup.id} onChange={event=>onGroupChange(event.target.value)}>{assetGroups.map(group=><option value={group.id} key={group.id}>{group.name}{group.deviceCount?` · ${group.deviceCount} 台`:""}</option>)}</select></div>
+      <div><small>组织 / 站点</small><strong>Demo Org · {selectedGroup.orgName || (selectedGroup.name==="水务资产组"?"良泉水厂":"按资产组范围")}</strong></div>
+      <div><small>统计范围</small><strong>今日 00:00—当前</strong></div>
+      <div><small>数据状态</small><strong className={deviceDataMode==="api"?"context-live":"context-preview"}>{deviceDataMode==="api"?`节点 API · ${updatedAt}`:devices.length?`源站可见快照 · ${updatedAt}`:"仅有源站总数 · 等待节点明细"}</strong></div>
+    </div>
+    {total>0&&devices.length===0&&<div className="asset-data-gap"><strong>{selectedGroup.name} 已确认纳管 {total} 台</strong><span>节点明细接口本次未返回数据,因此没有借用其他资产组的设备。请刷新源数据;若仍为空,请到“数据连接”更新 Cookie / Token。</span></div>}
+    <div className="scenario-body">
+      <div className="scenario-main">
+        {scenario.key!=="period"&&<div className="station-kpi-grid">
+          {displayMetrics.map(metric=><article key={metric.label} className={`station-kpi ${metric.tone??""}`}>
+            <span>{metric.label}</span><strong>{metric.value}</strong><small>{metric.delta}</small>
+          </article>)}
+        </div>}
+        <ScenarioVisual scenario={scenario} devices={devices} alarms={alarms} selectedGroup={selectedGroup} total={total} running={running} offline={offline} warnings={warnings} faults={faults} unknown={unknown} health={health} generatedAt={generatedAt} deviceDataMode={deviceDataMode} muConfig={muConfig} muSecret={muSecret}/>
+        {!isQa&&<section className="scenario-section insight-section insight-emphasis">
+          <div className="section-heading insight-heading"><div><span>AI INSIGHT</span><h2>AI 场站洞察</h2><p>把设备证据归纳为场站影响和行动优先级</p></div><div className="insight-heading-actions"><small>{insightMeta}</small><button className={promptOpen?"active":""} onClick={()=>setPromptOpen(value=>!value)}>{promptOpen?"收起提示词":"查看生成提示词"}</button></div></div>
+          {promptOpen&&<div className="prompt-workbench">
+            <div className="prompt-workbench-title"><div><strong>生成提示词</strong><span>可人工补充关注重点、输出语气或排序规则</span></div><em>Prompt 可编辑</em></div>
+            <textarea aria-label="AI Insight 生成提示词" value={insightPrompt} onChange={event=>setInsightPrompt(event.target.value)} />
+            <div className="prompt-workbench-actions"><span>数据统计和异常设备证据会随请求一并发送;未接入数据已列为不可用域。</span><button onClick={()=>setInsightPrompt(defaultInsightPrompt)} disabled={insightLoading}>恢复默认</button><button className="primary" onClick={regenerateInsights} disabled={insightLoading}>{insightLoading?"正在生成…":"按当前提示词重新生成"}</button></div>
+          </div>}
+          <div className="insight-list">{displayInsights.map((item,index)=><div key={`${index}-${item}`}><b>{index+1}</b><span>{item}</span><em className={index===0?"high":index===1?"medium":"low"}>{index===0?"优先":index===1?"关注":"观察"}</em></div>)}</div>
+        </section>}
+      </div>
+      <aside className="scenario-aside">
+        <section className="ai-summary-card">
+          <div className="ai-summary-title"><span>AI</span><div><strong>场站运营摘要</strong><small>基于当前场站上下文</small></div></div>
+          <p>{isDaily?dailySummary:isQa?`${selectedGroup.name}问答已锁定 Demo Org / ${selectedGroup.orgName||(selectedGroup.name==="水务资产组"?"良泉水厂":"当前范围")},当前载入 ${devices.length} 条节点状态证据。资产组和节点状态可直接回答;报警闭环、工单、保养和 SOP 尚未接入时会明确提示,不生成推测内容。`:isPeriod?`${selectedGroup.name}运行环比将按真实资产组节点范围,比较本月截至当前与上月同期。事件量、影响节点和异常时长来自历史事件接口;能耗与工单指标未接入前不展示示例值。`:scenario.summary}</p>
+          <div className="summary-evidence"><span>结论可追溯</span><span>{isDaily?`日报生成 ${generatedAt}`:isQa?`上下文更新 ${updatedAt}`:`数据截止 ${updatedAt}`}</span></div>
+        </section>
+        <section className="aside-card">
+          <h3>数据来源</h3>
+          <div className="source-list">{scenario.sources.map((source,index)=>{const ready=isPeriod?[1,3].includes(index):index===0||(isDaily||isQa)&&index===1&&deviceDataMode==="api";return <div key={source}><i className={ready?"ready":"planned"}/><span>{source}</span><small>{ready?(isPeriod?"已接线":"已接入"):"待映射"}</small></div>})}</div>
+        </section>
+        <section className="aside-card implementation-card">
+          <h3>计算与实现口径</h3>
+          <p>{scenario.formula}</p>
+          <div><small>实施成熟度</small><strong>{scenario.maturity}</strong></div>
+        </section>
+        <section className="scope-card">
+          <strong>场站级产品边界</strong>
+          <p>分析对象为场站与功能系统;设备和测点仅作为结论证据,下钻后展示。</p>
+        </section>
+      </aside>
+    </div>
+  </section>
+}
+
+function ArchitectureChapter() {
+  const itemList=(title:string,subtitle:string,items:ArchitectureItem[])=><section className="architecture-catalog">
+    <div className="architecture-section-title"><span>CAPABILITY CATALOG</span><h2>{title}</h2><p>{subtitle}</p></div>
+    <div className="architecture-table"><div className="architecture-table-head"><span>编号 / 名称</span><span>目的</span><span>实施与边界</span><span>阶段</span></div>{items.map(item=><article key={item.code}><div><b>{item.code}</b><strong>{item.name}</strong></div><p>{item.purpose}</p><p>{item.details}</p><em>{item.phase}</em></article>)}</div>
+  </section>;
+  return <section className="architecture-chapter">
+    <header className="architecture-heading"><div><span>AI ENGINEERING ARCHITECTURE</span><h2>整体架构</h2><p>规划完成场站 AI 站点所需的 Skill、MCP 与 Agent;具体 MCP 建设与治理遵循公司既有规范。</p></div><em>第 13 章 · V0.5</em></header>
+    <div className="architecture-principles">
+      <div><b>Skill</b><strong>定义“如何完成任务”</strong><p>业务流程、指标口径、提示模板、数据要求、降级策略与验收样例。</p></div>
+      <div><b>Agent</b><strong>负责“理解、计划与协作”</strong><p>选择 Skill、调用 MCP、执行计算、生成解释并在发布前完成校验。</p></div>
+      <div><b>MCP</b><strong>负责“访问事实和动作”</strong><p>公司 API 全部经领域 MCP Tool 暴露,具体封装和治理沿用公司既有要求。</p></div>
+    </div>
+    <section className="architecture-diagram-card">
+      <div className="architecture-section-title"><span>REFERENCE ARCHITECTURE</span><h2>总体架构图</h2><p>治理护栏横向贯穿所有层;控制能力与普通只读查询隔离。</p></div>
+      <div className="architecture-guardrail"><strong>治理护栏</strong><span>Tenant / Org / AssetGroup 隔离</span><span>证据与数据质量</span><span>权限与敏感信息</span><span>控制审批与审计</span></div>
+      <div className="architecture-flow">{architectureLayers.map((layer,index)=><div key={layer.id} className="architecture-flow-wrap"><article className={`architecture-layer ${layer.tone}`}><b>{layer.index}</b><div><span>{layer.title}</span><p>{layer.role}</p><div>{layer.items.map(item=><em key={item}>{item}</em>)}</div></div></article>{index<architectureLayers.length-1&&<i>↓</i>}</div>)}</div>
+      <div className="architecture-rule"><strong>调用主链</strong><span>用户 / 页面 → 总控 Agent → 选择 Skill → 专业 Agent → MCP Tool → MU / 企业数据源 → 证据化结果</span></div>
+    </section>
+    <section className="architecture-boundary">
+      <div><span>01</span><strong>Skill 只定义业务能力</strong><p>Skill 声明任务流程、计算口径、所需 MCP Tool、降级方式和验收样例。</p></div>
+      <div><span>02</span><strong>不让模型计算基础指标</strong><p>数量、比率、周期窗口和评分由规则 Agent 计算,模型只解释已经验证的结果。</p></div>
+      <div><span>03</span><strong>Agent 通过 MCP 取数</strong><p>Agent 不依赖源页面结构,所有公司 API 能力都由对应领域 MCP Tool 提供。</p></div>
+      <div><span>04</span><strong>不把演示页面当事实源</strong><p>只有真实 API 或正式企业数据服务可进入 MCP;演示值只能用于 UI 占位。</p></div>
+    </section>
+    {itemList("需要建设的 Skills(15 项)","12 个场景 Skill 加 3 个公共 Skill;P0 先覆盖当前已经开发的三个页面。",requiredSkills)}
+    {itemList("需要建设的 MCP(8 项)","按领域建设 8 个 MCP Server,不按单个 API 拆分;具体封装和治理遵循公司既有规范。",requiredMcps)}
+    {itemList("需要建设的 Agents(9 个逻辑角色)","这些首先是逻辑职责;P0 可以部署在一个 Agent Runtime 中,后续按性能和合规要求拆分。",requiredAgents)}
+    <section className="delivery-roadmap"><div className="architecture-section-title"><span>DELIVERY ROADMAP</span><h2>分阶段实施建议</h2><p>先完成事实层闭环,再扩展预测和控制,避免在数据尚未稳定时堆叠模型。</p></div><div>{deliveryPhases.map((item,index)=><article key={item.phase}><b>{String(index+1).padStart(2,"0")}</b><div><span>{item.phase}</span><h3>{item.goal}</h3><p>{item.scope}</p></div></article>)}</div></section>
+  </section>;
+}
+
+function DesignNotesPage() {
+  const [selected,setSelected] = useState<DesignSectionKey>("daily");
+  const [exportNotice,setExportNotice] = useState("");
+  const scenarioKey:ScenarioKey = selected==="architecture"?"daily":selected;
+  const scenario = skills.find(item=>item.key===scenarioKey) ?? skills[0];
+  const design = scenarioDesigns[scenarioKey];
+  const narrative = pageNarratives[scenarioKey];
+  const panelItems = getPanelItems(scenario,design);
+  const exportMarkdown=()=>{const bytes=downloadTextFile("\ufeff"+buildDesignMarkdown(),"场站AI页面与数据口径设计说明.md","text/markdown;charset=utf-8");setExportNotice(`已生成 Markdown · 整体架构 + 全部 12 页 · ${Math.ceil(bytes/1024)} KB`)};
+  const exportWord=()=>{const bytes=downloadTextFile("\ufeff"+buildDesignWordHtml(),"场站AI页面与数据口径设计说明.doc","application/msword;charset=utf-8");setExportNotice(`已生成 Word · 整体架构 + 全部 12 页 · ${Math.ceil(bytes/1024)} KB`)};
+  return <section className="design-page">
+    <header className="design-heading">
+      <div className="design-heading-icon">册</div>
+      <div><span>系统连接 / 设计说明</span><h1>页面、数据口径与整体架构</h1><p>包含 12 个页面的逐 Panel 数据设计,以及 Skill、MCP、Agent 整体架构。</p></div>
+      <div className="design-heading-actions"><div className="design-version"><small>口径版本</small><strong>V0.5 · 2026-08-22</strong></div><div className="design-export-actions"><button onClick={exportWord}>下载 Word</button><button className="primary" onClick={exportMarkdown}>导出 MD</button></div>{exportNotice&&<div className="export-notice" role="status">✓ {exportNotice}</div>}</div>
+    </header>
+    <div className="design-status-strip">
+      <div><i className="status-dot verified"/><span>已实现</span><strong>日报、问答、运行环比三页交互框架</strong></div>
+      <div><i className="status-dot source"/><span>真实接线</span><strong>资产组 / 节点;历史事件需有效认证实测</strong></div>
+      <div><i className="status-dot external"/><span>明确排除</span><strong>综合数据分析演示值、未接入运维与能耗指标</strong></div>
+    </div>
+    <div className="design-layout">
+      <aside className="design-index">
+        <div className="design-index-title"><span>设计说明章节</span><small>12 个场景页面 + 整体架构</small></div>
+        {skills.map((item,index)=><button key={item.key} className={selected===item.key?"active":""} onClick={()=>setSelected(item.key)}><b>{String(index+1).padStart(2,"0")}</b><span><strong>{item.title}</strong><small>{item.section}</small></span></button>)}
+        <button className={`architecture-index-button ${selected==="architecture"?"active":""}`} onClick={()=>setSelected("architecture")}><b>13</b><span><strong>整体架构</strong><small>Skill · MCP · Agent</small></span></button>
+      </aside>
+      <main className="design-content">
+        {selected==="architecture"?<ArchitectureChapter />:<>
+        <section className="feasibility-card">
+          <div className={`scenario-title-icon ${scenario.tone}`}>{scenario.icon}</div>
+          <div><span>可实现性结论</span><h2>{scenario.title}</h2><p>{design.feasibility}</p></div>
+          <em>{scenario.maturity}</em>
+        </section>
+        <section className="page-purpose-card">
+          <div className="purpose-card audience"><span>01</span><div><small>目标客户</small><h3>谁会使用这个页面</h3><p>{narrative.audience}</p></div></div>
+          <div className="purpose-card value"><span>02</span><div><small>页面价值</small><h3>为客户带来的收益</h3><p>{narrative.value}</p></div></div>
+          <div className="purpose-card ai"><span>03</span><div><small>采用 AI 的理由</small><h3>为什么不是普通报表</h3><p>{narrative.whyAI}</p></div></div>
+          <div className="purpose-card evidence"><span>04</span><div><small>可行性依据</small><h3>源站已经具备什么</h3><p>{narrative.feasibilityEvidence}</p></div></div>
+        </section>
+        <section className="design-method-card">
+          <div><small>场站级数据组织</small><p>{design.sourceText}</p></div>
+          <div><small>当前数据缺口</small><p>{design.gaps}</p></div>
+        </section>
+        <div className="design-section-title"><div><span>PANEL SPECIFICATION</span><h2>本页面 Panel 数据说明</h2></div><small>共 {panelItems.length} 个口径项</small></div>
+        <div className="panel-spec-list">
+          {panelItems.map((panel,index)=><article className="panel-spec-card" key={`${panel.name}-${index}`}>
+            <div className="panel-spec-head"><b>{String(index+1).padStart(2,"0")}</b><div><span>{panel.kind}</span><h3>{panel.name}</h3></div></div>
+            <div className="panel-spec-body">
+              <div><small>数据来源说明</small><p>{panel.source}</p></div>
+              <div><small>计算方法</small><p>{panel.calc}</p></div>
+              <div className="panel-api-block"><small>对应 API</small><div className="panel-api-list">{panel.apis.map(api=><div key={`${panel.name}-${api.path}`}><span className={`api-status ${api.status}`}>{apiStatusLabel[api.status]}</span><code>{api.method} {api.path}</code><p>{api.note}</p></div>)}</div></div>
+            </div>
+          </article>)}
+        </div>
+        <section className="design-footnote"><strong>统一实施原则</strong><p>所有数字先由 API 数据和确定性规则计算,再由 AI 解释;设备、测点只作为场站结论的证据下钻。源站“综合数据分析”演示页面不作为真实数据或可行性证据;控制相关接口首期只读,不自动下发指令。</p></section>
+        </>}
+      </main>
+    </div>
+  </section>
+}
+
+function DailyReportVisual({devices,selectedGroup,total,running,offline,warnings,faults,unknown,health,generatedAt,deviceDataMode}:{devices:Device[];selectedGroup:AssetGroup;total:number;running:number;offline:number;warnings:number;faults:number;unknown:number;health:number;generatedAt:string;deviceDataMode:"api"|"snapshot"}) {
+  const abnormal = devices.filter(device=>["故障","告警","离线"].includes(device.status)).sort((a,b)=>a.score-b.score);
+  const statusRows = [
+    {label:"运行",value:running,tone:"running"},
+    {label:"告警",value:warnings,tone:"warning"},
+    {label:"故障",value:faults,tone:"fault"},
+    {label:"离线",value:offline,tone:"offline"},
+    {label:"待补齐",value:unknown,tone:"unknown"},
+  ];
+  const conclusion = !total ? "等待数据" : faults>0 || offline>=Math.max(2,total*.2) ? "需重点关注" : warnings>0 ? "总体可控" : "运行平稳";
+  const stationLabel = selectedGroup.orgName || (selectedGroup.name==="水务资产组"?"良泉水厂":"按资产组范围");
+  return <>
+    <section className="scenario-section report-preview live-daily-report">
+      <div className="section-heading"><div><span>STATION DAILY BRIEF</span><h2>{selectedGroup.name} · 站点智能日报</h2></div><small>{new Date().toLocaleDateString("zh-CN")} · {generatedAt} 生成</small></div>
+      <div className="daily-report-overview">
+        <div className="report-cover"><span>今日运行结论</span><strong>{conclusion}</strong><p>Demo Org / {stationLabel}<br/>纳管 {total||"—"} 台,当前故障 {faults} 台、告警 {warnings} 台、离线 {offline} 台。</p><em>{deviceDataMode==="api"?"节点 API 实时数据":devices.length?"源站可见快照 · 非模拟数据":"已确认总数 · 节点明细待接口返回"}</em></div>
+        <div className="daily-health-block"><div className="daily-health-ring" style={{"--health":`${health}%`} as CSSProperties}><strong>{health}</strong><span>运行健康指数</span></div><div className="daily-status-distribution">{statusRows.map(row=><div key={row.label}><span>{row.label}</span><i><b className={row.tone} style={{width:`${total?Math.max(3,row.value/total*100):0}%`}}/></i><strong>{row.value}</strong></div>)}</div></div>
+      </div>
+      <div className="report-data-boundary"><strong>数据边界</strong><span>{total?"设备状态与数量已纳入日报":"资产组已切换,节点数据待返回"}</span><span className="pending">未闭环报警:待报警流程 API</span><span className="pending">工单 / 保养:待运维 API</span></div>
+    </section>
+    <section className="scenario-section daily-exception-section">
+      <div className="section-heading"><div><span>TRACEABLE EVIDENCE</span><h2>异常设备证据清单</h2></div><small>按故障 → 告警 → 离线排序 · 共 {abnormal.length} 台</small></div>
+      {abnormal.length?<div className="daily-device-table"><div className="daily-device-head"><span>设备</span><span>站点层级</span><span>类型</span><span>状态</span><span>日报建议</span></div>{abnormal.slice(0,12).map(device=><div key={device.id}><strong>{device.name}</strong><span>{device.site}</span><span>{device.type}</span><em className={`device-status ${device.status}`}>{device.status}</em><span>{device.status==="故障"?"立即确认影响范围并建立处置任务":device.status==="告警"?"核查关联测点与持续时间":"核查通信、供电及维护状态"}</span></div>)}</div>:<div className="empty-daily-state"><strong>尚未取得异常设备</strong><p>切换资产组后会按该组节点清单重新计算,不沿用其他资产组数据。</p></div>}
+    </section>
+  </>;
+}
+
+function StationQaVisual({devices,selectedGroup,total,running,offline,warnings,faults,unknown,deviceDataMode}:{devices:Device[];selectedGroup:AssetGroup;total:number;running:number;offline:number;warnings:number;faults:number;unknown:number;deviceDataMode:"api"|"snapshot"}) {
+  const defaultQuestion = "本站当前最需要关注什么?";
+  const [question,setQuestion] = useState(defaultQuestion);
+  const [lastQuestion,setLastQuestion] = useState("");
+  const [answer,setAnswer] = useState("");
+  const [loading,setLoading] = useState(false);
+  const [answerMeta,setAnswerMeta] = useState("等待提问");
+  const stationName = selectedGroup.orgName || (selectedGroup.name==="水务资产组"?"良泉水厂":"按资产组范围");
+  const suggestions = ["本站当前有哪些故障和告警设备?","哪些设备离线,集中在哪个泵站?","当前数据能否发布正式日报?"];
+  const abnormal = devices.filter(device=>["故障","告警","离线"].includes(device.status));
+  const evidenceDevices = (()=>{
+    if (/离线/.test(lastQuestion)) return devices.filter(device=>device.status==="离线");
+    if (/故障/.test(lastQuestion)) return devices.filter(device=>device.status==="故障"||device.status==="告警");
+    if (/告警|异常/.test(lastQuestion)) return devices.filter(device=>device.status==="告警"||device.status==="故障");
+    if (/在线|运行/.test(lastQuestion)) return devices.filter(device=>device.status==="运行"||device.status==="正常");
+    return abnormal;
+  })().slice(0,8);
+
+  useEffect(()=>{
+    setQuestion(defaultQuestion);
+    setLastQuestion("");
+    setAnswer("");
+    setAnswerMeta("已切换资产组 · 等待提问");
+  },[selectedGroup.id]);
+
+  async function askStation(input:string=question) {
+    const prompt = input.trim();
+    if (!prompt || loading) return;
+    setQuestion(prompt);
+    setLastQuestion(prompt);
+    setLoading(true);
+    setAnswerMeta("正在检索当前资产组数据…");
+    try {
+      const result = await analyzeModule("station-qa",prompt,{
+        assetGroup:{id:selectedGroup.id,name:selectedGroup.name,organization:"Demo Org",station:stationName},
+        statistics:{total,running,warnings,faults,offline,unknown,dataMode:deviceDataMode},
+        devices,
+        apiSources:[
+          "GET {APM}/api/v1/asset/group/list?count=1000",
+          `GET {APM}/api/v1/asset/node/list?groupId=${selectedGroup.id}&groupName=${selectedGroup.name}&index=0&count=10000`,
+        ],
+        unavailableDomains:["报警流程闭环","工单","保养"],
+      });
+      setAnswer(result.fallback?buildStationQaFallback(prompt,devices,selectedGroup,total,unknown):(result.analysis||"没有生成可用回答,请换一种问法。"));
+      setAnswerMeta(result.fallback?"本地规则回答 · 数据可追溯":`AI 模型 ${result.model||""} · 数据可追溯`);
+    } catch (error) {
+      setAnswer(`问答服务暂不可用:${error instanceof Error?error.message:"请求失败"}。当前资产组仍可查看右侧节点证据。`);
+      setAnswerMeta("服务调用失败 · 未生成推测内容");
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  return <section className="scenario-section station-qa-live">
+    <div className="section-heading qa-live-heading"><div><span>STATION Q&A · LIVE</span><h2>{selectedGroup.name}场站问答</h2><p>回答范围固定为 Demo Org / {stationName} / {selectedGroup.name}</p></div><small>{deviceDataMode==="api"?"节点 API 实时上下文":"源站可见快照上下文"}</small></div>
+    <div className="station-qa-shell">
+      <div className="station-qa-conversation">
+        <div className="qa-scope-strip"><span>已锁定资产组</span><strong>{selectedGroup.name}</strong><em>纳管 {total||"—"} 台 · 已加载 {devices.length} 条设备证据</em></div>
+        <div className="qa-question-box"><textarea aria-label="场站问题" value={question} onChange={event=>setQuestion(event.target.value)} onKeyDown={event=>{if(event.key==="Enter"&&!event.shiftKey){event.preventDefault();askStation();}}} placeholder="例如:本站有哪些离线设备,集中在哪个泵站?"/><button onClick={()=>askStation()} disabled={loading||!question.trim()}>{loading?"分析中":"提问 →"}</button></div>
+        <div className="qa-quick-questions">{suggestions.map(item=><button key={item} onClick={()=>askStation(item)} disabled={loading}>{item}</button>)}</div>
+        <div className={`qa-live-answer ${answer?"answered":"empty"}`}><div className="qa-ai-avatar">AI</div><div><div className="qa-answer-meta"><strong>{answer?lastQuestion:"可以问我当前场站的运行与异常情况"}</strong><span>{answerMeta}</span></div><p>{answer||`当前已准备 ${devices.length} 条节点证据。回答只使用当前资产组数据,不会引用其他站点,也不会编造尚未接入的报警闭环、工单或保养数量。`}</p>{answer&&<div className="qa-answer-citations"><span>资产组清单</span><span>节点状态 {devices.length}</span><span>数据完整度 {Math.max(0,total-unknown)}/{total||0}</span></div>}</div></div>
+      </div>
+      <aside className="station-qa-evidence">
+        <div className="qa-evidence-title"><div><span>EVIDENCE</span><h3>回答证据</h3></div><em>{evidenceDevices.length} 条相关节点</em></div>
+        <div className="qa-evidence-list">{evidenceDevices.length?evidenceDevices.map(device=><div key={device.id}><i className={`qa-status-dot ${device.status}`}/><div><strong>{device.name}</strong><span>{device.site}</span></div><em className={`device-status ${device.status}`}>{device.status}</em></div>):<div className="qa-no-evidence"><strong>暂无匹配节点</strong><span>{lastQuestion?"当前问题没有匹配到设备状态证据":"提问后将自动筛选相关证据"}</span></div>}</div>
+        <div className="qa-api-trace"><strong>实际数据来源</strong><div><span className="verified">已验证</span><code>GET /api/v1/asset/group/list</code></div><div><span className={deviceDataMode==="api"?"verified":"snapshot"}>{deviceDataMode==="api"?"已加载":"已接线"}</span><code>GET /api/v1/asset/node/list</code></div><div><span className="pending">待映射</span><code>GET /api/v1/event/latest</code></div></div>
+      </aside>
+    </div>
+  </section>;
+}
+
+function PeriodComparisonVisual({selectedGroup,muConfig,muSecret}:{selectedGroup:AssetGroup;muConfig:MuConfig;muSecret:string}) {
+  const [data,setData] = useState<PeriodComparisonResponse|null>(null);
+  const [loading,setLoading] = useState(true);
+  const [error,setError] = useState("");
+  const [analysis,setAnalysis] = useState("");
+  const [analysisMeta,setAnalysisMeta] = useState("");
+
+  async function loadComparison() {
+    setLoading(true);
+    setError("");
+    setAnalysis("");
+    try {
+      const result = await loadPeriodComparison(muConfig,muSecret,selectedGroup.source==="api"?selectedGroup.id:"",selectedGroup.name);
+      setData(result);
+      if (!result.ok) {
+        setError(result.message||"运行环比接口未返回可用数据");
+        return;
+      }
+      setLoading(false);
+      setAnalysisMeta("正在生成场站级解释…");
+      const insight = await analyzeModule("period-analysis","解释当前资产组本月与上月同期的运行变化,指出主要贡献层级和建议动作。",result);
+      setAnalysis(insight.analysis||"当前没有生成分析结论");
+      setAnalysisMeta(insight.fallback?"确定性规则分析":"AI 模型分析");
+    } catch (reason) {
+      setError(reason instanceof Error?reason.message:"运行环比加载失败");
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  useEffect(()=>{ loadComparison(); },[selectedGroup.id,selectedGroup.name]);
+
+  const current = data?.current;
+  const previous = data?.previous;
+  const deltas = data?.deltas;
+  const deltaText = (value:number|null|undefined,suffix="%") => value==null?"无上月基数":`${value>0?"+":""}${value}${suffix}`;
+  const deltaTone = (value:number|null|undefined,lowerIsBetter=false) => value==null?"neutral":(lowerIsBetter?value<=0:value>=0)?"positive":"negative";
+  const metricRows = current&&previous ? [
+    {label:"事件影响可用率(估算)",current:current.availabilityEstimate,previous:previous.availabilityEstimate,unit:"%",delta:deltas?.availabilityEstimate,deltaUnit:" 个百分点",lower:false},
+    {label:"历史事件",current:current.eventCount,previous:previous.eventCount,unit:" 条",delta:deltas?.eventCount,deltaUnit:"%",lower:true},
+    {label:"影响节点",current:current.affectedNodes,previous:previous.affectedNodes,unit:" 台",delta:deltas?.affectedNodes,deltaUnit:"%",lower:true},
+    {label:"事件影响时长",current:current.impactHours,previous:previous.impactHours,unit:" h",delta:deltas?.impactHours,deltaUnit:"%",lower:true},
+  ] : [];
+
+  return <section className="period-live-panel">
+    <div className="period-toolbar">
+      <div><span>REAL API · MONTH OVER MONTH</span><h2>{selectedGroup.name}运行环比</h2><p>本月截至当前 vs 上月同期,保证比较窗口长度一致</p></div>
+      <div className="period-toolbar-actions"><small>{loading?"正在读取真实历史数据":data?.periods?`${data.periods.current.label} / ${data.periods.previous.label}`:"等待数据"}</small><button onClick={loadComparison} disabled={loading}>{loading?"分析中…":"刷新分析"}</button></div>
+    </div>
+
+    {error&&<div className="period-state error"><strong>真实 API 暂不可用</strong><span>{error}</span><small>页面没有使用示例环比值;请到“数据连接”更新原始请求或 Token 后重试。</small></div>}
+    {!error&&loading&&<div className="period-state loading"><strong>正在建立本期与基期数据集</strong><span>资产组 → 节点范围 → 本月事件 → 上月同期事件 → 场站聚合</span></div>}
+    {!loading&&data?.ok&&<>
+      <div className={`period-scope-status ${data.historyScoped?"verified":"warning"}`}><div><strong>{data.historyScoped?"资产组范围已验证":"历史事件范围仍需映射"}</strong><span>{data.message}</span></div><em>{data.historyScoped?"REAL DATA":"DATA GAP"}</em></div>
+
+      <div className="period-kpi-grid">
+        {metricRows.map(row=><article key={row.label}><span>{row.label}</span><div><strong>{row.current==null?"—":`${row.current}${row.unit}`}</strong><small>上月同期 {row.previous==null?"—":`${row.previous}${row.unit}`}</small></div><em className={deltaTone(row.delta,row.lower)}>{deltaText(row.delta,row.deltaUnit)}</em></article>)}
+      </div>
+
+      <div className="period-analysis-grid">
+        <section className="period-chart-card">
+          <div className="period-card-heading"><div><span>PERIOD COMPARISON</span><h3>本月与上月同期</h3></div><div className="compare-legend"><span><i className="current"/>本月</span><span><i className="baseline"/>上月同期</span></div></div>
+          {metricRows.map(row=>{const max=Math.max(Number(row.current)||0,Number(row.previous)||0,1);return <div className="period-compare-row" key={row.label}><strong>{row.label}</strong><div><i className="baseline" style={{width:`${Math.max(2,(Number(row.previous)||0)/max*100)}%`}}/><i className="current" style={{width:`${Math.max(2,(Number(row.current)||0)/max*100)}%`}}/></div><span>{row.current==null?"—":row.current}</span></div>})}
+          <p className="period-method-note">可用率为“1 − 事件影响时长 ÷(纳管节点数 × 比较窗口小时)”的场站运行估算;事件重叠未去重时可能偏保守。</p>
+        </section>
+
+        <section className="period-contribution-card">
+          <div className="period-card-heading"><div><span>CONTRIBUTION</span><h3>本月事件贡献层级</h3></div><small>{data.siteContributions?.length||0} 个层级</small></div>
+          <div className="period-contribution-list">{data.siteContributions?.length?data.siteContributions.map((site,index)=>{const max=data.siteContributions?.[0]?.eventCount||1;return <div key={`${site.name}-${index}`}><b>{index+1}</b><span title={site.name}>{site.name}</span><i><strong style={{width:`${site.eventCount/max*100}%`}}/></i><em>{site.eventCount} 条</em></div>}):<div className="period-empty">本月当前资产组没有可归属的历史事件</div>}</div>
+        </section>
+      </div>
+
+      <section className="period-device-card">
+        <div className="period-card-heading"><div><span>TRACEABLE EVIDENCE</span><h3>高频影响节点</h3></div><small>按本月事件数排序</small></div>
+        <div className="period-device-table"><div className="head"><span>节点</span><span>场站层级</span><span>事件数</span><span>高优先级</span></div>{data.topDevices?.length?data.topDevices.map(device=><div key={device.id}><strong>{device.name}</strong><span>{device.site}</span><em>{device.eventCount}</em><b>{device.highPriority}</b></div>):<div className="period-empty">没有可展示的节点级事件证据</div>}</div>
+      </section>
+
+      <section className="period-ai-card"><div className="period-ai-title"><span>AI</span><div><strong>运行变化解读</strong><small>{analysisMeta||"等待分析"}</small></div><button onClick={loadComparison} disabled={loading}>重新计算并分析</button></div><p>{analysis||"正在基于真实环比结果生成场站级解释…"}</p></section>
+
+      <section className="period-api-trace"><strong>本次真实请求</strong><div>{data.endpoints?.map((endpoint,index)=><span key={`${endpoint.path}-${index}`} className={endpoint.ok?"ok":"bad"}><b>{endpoint.ok?`HTTP ${endpoint.status}`:`失败 ${endpoint.status||"—"}`}</b><code>{endpoint.path}</code><em>{endpoint.rowCount??0} 行</em></span>)}</div></section>
+    </>}
+  </section>;
+}
+
+function ScenarioVisual({scenario,devices,alarms,selectedGroup,total,running,offline,warnings,faults,unknown,health,generatedAt,deviceDataMode,muConfig,muSecret}:{scenario:ScenarioDefinition;devices:Device[];alarms:number;selectedGroup:AssetGroup;total:number;running:number;offline:number;warnings:number;faults:number;unknown:number;health:number;generatedAt:string;deviceDataMode:"api"|"snapshot";muConfig:MuConfig;muSecret:string}) {
+  if (scenario.key === "period") return <PeriodComparisonVisual selectedGroup={selectedGroup} muConfig={muConfig} muSecret={muSecret}/>;
+  if (scenario.visual === "qa") return <StationQaVisual devices={devices} selectedGroup={selectedGroup} total={total} running={running} offline={offline} warnings={warnings} faults={faults} unknown={unknown} deviceDataMode={deviceDataMode}/>;
+
+  if (scenario.visual === "report") return <DailyReportVisual devices={devices} selectedGroup={selectedGroup} total={total} running={running} offline={offline} warnings={warnings} faults={faults} unknown={unknown} health={health} generatedAt={generatedAt} deviceDataMode={deviceDataMode}/>;
+
+  if (scenario.visual === "compare") return <section className="scenario-section comparison-preview">
+    <div className="section-heading"><div><span>OPERATIONS COMPARE</span><h2>{scenario.key==="benchmark"?"同类场站标准化对比":"本月与上月运营对比"}</h2></div><small>已按容量与负荷归一化</small></div>
+    <div className="compare-legend"><span><i className="current"/>本期 / 本站</span><span><i className="baseline"/>基期 / 同类中位</span></div>
+    {["运行可用性","单位产出能效","报警闭环","运维履约"].map((label,index)=><div className="compare-row" key={label}><strong>{label}</strong><div><i className="baseline" style={{width:`${[78,72,69,74][index]}%`}}/><i className="current" style={{width:`${scenario.bars[index]}%`}}/></div><b>{scenario.bars[index]}</b></div>)}
+  </section>;
+
+  if (scenario.visual === "operations") return <section className="scenario-section operations-preview">
+    <div className="section-heading"><div><span>O&M DELIVERY</span><h2>今日运维履约漏斗</h2></div><small>计划 → 响应 → 处置 → 验证</small></div>
+    <div className="operations-funnel">{[["计划任务","10","100%"],["按时响应","9","90%"],["完成处置","8","80%"],["效果验证","7","70%"]].map(([name,count,width])=><div key={name} style={{width}}><span>{name}</span><b>{count}</b></div>)}</div>
+    <div className="sla-track"><span>首次响应</span><i><b style={{width:"92%"}}/></i><strong>92%</strong><span>一次修复</span><i><b style={{width:"84%"}}/></i><strong>84%</strong></div>
+  </section>;
+
+  if (scenario.visual === "health") return <section className="scenario-section health-preview">
+    <div className="section-heading"><div><span>STATION HEALTH</span><h2>六维运营健康度</h2></div><small>总分可解释、可下钻</small></div>
+    <div className="health-layout"><div className="health-ring"><strong>86</strong><span>良好</span></div><div className="health-dimensions">{[["运行可用性",94],["报警风险",78],["运维履约",88],["能效与产出",76],["数据可信度",91],["安全合规",96]].map(([label,value])=><div key={String(label)}><span>{label}</span><i><b style={{width:`${value}%`}}/></i><strong>{value}</strong></div>)}</div></div>
+  </section>;
+
+  if (scenario.visual === "alarm") return <section className="scenario-section alarm-preview-new">
+    <div className="section-heading"><div><span>EVENT CORRELATION</span><h2>27 条报警归并为 3 个场站事件</h2></div><small>压缩率 88.9%</small></div>
+    <div className="event-clusters">{[["冷却水能力下降","16 条关联报警","P2","38 分钟"],["通信质量波动","8 条关联报警","P3","12 分钟"],["环境温度短时越限","3 条关联报警","P3","已恢复"]].map((row,index)=><div key={row[0]} className={index===0?"active":""}><b>{index+1}</b><div><strong>{row[0]}</strong><small>{row[1]}</small></div><span>{row[2]}</span><em>{row[3]}</em></div>)}</div>
+  </section>;
+
+  if (scenario.visual === "efficiency") return <section className="scenario-section efficiency-preview">
+    <div className="section-heading"><div><span>RESOURCE EFFICIENCY</span><h2>单位产出能耗与偏差贡献</h2></div><small>同工况基线 = 100</small></div>
+    <div className="efficiency-layout"><div className="efficiency-score"><strong>93.8</strong><span>综合能效指数</span><small>低于基线 6.2%</small></div><div className="contribution-list">{[["冷却塔",38],["二次泵",33],["冷机",18],["末端空调",11]].map(([label,value])=><div key={String(label)}><span>{label}</span><i><b style={{width:`${value}%`}}/></i><strong>{value}%</strong></div>)}</div></div>
+  </section>;
+
+  if (scenario.visual === "strategy") return <section className="scenario-section strategy-preview-new">
+    <div className="section-heading"><div><span>OPERATION STRATEGY</span><h2>场站协同运行建议</h2></div><small>当前仅展示,不执行</small></div>
+    <div className="strategy-proposal"><div><span>建议策略 S-0822-03</span><strong>低负荷窗口调整冷源组合</strong><p>预计节能 3.2%,机柜进风温度保持在安全范围,并保留 N+1 冗余。</p></div><b>低风险</b></div>
+    <div className="strategy-flow">{["AI 建议","收益预测","约束校验","仿真","人工审批","执行验证"].map((step,index)=><div key={step} className={index<3?"done":""}><b>{index<3?"✓":index+1}</b><span>{step}</span></div>)}</div>
+  </section>;
+
+  if (scenario.visual === "emergency") return <section className="scenario-section emergency-preview-new">
+    <div className="section-heading"><div><span>INCIDENT RESPONSE</span><h2>最近一次场站事件处置时间线</h2></div><small>事件已闭环</small></div>
+    <div className="incident-banner"><span>影响等级 P2</span><div><strong>冷却冗余能力下降</strong><p>未影响核心业务 · 持续 38 分钟</p></div><b>已恢复</b></div>
+    <div className="incident-timeline">{[["10:15","事件触发","影响分析完成"],["10:21","备用路径启用","SOP 4/8"],["10:37","指标恢复","进入观察期"],["10:53","事件关闭","生成 3 项改进任务"]].map(([time,title,detail],index)=><div key={time}><i className={index===3?"done":""}/><b>{time}</b><span><strong>{title}</strong><small>{detail}</small></span></div>)}</div>
+  </section>;
+
+  return <section className={`scenario-section ${scenario.visual}-preview`}>
+    <div className="section-heading"><div><span>{scenario.visual==="capacity"?"CAPACITY FORECAST":"RISK FORECAST"}</span><h2>{scenario.visual==="capacity"?"未来 24 小时负荷与能力裕度":"未来 24 小时场站风险趋势"}</h2></div><small>预测区间 · 置信度 82%</small></div>
+    <div className="forecast-chart"><div className="forecast-limit">安全边界</div>{scenario.bars.map((value,index)=><div key={index}><i style={{height:`${value}%`}} className={value>75?"risk":""}/><span>{String(index*3).padStart(2,"0")}:00</span></div>)}</div>
+    <div className="forecast-note"><b>{scenario.visual==="capacity"?"能力裕度最低 16%":"14:00—17:00 风险上升"}</b><span>{scenario.insights[0]}</span></div>
+  </section>;
+}
+
+function HomePanel({onOpen,connection,devices,alarms,onAnalyze,summary,loading}:{onOpen:(k:SkillKey)=>void;connection:ConnectionState;devices:Device[];alarms:number;onAnalyze:()=>void;summary:string;loading:boolean}) {
+  return <section className="content">
+    <div className="hero"><span className="eyebrow">设备监控 · AI 工作台</span><h1>今天需要 AI 帮你处理什么?</h1><p>面向燃气站、水站、污水处理厂、新能源场站与 AIDC 的设备运行助手。</p><button className="ask-launch" onClick={()=>onOpen("qa")}><span>输入问题,或选择一个站点 AI 技能</span><b>↑</b></button></div>
+    <div className="skill-grid">{skills.map(s=><button key={s.key} className="skill-card" onClick={()=>onOpen(s.key)}><span className={`skill-icon ${s.tone}`}>{s.icon}</span><span className="skill-copy"><strong>{s.title}</strong><small>{s.short}</small></span><span className="arrow">→</span></button>)}</div>
+    <div className="overview-row"><div><span>数据状态</span><strong>{connection==="live"?"实时连接":"安全预览"}</strong></div><div><span>设备数</span><strong>{devices.length}</strong></div><div><span>需关注</span><strong className="danger-text">{alarms}</strong></div><div><span>控制边界</span><strong>建议 → 仿真 → 审批</strong></div></div>
+    <div className="callout blue-line" style={{marginTop:18}}>
+      <strong>AI 实时分析(CodeBuddy · hy3)</strong>
+      <p>基于已加载的真实设备与告警数据,调用 CodeBuddy(默认模型 hy3)生成整体分析结论。未配置密钥时自动回退到本地规则分析。</p>
+      <button className="primary" onClick={onAnalyze} disabled={loading}>{loading?"分析中…":"AI 分析实时数据"}</button>
+      {summary && <div className="ai-answer" style={{whiteSpace:"pre-wrap",marginTop:12,lineHeight:1.7}}>{summary}</div>}
+    </div>
+  </section>
+}
+
+function MuSettings({
+  config,setConfig,secret,setSecret,connection,detail,endpoints,sentHeaders,onTest,onSave,onReset,
+}:{
+  config:MuConfig;
+  setConfig:(value:MuConfig)=>void;
+  secret:string;
+  setSecret:(value:string)=>void;
+  connection:ConnectionState;
+  detail:string;
+  endpoints:Array<{path:string;status:number;ok:boolean;snippet?:string;error?:string}>;
+  sentHeaders:string[];
+  onTest:()=>void;
+  onSave:()=>void;
+  onReset:()=>void;
+}) {
+  const change = <K extends keyof MuConfig>(key:K,value:MuConfig[K])=>setConfig({...config,[key]:value});
+  const secretLabel = config.authMode==="bearer" ? "访问令牌" : config.authMode==="eitoken" ? "EIToken" : "密码";
+  return <section className="settings-page">
+    <div className="settings-main">
+      <div className="settings-heading">
+        <span className="settings-orb">⚙</span>
+        <div><span>数据连接</span><h1>MU 数据源配置</h1><p>配置原始 MU 站点位置、组织信息和认证方式。连接成功后,AI 页面会使用真实设备、告警和测点数据。</p></div>
+      </div>
+      <div className="settings-card">
+        <div className="settings-card-title"><b>1</b><div><strong>站点位置</strong><small>填写 MU 门户的根地址,不要包含 # 后面的页面路由。</small></div></div>
+        <label className="field full-field"><span>MU 站点地址</span><input value={config.baseUrl} onChange={e=>change("baseUrl",e.target.value)} placeholder="https://your-mu-site.example.com"/></label>
+        <label className="field full-field"><span>APM API 地址(可选)</span><input value={config.apiBaseUrl} onChange={e=>change("apiBaseUrl",e.target.value)} placeholder="自动推导:https://portal-apm-.../api-apm"/><small className="secret-note">通常留空即可。后端会从 Desk 门户地址自动推导;只有特殊部署才需要手工覆盖。</small></label>
+        <div className="field-grid">
+          <label className="field"><span>租户 / Tenant(可选)</span><input value={config.tenant} onChange={e=>change("tenant",e.target.value)} placeholder="例如 Tenant001"/></label>
+          <label className="field"><span>组织编码(可选)</span><input value={config.orgCode} onChange={e=>change("orgCode",e.target.value)} placeholder="例如 Demo Org"/></label>
+        </div>
+      </div>
+      <div className="settings-card">
+        <div className="settings-card-title"><b>2</b><div><strong>认证方式</strong><small>优先选择现有登录态;跨域或独立部署时可使用网关认证。</small></div></div>
+        <div className="auth-options">
+          {[
+            ["session","沿用浏览器登录","仅适合同域部署;localhost 无法读取 MU 登录态"],
+            ["basic","HTTP Basic","仅适合网关明确支持 Basic Auth 的专用账号"],
+            ["bearer","Bearer Token","适合已有 API 访问令牌"],
+            ["eitoken","EIToken Cookie","wise-insightapm 专用:支持 Bearer 前缀、裸 Token、EIToken=xxx 或整段 Cookie"],
+            ["login","登录接口","仅适合具有独立登录 API 的站点;门户 SSO 不适用"],
+          ].map(([value,title,desc])=><button key={value} className={config.authMode===value?"active":""} onClick={()=>{change("authMode",value as AuthMode);setSecret("")}}><i>{config.authMode===value?"✓":""}</i><strong>{title}</strong><small>{desc}</small></button>)}
+        </div>
+        {config.authMode!=="session" && <div className="credentials">
+          {config.authMode!=="bearer" && config.authMode!=="eitoken" && <label className="field"><span>账号</span><input autoComplete="username" value={config.username} onChange={e=>change("username",e.target.value)} placeholder="MU 登录账号"/></label>}
+          <label className="field"><span>{secretLabel}</span><input type={config.authMode==="eitoken"?"text":"password"} autoComplete="current-password" value={secret} onChange={e=>setSecret(e.target.value)} placeholder={config.authMode==="bearer"?"粘贴访问令牌":config.authMode==="eitoken"?"推荐粘贴整套 Cookie(见下);也可只贴 EIToken(裸 JWT / Bearer xxx / EIToken=xxx)":"仅在当前页面内存中使用"}/><small className="secret-note">最稳妥:在站点 F12→Network 里点任意 /selfAPM/ 请求→Request Headers→复制整行 <b>Cookie:</b> 值(含 ; 分隔的一整串)粘到这里。后端会原样发送并自动从 EIToken 推 Bearer。仅存于内存。</small></label>
+          {config.authMode==="eitoken" && <div className="field-grid">
+            <label className="field"><span>组织 ID(org-id,可选)</span><input value={config.orgId} onChange={e=>change("orgId",e.target.value)} placeholder="浏览器 F12→Application→Local Storage→orgMapList[0].id"/></label>
+            <label className="field"><span>租户 ID(Tenant-Id,可选)</span><input value={config.tenantId} onChange={e=>change("tenantId",e.target.value)} placeholder="同上取 tenantId"/></label>
+          </div>}
+        </div>}
+        <div className="raw-request-box">
+          <div className="settings-card-title"><b>★</b><div><strong>原始请求(本地连接推荐)</strong><small>在目标站点 F12→Network 中选择发往 <code>portal-apm-...</code>、路径包含 <code>/api-apm/api/v1/</code> 的请求,右键 Copy → Copy as cURL (bash),把整段命令粘贴到下方。后端只提取认证与组织请求头,不保存凭据。</small></div></div>
+          <label className="field full-field"><span>原始请求(curl 命令或请求头)</span><textarea className="raw-curl" value={config.rawCurl} onChange={e=>change("rawCurl",e.target.value)} placeholder={"curl 'https://portal-apm-desk-cluster01.cn.wise-insightapm.com/api-apm/api/v1/asset/group/list?count=1000' -H 'Authorization: Bearer eyJ...' -H 'org-id: ...' -H 'Cookie: EIToken=...; ...'"}/></label>
+        </div>
+        {config.authMode==="login" && <div className="login-api-box">
+          <label className="field full-field"><span>登录接口路径</span><input value={config.loginPath} onChange={e=>change("loginPath",e.target.value)} placeholder="/api/v1/auth/login"/></label>
+          <div className="field-grid">
+            <label className="field"><span>账号字段名</span><input value={config.usernameField} onChange={e=>change("usernameField",e.target.value)} placeholder="username"/></label>
+            <label className="field"><span>密码字段名</span><input value={config.passwordField} onChange={e=>change("passwordField",e.target.value)} placeholder="password"/></label>
+          </div>
+        </div>}
+      </div>
+      <div className="settings-actions">
+        <button className="ghost-button" onClick={onReset}>恢复默认</button>
+        <button className="soft-button" onClick={onSave}>保存当前会话配置</button>
+        <button className="primary test-button" onClick={onTest} disabled={connection==="connecting"}>{connection==="connecting"?"正在测试连接…":"测试连接并加载数据"}</button>
+      </div>
+    </div>
+    <aside className="settings-side">
+      <h2>连接状态</h2>
+      <div className={`connection-result ${connection}`}><span className="dot"/><div><strong>{connection==="live"?"连接成功":connection==="connecting"?"正在连接":"尚未连接"}</strong><p>{detail}</p></div></div>
+      <h3>将验证以下接口</h3>
+      <div className="endpoint-list">{API_ENDPOINTS.map(path=><code key={path}>{path}</code>)}</div>
+      {sentHeaders.length > 0 && (
+        <div className="endpoint-results">
+          <h3>本次发出的请求头</h3>
+          <div className="ep-headers">{sentHeaders.map(h=><code key={h}>{h}</code>)}</div>
+        </div>
+      )}
+      {endpoints.length > 0 && (
+        <div className="endpoint-results">
+          <h3>本次探测结果</h3>
+          {endpoints.map(e=>(
+            <div key={e.path} className={`ep-row ${e.ok?"ok":"fail"}`}>
+              <code className="ep-path">{e.path}</code>
+              <span className="ep-status">{e.status ? `HTTP ${e.status}` : "连接失败"}</span>
+              {e.snippet && e.ok && <small className="ep-snippet">{e.snippet}</small>}
+              {e.error && <small className="ep-error">{e.error}</small>}
+            </div>
+          ))}
+        </div>
+      )}
+      <div className="security-card">
+        <strong>安全说明</strong>
+        <ul><li>密码和令牌仅保存在页面内存中。</li><li>站点地址与非敏感配置只保存到当前标签页会话。</li><li>仅允许 HTTPS 域名,代理不会转发到任意 IP 地址。</li><li>正式环境建议部署为 MU 同域微应用或使用专用 BFF。</li></ul>
+      </div>
+      <a className="open-configured" href={`${config.baseUrl.replace(/\/$/,"")}/#/MU/deviceMonitoring/deviceMonitoring/overview`} target="_blank" rel="noreferrer">在新标签页打开该 MU 站点 ↗</a>
+    </aside>
+  </section>
+}
+
+function QaPanel({query,setQuery,asked,setAsked,devices,alarms,onSend,aiAnswer,loading}:{query:string;setQuery:(s:string)=>void;asked:boolean;setAsked:(b:boolean)=>void;devices:Device[];alarms:Device[];onSend:()=>void;aiAnswer:string;loading:boolean}) {
+  const renderBody = () => {
+    if (!asked) return (
+      <div className="starter-questions">
+        <button onClick={()=>setQuery("统计各站点离线设备和高优先级告警")}>统计各站点离线设备和高优先级告警</button>
+        <button onClick={()=>setQuery("分析 AIDC 冷却系统温差和泵组状态")}>分析 AIDC 冷却系统温差和泵组状态</button>
+        <button onClick={()=>setQuery("找出最近 7 天健康度下降最快的设备")}>找出健康度下降最快的设备</button>
+      </div>
+    );
+    if (loading) return <p className="answer-lead">AI 正在分析实时数据…</p>;
+    if (aiAnswer) return <div className="ai-answer" style={{whiteSpace:"pre-wrap",lineHeight:1.7}}>{aiAnswer}</div>;
+    return (
+      <>
+        <p className="answer-lead">已找到 <strong>{alarms.length}</strong> 台需要优先关注的设备,并按风险排序。</p>
+        <div className="result-list">{alarms.slice(0,4).map((d,i)=><div className="result-card" key={d.id}><span className="rank">{i+1}</span><div><strong>{d.site} · {d.name}</strong><small>{d.status} · {d.alarm} · 当前值 {d.metric}</small></div><span className={`severity ${i<2?"high":"mid"}`}>{i<2?"高":"中"}</span></div>)}</div>
+        <div className="action-row"><button>查看告警证据链</button><button>生成处置清单</button><button>加入交接班摘要</button></div>
+      </>
+    );
+  };
+  return (
+    <div className="panel-body">
+      <div className="prompt-bubble">{asked?query:"可直接询问设备、站点、告警和测点关系"}</div>
+      {renderBody()}
+      <div className="composer"><textarea value={query} onChange={e=>setQuery(e.target.value)} aria-label="设备问题"/><button onClick={()=>{setAsked(true);onSend();}}>发送</button></div>
+      <div className="ai-note">AI 结论仅作为辅助,关键操作需人工确认</div>
+    </div>
+  );
+}
+
+
+function ReportPanel({devices,alarms}:{devices:Device[];alarms:Device[]}) { const [generated,setGenerated]=useState(true); return <div className="panel-body"><div className="prompt-bubble">生成今天的站点运行日报</div><p className="answer-lead">日报大纲已生成,可在右侧调整章节并导出 Word。</p><div className="callout blue-line"><strong>执行摘要</strong><p>覆盖 {devices.length} 台设备;需关注设备 {alarms.length} 台;建议跟进事项 5 项。</p></div><div className="callout orange-line"><strong>AI 已发现的重点</strong><ol><li>AIDC 冷却系统能效较昨日下降 6.2%</li><li>污水站 1# 鼓风机振动持续上升</li><li>燃气调压站 PT-101 数据中断</li></ol></div><div className="callout cyan-line"><strong>输出格式</strong><p>Word / PDF / 邮件正文;支持站点、区域、班次与管理层视角。</p></div><button className="primary" onClick={()=>setGenerated(!generated)}>{generated?"重新汇总数据":"生成日报"}</button></div> }
+
+function DiagnosisPanel({device}:{device:Device}) { return <div className="panel-body"><div className="alarm-banner"><div><strong>告警:{device.alarm}</strong><small>{device.site} · {device.name} · 当前值 {device.metric}</small></div><span>P1</span></div><h3>AI 初步判断</h3><div className="callout orange-line"><strong>最可能原因:设备状态与过程量偏差同时出现</strong><p>置信度 78%。告警前 18 分钟出现状态波动,随后相关测点持续偏离。</p></div><h3>建议排查顺序</h3><ol className="steps"><li>确认设备通信、变频器或执行器状态</li><li>与同组冗余设备的流量、压力或负荷对比</li><li>检查过滤器、阀门开度或现场仪表</li><li>必要时按站点 SOP 切换备用设备</li></ol></div> }
+
+function HealthPanel({devices}:{devices:Device[]}) { const avg=Math.round(devices.reduce((a,d)=>a+d.score,0)/devices.length); return <div className="panel-body"><div className="metric-row"><div><b className="good-text">{avg}</b><span>总体健康度</span></div><div><b className="danger-text">{devices.filter(d=>d.score<70).length}</b><span>高风险设备</span></div><div><b className="warn-text">{devices.filter(d=>d.score<80).length}</b><span>需关注设备</span></div></div><h3>近 7 日健康趋势</h3><div className="chart-card"><MiniBars values={[90,88,85,82,78,76,72]} tone="green"/><div className="threshold">风险阈值</div></div><div className="callout orange-line"><strong>主要扣分因素</strong><p>告警频率 −6.2 离线时长 −4.8 测点异常 −3.6 能效偏离 −2.1 维护逾期 −1.7</p></div></div> }
+
+function PredictionPanel({device}:{device:Device}) { return <div className="panel-body"><div className="callout red-line"><strong>预测结论 · 风险上升</strong><p>{device.site} · {device.name} 的异常特征持续劣化,未来 72 小时进入高风险区间的概率约 68%。</p></div><h3>趋势与预测区间</h3><div className="chart-card tall"><MiniBars values={[24,27,31,39,48,61,76,88]} tone="violet"/><div className="threshold">阈值</div></div><div className="callout orange-line"><strong>维护窗口建议</strong><p>建议未来 24—48 小时内安排点检:传感器、润滑状态、联轴器对中和基础紧固。</p></div></div> }
+
+function ControlPanel({site,setSite,approval,setApproval}:{site:keyof typeof siteControl;setSite:(s:keyof typeof siteControl)=>void;approval:string;setApproval:(s:string)=>void}) { const item=siteControl[site]; return <div className="panel-body"><div className="site-tabs">{Object.keys(siteControl).map(k=><button key={k} className={site===k?"active":""} onClick={()=>{setSite(k as keyof typeof siteControl);setApproval("待审批")}}>{k}</button>)}</div><div className="callout green-line"><strong>{item.title}</strong><p>{item.suggestion}</p><small>{item.result}</small></div><h3>AI 解释</h3><div className="callout blue-line"><strong>适用设备</strong><p>{item.device}</p></div><div className="callout red-line"><strong>不适用条件</strong><p>{item.avoid}</p></div><h3>操作方式</h3><div className="flow"><span>仅建议</span><i>→</i><span>仿真通过</span><i>→</i><span className={approval==="已批准"?"done":""}>人工审批</span><i>→</i><span>分步执行</span><i>→</i><span>结果验证</span></div></div> }
+
+function EvidencePanel({device,connection}:{device:Device;connection:ConnectionState}) { return <div className="right-body"><h3>查询上下文</h3><div className="filter-card"><strong>过滤条件</strong><p>组织:Demo Org 站点:全部 时间:过去 2 小时</p></div><h3>结果证据</h3><div className="evidence-card red-line"><strong>{device.site} · {device.name}</strong><span className="score-badge">风险 {100-device.score}</span><p>最新状态:{device.status}<br/>关联事件:{device.alarm}<br/>当前值:{device.metric}<br/>数据连接:{connection==="live"?"设备监控实时接口":"预览数据"}</p></div><MiniBars tone="red"/><h3>数据来源</h3><div className="chips"><span>节点状态 API</span><span>告警 API</span><span>实时测点 API</span><span>资产关系</span></div></div> }
+
+function ReportEditor({alarms}:{alarms:number}) { return <div className="right-body"><h3>日报编辑器 · 今日</h3>{["执行摘要","站点运行概况","关键告警与处置","能效与运行偏差","明日建议与责任人"].map((x,i)=><div className="outline-row" key={x}><b>{i+1}</b><div><strong>{x}</strong><small>{i===2?`${alarms} 条需关注告警`:"点击展开并编辑"}</small></div><span>+ ⋮</span></div>)}<h3>报告设置</h3><div className="filter-card green-line"><strong>数据口径</strong><p>时间:00:00—当前 对比:昨日 / 近 7 日<br/>范围:燃气站、水站、污水处理厂、新能源场站、AIDC</p></div><button className="primary full">生成 Word</button></div> }
+
+function RootCause({device}:{device:Device}) { return <div className="right-body"><h3>证据链与根因候选</h3><div className="timeline">{[["08:59","状态开始偏离基线"],["09:08","通信短时中断"],["09:12","关联过程量变化 14%"],["09:17",device.alarm]].map((x,i)=><div key={i}><i className={i%2?"red":"orange"}/><b>{x[0]}</b><span>{x[1]}</span></div>)}</div><h3>根因候选</h3>{[["设备故障或降频",78],["过滤器堵塞",46],["阀门开度异常",31]].map(([x,v])=><div className="factor" key={String(x)}><span>{x}</span><i><b style={{width:`${v}%`}}/></i><strong>{v}%</strong></div>)}<div className="callout red-line"><strong>安全边界</strong><p>涉及泵切换、阀门动作或设定值修改时,必须进入审批与联锁检查流程。</p></div></div> }
+
+function RiskList({devices}:{devices:Device[]}) { return <div className="right-body"><h3>风险设备排名</h3>{[...devices].sort((a,b)=>a.score-b.score).map(d=><div className="risk-row" key={d.id}><div><strong>{d.site} · {d.name}</strong><small>{d.status} · {d.alarm}</small></div><b className={d.score<70?"bad":"warn"}>{d.score}</b></div>)}<div className="callout blue-line"><strong>评分解释</strong><p>状态可用性、告警、测点偏离、能效、维护及时性五类指标均可追溯到原始证据。</p></div></div> }
+
+function ModelPanel() { return <div className="right-body"><h3>模型与证据</h3><div className="callout blue-line"><strong>输入信号</strong><p>振动 RMS、轴承温度、电流、出口压力、运行负荷、启停次数</p></div><div className="callout violet-line"><strong>基线与模型</strong><p>同型设备群组基线 + 设备个体基线;异常分数、趋势预测与规则阈值联合判断</p></div><div className="callout green-line"><strong>置信度说明</strong><p>数据完整性 96% · 当前工况可比性 89% · 历史维修标签有限</p></div><h3>影响因子</h3>{[["振动 RMS",84],["轴承温度",68],["运行负荷",42],["启停频次",31]].map(([x,v])=><div className="factor" key={String(x)}><span>{x}</span><i><b style={{width:`${v}%`}}/></i></div>)}</div> }
+
+function SafetyPanel({site,approval,setApproval}:{site:keyof typeof siteControl;approval:string;setApproval:(s:string)=>void}) { const [windowOk,setWindowOk]=useState(false); return <div className="right-body"><h3>执行前安全检查 · {site}</h3>{[["数据质量",true],["设备权限",true],["联锁条件",true],["冗余能力",true],["变更窗口",windowOk]].map(([x,ok])=><button className="check-row" key={String(x)} onClick={()=>x==="变更窗口"&&setWindowOk(true)}><span>{x}</span><b className={ok?"pass":"confirm"}>{ok?"通过":"需确认"}</b></button>)}<div className="callout red-line"><strong>回退策略</strong><p>任一关键指标越限、设备告警升级或通信中断时,恢复原设定值并记录审计事件。</p></div><h3>审批链</h3><div className="approval-chain"><span>值班工程师</span><i>→</i><span>站点负责人</span><i>→</i><span>控制网关</span></div><div className="dual-buttons"><button onClick={()=>setApproval("已驳回")}>驳回并说明</button><button className="primary" disabled={!windowOk||approval==="已批准"} onClick={()=>setApproval("已批准")}>{approval==="已批准"?"已批准 · 等待执行":"确认进入变更窗口"}</button></div></div> }
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
new file mode 100644 (file)
index 0000000..0c1b8ad
--- /dev/null
@@ -0,0 +1,119 @@
+// 与后端 AI 分析接口通信的封装层
+// 后端地址在开发期由 Vite 代理到 http://localhost:8000(见 vite.config.ts)
+// 生产期应通过同域反向代理暴露 /api,使微应用与后端同源
+
+export type AnalyzeResponse = {
+  ok: boolean;
+  analysis?: string;
+  model?: string;
+  fallback?: boolean;
+  message?: string;
+};
+
+export type PeriodSummary = {
+  eventCount: number;
+  affectedNodes: number;
+  highPriority: number;
+  impactHours: number;
+  availabilityEstimate: number | null;
+  windowHours: number;
+};
+
+export type PeriodComparisonResponse = {
+  ok: boolean;
+  message?: string;
+  assetGroup?: { id: string; name: string; deviceCount: number };
+  periods?: {
+    current: { start: string; end: string; label: string };
+    previous: { start: string; end: string; label: string };
+  };
+  current?: PeriodSummary;
+  previous?: PeriodSummary;
+  deltas?: {
+    eventCount: number | null;
+    affectedNodes: number | null;
+    impactHours: number | null;
+    availabilityEstimate: number | null;
+  };
+  siteContributions?: Array<{ name: string; eventCount: number }>;
+  topDevices?: Array<{ id: string; name: string; site: string; eventCount: number; highPriority: number }>;
+  historyScoped?: boolean;
+  endpoints?: Array<{ path: string; status: number; ok: boolean; rowCount?: number; error?: string }>;
+};
+
+export async function analyzeChat(
+  prompt: string,
+  devices: unknown[],
+  alarms: unknown[]
+): Promise<AnalyzeResponse> {
+  const res = await fetch("/api/ai/analyze", {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify({
+      module: "qa",
+      prompt,
+      context: { devices, alarms },
+    }),
+  });
+  return res.json();
+}
+
+export async function analyzeSummary(
+  devices: unknown[],
+  alarms: unknown[]
+): Promise<AnalyzeResponse> {
+  const res = await fetch("/api/ai/analyze", {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify({
+      module: "summary",
+      prompt: "对当前实时设备与告警数据做整体分析,给出重点结论与建议。",
+      context: { devices, alarms },
+    }),
+  });
+  return res.json();
+}
+
+export async function analyzeModule(
+  module: string,
+  prompt: string,
+  context: unknown
+): Promise<AnalyzeResponse> {
+  const endpoint = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"
+    ? "http://localhost:8000/api/ai/analyze"
+    : "/api/ai/analyze";
+  const res = await fetch(endpoint, {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify({ module, prompt, context }),
+  });
+  return res.json();
+}
+
+export async function loadPeriodComparison(
+  config: unknown,
+  secret: string,
+  selectedGroupId: string,
+  selectedGroupName: string
+): Promise<PeriodComparisonResponse> {
+  const endpoint = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"
+    ? "http://localhost:8000/api/mu-period-compare"
+    : "/api/mu-period-compare";
+  const res = await fetch(endpoint, {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify({
+      config,
+      secret,
+      selectedGroupId,
+      selectedGroupName,
+      comparisonMode: "month",
+    }),
+  });
+  const text = await res.text();
+  try {
+    return JSON.parse(text) as PeriodComparisonResponse;
+  } catch {
+    return { ok: false, message: `运行环比服务返回异常(HTTP ${res.status})` };
+  }
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644 (file)
index 0000000..172c90b
--- /dev/null
@@ -0,0 +1,285 @@
+:root{--ink:#18273a;--muted:#6e7b8e;--line:#dbe3ec;--blue:#0e95d0;--pale:#f5f8fc;--danger:#df4c51;--orange:#f39a32;--green:#24a575;--violet:#7463e8}
+*{box-sizing:border-box}body{margin:0;background:#fff;color:var(--ink);font-family:"Microsoft YaHei","PingFang SC",Arial,sans-serif}button,input,textarea{font:inherit}button{cursor:pointer}.app-shell{height:100vh;display:grid;grid-template-columns:266px minmax(0,1fr);grid-template-rows:56px minmax(0,1fr);overflow:hidden}.topbar{grid-column:1/-1;height:58px;border-top:4px solid var(--blue);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 24px 0 16px;background:#fff;z-index:3}.brand{border:0;background:transparent;display:flex;align-items:center;gap:10px;font-size:20px;font-weight:800;color:var(--ink)}.brand-mark{width:34px;height:34px;display:grid;place-items:center;border-radius:50%;background:linear-gradient(135deg,#25b9c8,#2e8cf0);color:#fff;font-size:14px}.header-actions{display:flex;align-items:center;gap:12px}.source-link{font-size:12px;color:#3379ae;text-decoration:none}.live-pill{font-size:12px;color:#4c5c70;background:#f2f7fb;border:1px solid #d7e5ef;padding:8px 12px;border-radius:999px}.live-pill.live{color:#137b59;background:#e9f8f2}.live-pill.preview{color:#a56d1c;background:#fff6e8}.dot{display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:#f5a431}.live .dot{background:#28a878}.connecting .dot{animation:pulse 1s infinite}@keyframes pulse{50%{opacity:.35}}.sidebar{grid-row:2;border-right:1px solid var(--line);padding:18px 14px;background:#fbfcfe;overflow:auto}.assistant-title{font-weight:800;margin-bottom:16px;display:flex;gap:8px;align-items:center}.bot{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;background:#7467ee;color:#fff}.new-chat{width:100%;padding:11px;border:0;border-radius:5px;color:#fff;background:#0b9dd4;font-weight:700}.section-label{color:#9aa7b7;font-size:12px;padding:20px 8px 8px}.history{width:100%;border:0;background:transparent;padding:9px 8px;display:flex;justify-content:space-between;align-items:center;color:#26364a;border-radius:5px;text-align:left}.history>span{display:flex;align-items:center;gap:8px}.history.active{background:#e5f4fb;color:#088fc8}.history b{color:#a2adba}.nav-icon{font-style:normal;width:26px;height:26px;border-radius:50%;color:#fff;display:inline-grid;place-items:center;font-size:12px;font-weight:800}.blue{background:#3b77ef!important}.cyan{background:#2bbac1!important}.orange{background:#f39b33!important}.green{background:#21a878!important}.violet{background:#7463e8!important}.red{background:#de5156!important}.data-card{margin-top:24px;border:1px solid #dce6ef;background:#fff;border-radius:8px;padding:13px;display:flex;flex-direction:column;gap:5px}.data-card span,.data-card small{font-size:11px;color:var(--muted)}.content{padding:58px 6vw 36px;overflow:auto;background:radial-gradient(circle at 65% 10%,#eff9ff 0,transparent 33%),#fff}.hero{max-width:850px;margin:0 auto;text-align:center}.eyebrow{color:#188ec4;background:#e8f7fd;border:1px solid #c7eaf7;padding:7px 12px;border-radius:999px;font-weight:700;font-size:13px}.hero h1{margin:25px 0 10px;font-size:40px;line-height:1.15;letter-spacing:-1px}.hero p{color:var(--muted);margin:0 0 28px}.ask-launch{width:100%;display:flex;justify-content:space-between;align-items:center;border:1px solid #bdcad8;box-shadow:0 10px 32px rgba(35,73,110,.10);border-radius:9px;padding:20px 16px 20px 24px;background:#fff;color:#9aa6b6}.ask-launch b{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;background:#2589ed;color:#fff;font-size:20px}.skill-grid{max-width:960px;margin:36px auto 0;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:15px}.skill-card{min-height:116px;border:1px solid #d6dfe9;border-radius:10px;background:#fff;padding:18px;display:grid;grid-template-columns:42px 1fr 18px;gap:12px;align-items:start;text-align:left;transition:.18s}.skill-card:hover{transform:translateY(-2px);border-color:#3ca6df;box-shadow:0 12px 28px rgba(40,104,149,.12)}.skill-icon{width:40px;height:40px;border-radius:50%;color:#fff;display:grid;place-items:center;font-weight:800}.skill-copy{display:flex;flex-direction:column;gap:8px}.skill-copy strong{font-size:15px}.skill-copy small{color:var(--muted);line-height:1.5}.arrow{color:#a3afbd}.overview-row{max-width:960px;margin:20px auto 0;border:1px solid #dbe6ef;background:#f7fafc;border-radius:9px;padding:14px 20px;display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.overview-row>div{display:flex;flex-direction:column;gap:4px}.overview-row span{font-size:11px;color:#8b98a9}.overview-row strong{font-size:14px}.workspace{grid-column:2;grid-row:2;display:grid;grid-template-columns:minmax(520px,46%) minmax(560px,54%);min-height:0}.conversation,.evidence{min-height:0;overflow:auto}.conversation{border-right:1px solid var(--line);background:#fff}.evidence{background:#f7f9fc}.pane-title{height:52px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:9px;padding:0 20px;position:sticky;top:0;background:#fff;z-index:2}.pane-title em{font-style:normal;font-size:12px;color:var(--muted);margin-left:auto}.evidence>h2{height:52px;margin:0;border-bottom:1px solid var(--line);display:flex;align-items:center;padding:0 20px;font-size:17px;position:sticky;top:0;background:#f7f9fc;z-index:2}.panel-body,.right-body{padding:20px 22px 28px}.prompt-bubble{background:#dff3fb;border-radius:22px;padding:13px 18px;margin-left:auto;max-width:76%;font-size:13px}.answer-lead{margin:24px 0 14px}.result-list{display:grid;gap:10px}.result-card{border:1px solid #d5dee8;border-left:4px solid var(--danger);border-radius:5px;padding:13px;display:grid;grid-template-columns:28px 1fr 58px;gap:10px;align-items:center}.result-card small,.risk-row small,.alarm-banner small,.callout small,.outline-row small{display:block;color:var(--muted);margin-top:5px;line-height:1.45}.rank{width:25px;height:25px;background:#143b5d;color:#fff;border-radius:50%;display:grid;place-items:center;font-size:12px}.severity{border-radius:999px;text-align:center;padding:5px;font-size:12px}.severity.high{color:var(--danger);background:#fdebec}.severity.mid{color:#bc731e;background:#fff1df}.action-row{display:flex;gap:9px;margin:18px 0}.action-row button,.starter-questions button{border:0;background:#e8f4fb;color:#187ac0;border-radius:999px;padding:8px 13px;font-size:12px}.starter-questions{display:grid;gap:12px;margin:40px 0}.starter-questions button{text-align:left;border-radius:7px;padding:14px}.composer{position:sticky;bottom:0;background:#fff;border:1px solid #cfd9e5;border-radius:8px;padding:8px;display:flex;gap:8px;margin-top:26px;box-shadow:0 8px 24px rgba(32,63,91,.08)}.composer textarea{resize:none;flex:1;min-height:58px;border:0;outline:0;padding:8px}.composer button,.primary{border:0;border-radius:6px;background:var(--blue);color:#fff;padding:0 18px;font-weight:700}.ai-note{text-align:center;color:#9aa7b7;font-size:10px;margin-top:9px}.right-body h3,.panel-body h3{font-size:15px;margin:18px 0 10px}.filter-card,.evidence-card,.callout{position:relative;border:1px solid #d8e1ea;border-radius:7px;background:#fff;padding:14px 15px;margin-bottom:14px}.filter-card p,.evidence-card p,.callout p{color:var(--muted);font-size:13px;line-height:1.55;margin:7px 0 0}.blue-line{border-left:5px solid #3b77ef}.orange-line{border-left:5px solid var(--orange)}.cyan-line{border-left:5px solid #2bbac1}.red-line{border-left:5px solid var(--danger)}.green-line{border-left:5px solid var(--green)}.violet-line{border-left:5px solid var(--violet)}.score-badge{position:absolute;right:14px;top:12px;background:#fdebed;color:var(--danger);padding:5px 9px;border-radius:999px;font-size:12px}.chips{display:flex;gap:8px;flex-wrap:wrap}.chips span{background:#edf1f5;color:#657286;padding:7px 10px;border-radius:999px;font-size:11px}.mini-bars{height:130px;display:flex;align-items:flex-end;gap:13px;padding:18px 15px;border:1px solid #dfe6ed;background:repeating-linear-gradient(to bottom,#fff 0,#fff 31px,#e6ebf0 32px)}.mini-bars span{flex:1;min-width:8px;border-radius:4px 4px 0 0;opacity:.9}.outline-row{border:1px solid #d5dee8;background:#fff;border-radius:6px;padding:12px;margin-bottom:9px;display:grid;grid-template-columns:30px 1fr auto;gap:10px;align-items:center}.outline-row>b{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;background:#153f61;color:#fff}.outline-row>span{color:#2c78eb}.full{width:100%;padding:14px;margin-top:12px}.alarm-banner{border:1px solid #d7e0e9;border-left:5px solid var(--danger);padding:15px;display:flex;justify-content:space-between}.alarm-banner>span{height:28px;padding:5px 25px;border-radius:999px;background:#fdebed;color:var(--danger)}.steps{list-style:none;padding:0}.steps li{counter-increment:step;padding:9px 0 9px 42px;position:relative}.steps li:before{content:counter(step);position:absolute;left:4px;top:6px;width:28px;height:28px;border-radius:50%;display:grid;place-items:center;background:#3779ec;color:#fff;font-weight:700}.timeline{border-left:4px solid #d2dae4;margin-left:20px}.timeline>div{display:grid;grid-template-columns:22px 48px 1fr;gap:10px;align-items:center;margin:0 0 25px -13px}.timeline i{width:20px;height:20px;border-radius:50%}.timeline b{font-size:12px;color:#657286}.timeline span{font-size:13px}.factor{display:grid;grid-template-columns:130px 1fr 42px;gap:10px;align-items:center;margin:16px}.factor>span{font-size:12px}.factor>i{height:16px;background:#e6ebf0;border-radius:99px;overflow:hidden}.factor>i>b{display:block;height:100%;background:#4179ec;border-radius:99px}.factor>strong{font-size:12px;color:var(--danger)}.metric-row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.metric-row>div{border:1px solid #d7e0e9;border-radius:7px;padding:20px;text-align:center}.metric-row b{font-size:34px;display:block}.metric-row span{font-size:12px;color:var(--muted)}.good-text{color:var(--green)}.danger-text{color:var(--danger)}.warn-text{color:var(--orange)}.chart-card{position:relative}.chart-card.tall .mini-bars{height:190px}.threshold{position:absolute;left:15px;right:15px;top:46%;border-top:2px solid var(--danger);text-align:right;color:var(--danger);font-size:10px}.risk-row{border:1px solid #d6dfe9;background:#fff;border-radius:7px;padding:13px 16px;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center}.risk-row>b{width:48px;height:48px;border-radius:50%;display:grid;place-items:center;color:#fff}.risk-row .bad{background:var(--danger)}.risk-row .warn{background:var(--orange)}.site-tabs{display:flex;gap:7px;overflow:auto;margin-bottom:15px}.site-tabs button{white-space:nowrap;border:1px solid #d7e0e9;background:#fff;border-radius:999px;padding:7px 12px;font-size:11px}.site-tabs button.active{background:#e2f3fb;color:#087fab;border-color:#9fd7ec}.flow{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.flow span{background:#e8f5ef;color:#15906a;border-radius:999px;padding:8px 12px;font-size:12px}.flow span.done{background:#dff1ff;color:#2873d9}.flow i{font-style:normal;color:#9ba7b5}.check-row{width:100%;border:1px solid #d8e0e9;background:#fff;border-radius:6px;padding:10px 14px;margin-bottom:8px;display:flex;justify-content:space-between}.check-row b{border-radius:999px;padding:4px 20px;font-size:12px}.pass{background:#e3f5ee;color:#188c67}.confirm{background:#fff0df;color:#e28b2c}.approval-chain{display:flex;align-items:center;justify-content:space-around;font-size:12px;margin:26px 0}.approval-chain i{font-style:normal;color:#8d99a8}.dual-buttons{display:grid;grid-template-columns:1fr 1fr;gap:12px}.dual-buttons button{padding:13px;border:0;border-radius:6px;background:#e9edf2;color:#687589}.dual-buttons button.primary{background:var(--blue);color:#fff}.dual-buttons button:disabled{opacity:.6;cursor:not-allowed}
+.settings-button{border:1px solid #d7e0e9;background:#fff;color:#536378;border-radius:6px;padding:7px 10px;font-size:12px}.slate{background:#5d6e82!important}.settings-page{grid-column:2;grid-row:2;display:grid;grid-template-columns:minmax(600px,1fr) 390px;min-height:0;background:#f4f7fb}.settings-main,.settings-side{overflow:auto}.settings-main{padding:34px 4vw 48px}.settings-side{border-left:1px solid var(--line);background:#fff;padding:32px 26px}.settings-heading{display:flex;gap:16px;align-items:flex-start;margin-bottom:24px}.settings-orb{width:48px;height:48px;border-radius:14px;display:grid;place-items:center;background:linear-gradient(135deg,#288de8,#27b6bf);color:#fff;font-size:21px;box-shadow:0 8px 18px rgba(30,132,190,.2)}.settings-heading>div>span{font-size:12px;color:#188ec4;font-weight:700}.settings-heading h1{font-size:27px;margin:4px 0 7px}.settings-heading p{margin:0;color:var(--muted);font-size:13px;line-height:1.55}.settings-card{background:#fff;border:1px solid #d8e1ea;border-radius:10px;padding:20px;margin-bottom:16px}.settings-card-title{display:flex;gap:12px;align-items:center;margin-bottom:19px}.settings-card-title>b{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;background:#153f61;color:#fff}.settings-card-title>div{display:flex;flex-direction:column;gap:3px}.settings-card-title small{color:var(--muted)}.field-grid,.credentials{display:grid;grid-template-columns:1fr 1fr;gap:14px}.field{display:flex;flex-direction:column;gap:7px;color:#415065;font-size:12px}.field input{width:100%;border:1px solid #ccd7e3;border-radius:6px;background:#fff;padding:11px 12px;outline:0;color:var(--ink)}.field input:focus{border-color:#2595d0;box-shadow:0 0 0 3px rgba(37,149,208,.12)}.full-field{margin-bottom:14px}.secret-note{font-size:10px;color:#8c99a9}.auth-options{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;margin-bottom:18px}.auth-options button{position:relative;min-height:112px;border:1px solid #d6dfe9;border-radius:8px;background:#fff;padding:15px 11px;text-align:left;display:flex;flex-direction:column;gap:7px}.auth-options button.active{border-color:#1a9ad1;background:#eef9fd;box-shadow:0 0 0 2px rgba(26,154,209,.08)}.auth-options button i{position:absolute;right:9px;top:8px;width:20px;height:20px;border-radius:50%;display:grid;place-items:center;background:#1a9ad1;color:#fff;font-style:normal;font-size:11px}.auth-options button strong{font-size:13px}.auth-options button small{color:var(--muted);line-height:1.45}.login-api-box{margin-top:16px;padding:16px;background:#f7f9fc;border:1px dashed #c7d3df;border-radius:8px}.settings-actions{display:flex;justify-content:flex-end;gap:10px}.settings-actions button{padding:11px 16px;border-radius:6px;font-weight:700}.ghost-button{background:#fff;border:1px solid #cdd8e3;color:#68768a}.soft-button{background:#e8f4fb;border:1px solid #c4e2f0;color:#167fb1}.test-button{min-width:180px;border:0}.test-button:disabled{opacity:.6;cursor:not-allowed}.settings-side h2{font-size:18px;margin:0 0 15px}.settings-side h3{font-size:14px;margin:23px 0 10px}.connection-result{display:flex;gap:12px;align-items:flex-start;border:1px solid #d7e1ea;border-radius:9px;background:#f7f9fc;padding:16px}.connection-result.live{background:#edf9f4;border-color:#bce8d8}.connection-result.preview{background:#fff8ed;border-color:#f2dbb5}.connection-result p{margin:5px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.endpoint-list{display:grid;gap:7px}.endpoint-list code{padding:8px 9px;border-radius:5px;background:#f1f4f7;color:#476078;font-size:10px;word-break:break-all}.security-card{margin-top:22px;border-left:5px solid var(--green);background:#f1faf6;border-radius:7px;padding:15px}.security-card ul{margin:10px 0 0;padding-left:18px;color:#5e6c7f;font-size:12px;line-height:1.8}.open-configured{display:block;margin-top:18px;border:1px solid #bedfed;background:#edf8fc;color:#167fae;border-radius:7px;text-align:center;padding:11px;text-decoration:none;font-size:12px}
+@media(max-width:1080px){.workspace{grid-template-columns:1fr}.evidence{display:none}.skill-grid{grid-template-columns:1fr 1fr}.overview-row{grid-template-columns:1fr 1fr}.pane-title em{display:none}.settings-page{grid-template-columns:1fr}.settings-side{border-left:0;border-top:1px solid var(--line)}.auth-options{grid-template-columns:1fr 1fr}}
+@media(max-width:760px){.app-shell{grid-template-columns:1fr}.sidebar{display:none}.workspace,.settings-page{grid-column:1}.content{padding:38px 16px}.skill-grid{grid-template-columns:1fr}.hero h1{font-size:31px}.source-link{display:none}.metric-row{grid-template-columns:1fr}.overview-row{grid-template-columns:1fr}.header-actions{gap:4px}.brand span:last-child{font-size:15px}.settings-main{padding:24px 14px}.settings-side{padding:24px 16px}.field-grid,.credentials{grid-template-columns:1fr}.auth-options{grid-template-columns:1fr}.settings-actions{flex-direction:column}.settings-actions button{width:100%}.settings-button{display:none}}
+
+/* KB Insight visual language */
+body{font-family:"Noto Sans SC",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Arial,sans-serif;background:#fafafa;color:#323233}.topbar{grid-column:1;grid-row:1;height:56px;border-top:4px solid #008cd6;border-bottom:0;padding:0 20px;background:#fff;box-shadow:0 2px 5px rgba(0,0,0,.24)}.header-left{height:52px;display:flex;align-items:center;min-width:0}.brand{height:48px;gap:11px;padding:0;color:#323233;white-space:nowrap}.wise-wordmark{font-family:Arial,sans-serif;font-size:29px;line-height:1;font-weight:900;letter-spacing:-1.5px;color:#005b80}.brand-divider{width:1px;height:27px;background:#cfd2d5}.portal-title{font-family:Roboto,"Noto Sans SC",Arial,sans-serif;font-size:20px;font-weight:700;color:#323233}.product-tabs{height:48px;margin-left:24px;display:flex;align-items:stretch}.product-tabs button,.product-tabs a{height:48px;display:flex;align-items:center;border:0;border-bottom:3px solid transparent;background:transparent;padding:0 12px;color:#89898a;text-decoration:none;font-size:16px;font-weight:700;white-space:nowrap}.product-tabs button:hover,.product-tabs a:hover{color:#008cd6}.product-tabs .active{color:#008cd6;border-bottom-color:#008cd6}.header-actions{height:52px;gap:12px}.live-pill{border:0;border-radius:0;background:transparent!important;color:#777!important;padding:7px 5px;font-size:12px;font-weight:500}.live-pill.live{color:#11815d!important}.live-pill.preview{color:#a56d1c!important}.header-separator{height:26px;width:1px;background:#d7d7d7;margin-left:5px}.icon-button{border:0;background:transparent;color:#838383;font-size:21px;padding:5px}.user-avatar{width:23px;height:23px;border:3px solid #888;border-radius:50%;display:grid;place-items:center;color:#888;font-size:8px}.function-tabs{grid-column:1;grid-row:2;height:48px;display:flex;align-items:stretch;justify-content:center;background:#fff;border-bottom:1px solid #d0d0d0;overflow-x:auto;overflow-y:hidden}.function-tabs button{height:48px;border:0;border-bottom:3px solid transparent;background:transparent;padding:0 18px;color:#737374;font-size:14px;font-weight:700;white-space:nowrap}.function-tabs button:hover{color:#008cd6;background:#f8f8f8}.function-tabs button.active{color:#008cd6;border-bottom-color:#008cd6}.sidebar{display:none}.content{grid-column:1;grid-row:3;padding:32px max(24px,calc((100vw - 1016px)/2)) 42px;background:#fafafa}.hero{max-width:1016px;text-align:left}.eyebrow{display:block;width:max-content;padding:0;border:0;border-radius:0;background:transparent;color:#89898a;font-size:14px;font-weight:700}.hero h1{margin:8px 0 7px;font-family:Roboto,"Noto Sans SC",Arial,sans-serif;font-size:24px;letter-spacing:0;color:#323233}.hero p{margin-bottom:18px;font-size:13px}.ask-launch{border:1px solid #c7c7c7;border-radius:2px;box-shadow:none;padding:10px 10px 10px 14px;min-height:52px;color:#aaa}.ask-launch b{width:34px;height:34px;border-radius:2px;background:#008cd6}.skill-grid{max-width:1016px;margin-top:16px;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.skill-card{min-height:106px;border:1px solid #cfcfcf;border-radius:2px;padding:16px;background:#fff;transition:border-color .15s}.skill-card:hover{transform:none;border-color:#008cd6;box-shadow:none}.skill-icon{border-radius:2px;width:34px;height:34px}.skill-copy strong{font-family:Roboto,"Noto Sans SC",Arial,sans-serif;color:#111;font-size:16px}.skill-copy small{color:#89898a}.overview-row{max-width:1016px;margin-top:12px;border-color:#cfcfcf;border-radius:2px;background:#fff}.workspace{grid-column:1;grid-row:3;width:100%;max-width:1280px;margin:0 auto;background:#fafafa}.conversation{border-left:1px solid #d2d2d2;border-right:1px solid #d2d2d2}.evidence{border-right:1px solid #d2d2d2;background:#fafafa}.pane-title,.evidence>h2{height:56px;border-bottom:1px solid #cfcfcf;background:#fff}.pane-title strong,.evidence>h2{font-family:Roboto,"Noto Sans SC",Arial,sans-serif;color:#323233}.nav-icon{border-radius:2px}.panel-body,.right-body{padding:22px 24px}.prompt-bubble{border-radius:2px;background:#eaf7fb;border:1px solid #c8e9f4}.result-card,.filter-card,.evidence-card,.callout,.outline-row,.alarm-banner,.metric-row>div,.risk-row,.site-tabs button,.check-row,.composer{border-radius:2px}.composer{box-shadow:none}.action-row button,.starter-questions button,.chips span,.flow span{border-radius:2px}.settings-page{grid-column:1;grid-row:3;max-width:1280px;width:100%;margin:0 auto;background:#fafafa}.settings-main{padding:32px 40px 48px}.settings-side{border-right:1px solid #d2d2d2}.settings-orb{border-radius:2px;background:#008cd6;box-shadow:none}.settings-heading h1{font-size:24px}.settings-card,.auth-options button,.login-api-box,.connection-result,.security-card,.open-configured{border-radius:2px}.auth-options button.active{border-color:#008cd6;background:#f0f9fd;box-shadow:none}.auth-options button i{border-radius:2px;background:#008cd6}.field input{border-radius:2px}.settings-actions button{border-radius:2px}
+@media(max-width:980px){.portal-title{font-size:17px}.product-tabs{margin-left:10px}.product-tabs button,.product-tabs a{padding:0 8px;font-size:14px}.function-tabs{justify-content:flex-start}.skill-grid{grid-template-columns:1fr}.workspace{grid-template-columns:1fr}.evidence{display:none}.settings-page{grid-template-columns:1fr}.settings-side{border-left:1px solid #d2d2d2}.auth-options{grid-template-columns:1fr 1fr}}
+@media(max-width:680px){.topbar{padding:0 10px}.wise-wordmark{font-size:23px}.brand-divider,.portal-title,.product-tabs,.live-pill,.header-separator{display:none}.function-tabs button{padding:0 12px;font-size:13px}.content{padding:24px 14px}.hero h1{font-size:22px}.settings-main{padding:24px 14px}.field-grid,.credentials{grid-template-columns:1fr}.auth-options{grid-template-columns:1fr}.settings-actions{flex-direction:column}.settings-actions button{width:100%}}
+
+/* StationAIDemo 页脚备案信息 */
+.site-footer{position:fixed;left:0;right:0;bottom:0;height:34px;display:flex;align-items:center;gap:10px;padding:0 22px;background:#ffffff;border-top:1px solid #dbe3ec;color:#6e7b8e;font-size:12px;z-index:5}
+.site-footer .footer-item{color:#6e7b8e}
+.site-footer a.footer-item{color:#3379ae;text-decoration:none}
+.site-footer a.footer-item:hover{text-decoration:underline}
+.site-footer .footer-sep{color:#c4ccd6}
+.app-shell{padding-bottom:34px}
+
+/* 手机端汉堡按钮(桌面隐藏) */
+.mobile-menu-btn{display:none}
+
+/* 手机端菜单遮罩 */
+.mobile-menu-overlay{position:fixed;top:56px;left:0;right:0;bottom:34px;background:rgba(0,0,0,.35);z-index:19}
+
+/* Desk-style two-level navigation */
+
+.app-shell.nav-collapsed{grid-template-columns:56px minmax(0,1fr)}
+.topbar{grid-column:1/-1;grid-row:1;position:sticky;top:0;z-index:10}
+.function-tabs{display:none}
+.left-navigation{grid-column:1;grid-row:2;display:grid;grid-template-columns:56px 210px;min-height:0;background:#ececec;border-right:1px solid #c9c9c9;position:relative;z-index:2;transition:grid-template-columns .18s ease}
+.nav-collapsed .left-navigation{grid-template-columns:56px}
+.module-rail{min-height:0;background:#f5f5f5;border-right:1px solid #d1d1d1;display:flex;flex-direction:column;align-items:stretch}
+.module-rail>button,.module-rail>a{position:relative;width:56px;min-height:58px;border:0;border-left:3px solid transparent;background:transparent;color:#858585;text-decoration:none;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;padding:6px 2px;font-weight:500}
+.module-rail>button:hover,.module-rail>a:hover{background:#fff;color:#008cd6}
+.module-rail>button.active{border-left-color:#008cd6;background:#fff;color:#008cd6}
+.module-rail small{font-size:10px;line-height:1.2;white-space:nowrap}
+.rail-icon{font-family:Arial,sans-serif;font-size:20px;line-height:1;font-weight:700}
+.module-rail .rail-expand{margin-top:0;min-height:52px;border-bottom:1px solid #d5d5d5;background:#fff;font-size:20px;color:#008cd6;font-weight:700}
+.function-sidebar{min-width:0;min-height:0;overflow-y:auto;overflow-x:hidden;background:#ececec;color:#323233}
+.nav-collapsed .function-sidebar{display:none}
+.sidebar-panel-title{height:44px;background:#dcecf8;border-bottom:1px solid #c5d7e3;display:flex;align-items:center;justify-content:space-between;padding-left:20px;color:#323233}
+.sidebar-panel-title strong{font-size:15px}
+.sidebar-collapse{width:18px;height:34px;margin-right:-1px;border:1px solid #c6c6c6;border-right:0;border-radius:6px 0 0 6px;background:#f8f8f8;color:#818181;font-size:19px;line-height:1}
+.function-sidebar>button{position:relative;width:100%;height:42px;border:0;border-left:3px solid transparent;background:transparent;color:#4a4a4a;padding:0 18px 0 20px;text-align:left;font-size:13px}
+.function-sidebar>button:hover{background:#f5f5f5;color:#008cd6}
+.function-sidebar>button.active{border-left-color:#008cd6;background:#fff;color:#008cd6;font-weight:700}
+.function-sidebar>button:focus{outline:0}
+.function-sidebar>button:focus-visible{box-shadow:inset 0 0 0 2px #76bfe4}
+.sidebar-section-label{margin:10px 16px 4px;padding:10px 4px 6px;border-top:1px solid #d1d1d1;color:#999;font-size:11px}
+.content,.workspace,.settings-page{grid-column:2;grid-row:2;overflow-y:auto;min-height:0}
+.content{padding:32px max(24px,calc((100vw - 266px - 1016px)/2)) 42px}
+.nav-collapsed .content{padding-left:max(24px,calc((100vw - 56px - 1016px)/2));padding-right:max(24px,calc((100vw - 56px - 1016px)/2))}
+.workspace,.settings-page{max-width:none;margin:0}
+
+@media(max-width:980px){
+  .app-shell{grid-template-columns:1fr}
+  .left-navigation{display:none;position:fixed;top:56px;left:0;bottom:0;width:280px;max-width:85vw;z-index:30;background:#fff;border-right:1px solid #c9c9c9;overflow-y:auto;transition:transform .25s ease;transform:translateX(-100%);box-shadow:2px 0 12px rgba(0,0,0,.12);border-radius:0 12px 12px 0}
+  .left-navigation.mobile-open{display:flex;flex-direction:column;transform:translateX(0)}
+  .left-navigation.mobile-open .module-rail{display:none}
+  .left-navigation.mobile-open .function-sidebar{display:block!important;flex:1;overflow-y:auto;background:#fff}
+  .mobile-menu-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;margin-right:4px;border:0;background:#f0f2f5;border-radius:8px;font-size:20px;color:#323233;cursor:pointer;padding:0}
+  .mobile-menu-btn:hover{background:#e1e4e8}
+  .content{padding:22px 16px 34px}
+  /* 遮罩 */
+  .mobile-menu-overlay{position:fixed;top:56px;left:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:25;display:none}
+  .mobile-menu-overlay.show{display:block}
+  /* 手机端菜单内样式 */
+  .left-navigation.mobile-open .function-sidebar{background:#fff;padding-bottom:20px}
+  .left-navigation.mobile-open .sidebar-panel-title{background:#e8f0fe;border-radius:8px 8px 0 0;margin:0}
+  .left-navigation.mobile-open .function-sidebar>button{height:48px;font-size:14px;padding:0 16px}
+  .left-navigation.mobile-open .scenario-menu-group{margin:4px 0}
+  .left-navigation.mobile-open .sidebar-section-label{margin:14px 12px 4px;font-size:12px}
+}
+@media(max-width:680px){
+  .content{padding:22px  14px 34px}
+}
+
+/* 连接探测诊断 */
+.endpoint-results{margin-top:14px;border-top:1px dashed #d1d1d1;padding-top:10px}
+.endpoint-results h3{font-size:12px;color:#666;margin:0 0 8px}
+.ep-row{display:flex;flex-direction:column;gap:2px;padding:6px 8px;border-radius:6px;margin-bottom:6px;background:#f6f7f9;border-left:3px solid #c2c2c2}
+.ep-row.ok{border-left-color:#2f9e44;background:#f1faf2}
+.ep-row.fail{border-left-color:#e03131;background:#fff5f5}
+.ep-path{font-size:11px;word-break:break-all;color:#333}
+.ep-status{font-size:11px;font-weight:600}
+.ep-row.ok .ep-status{color:#2f9e44}
+.ep-row.fail .ep-status{color:#e03131}
+.ep-snippet,.ep-error{font-size:10px;color:#888;word-break:break-all;white-space:pre-wrap;max-height:80px;overflow:auto}
+
+/* 本次发出的请求头 */
+.ep-headers{display:flex;flex-wrap:wrap;gap:4px}
+.ep-headers code{font-size:10px;background:#eef1f4;color:#37506b;padding:2px 6px;border-radius:4px}
+
+/* Station-level AI scenario workspace */
+.scenario-menu-group{display:block}
+.scenario-menu-group .sidebar-section-label{margin:6px 16px 2px;padding:9px 4px 5px;border-top:1px solid #d1d1d1;color:#8b8b8b;font-size:11px}
+.scenario-menu-group:first-of-type .sidebar-section-label{border-top:0;margin-top:2px}
+.scenario-menu-group>button{position:relative;width:100%;height:39px;border:0;border-left:3px solid transparent;background:transparent;color:#4a4a4a;padding:0 14px 0 16px;text-align:left;font-size:13px;display:flex;align-items:center;gap:9px}
+.scenario-menu-group>button:hover{background:#f5f5f5;color:#008cd6}
+.scenario-menu-group>button.active{border-left-color:#008cd6;background:#fff;color:#008cd6;font-weight:700}
+.scenario-menu-icon{width:22px;height:22px;display:grid;place-items:center;color:#fff;font-size:11px;font-weight:700;border-radius:2px;flex:0 0 auto}
+.scenario-workspace{grid-column:2;grid-row:2;min-width:0;min-height:0;overflow:auto;background:#f4f6f8;color:#323233}
+.scenario-header{min-height:94px;padding:21px 28px 18px;background:#fff;border-bottom:1px solid #d4d8dc;display:flex;align-items:flex-start;gap:14px}
+.scenario-title-icon{width:42px;height:42px;display:grid;place-items:center;color:#fff;font-size:17px;font-weight:800;border-radius:2px;flex:0 0 auto}
+.scenario-header>div:nth-child(2)>span{display:block;color:#008cd6;font-size:11px;font-weight:700;margin-bottom:3px}
+.scenario-header h1{font-size:23px;line-height:1.25;margin:0;color:#222}
+.scenario-header p{font-size:12px;color:#7a7e83;margin:4px 0 0}
+.scenario-header-actions{margin-left:auto;display:flex;gap:8px;padding-top:8px}
+.scenario-header-actions button{height:34px;padding:0 14px;border:1px solid #cbd1d6;background:#fff;color:#72777c;border-radius:2px;font-size:12px}
+.scenario-header-actions .primary{background:#008cd6;border-color:#008cd6;color:#fff}
+.scenario-header-actions button:disabled{opacity:1;cursor:default}
+.station-context-bar{min-height:58px;background:#fff;border-bottom:1px solid #d7dadd;display:grid;grid-template-columns:1.25fr 1fr 1fr 1fr;padding:0 28px}
+.station-context-bar>div{display:flex;flex-direction:column;justify-content:center;gap:3px;border-right:1px solid #eceeef;padding:0 18px}
+.station-context-bar>div:first-child{padding-left:0}.station-context-bar>div:last-child{border-right:0}
+.station-context-bar small{font-size:10px;color:#969a9e}.station-context-bar strong{font-size:12px;color:#404449;font-weight:600}
+.station-context-bar .context-live{color:#16815f}.station-context-bar .context-preview{color:#bf771d}
+.scenario-body{display:grid;grid-template-columns:minmax(640px,1fr) 328px;gap:16px;padding:16px 18px 28px;max-width:1540px;margin:0 auto}
+.scenario-main{min-width:0}.scenario-aside{display:flex;flex-direction:column;gap:12px}
+.station-kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:12px}
+.station-kpi{position:relative;min-height:112px;background:#fff;border:1px solid #d7dadd;border-top:3px solid #8b949c;padding:15px 16px;display:flex;flex-direction:column;gap:5px}
+.station-kpi.good{border-top-color:#22a477}.station-kpi.warn{border-top-color:#efa12b}.station-kpi.bad{border-top-color:#df4d54}
+.station-kpi>span{font-size:11px;color:#777e85}.station-kpi>strong{font-family:Arial,sans-serif;font-size:28px;line-height:1.15;color:#222}.station-kpi>small{font-size:10px;color:#838a91}
+.station-kpi.good>small{color:#19845f}.station-kpi.warn>small{color:#b67519}.station-kpi.bad>small{color:#ca4148}
+.scenario-section,.ai-summary-card,.aside-card,.scope-card{background:#fff;border:1px solid #d7dadd}
+.scenario-section{padding:18px;margin-bottom:12px;min-height:288px}
+.section-heading{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:16px}
+.section-heading>div>span{display:block;color:#008cd6;font-family:Arial,sans-serif;font-size:9px;font-weight:800;letter-spacing:.8px;margin-bottom:3px}
+.section-heading h2{font-size:16px;margin:0;color:#292d31}.section-heading>small{font-size:10px;color:#92979c;padding-top:5px}
+.ai-summary-card{border-top:3px solid #008cd6;padding:17px}
+.ai-summary-title{display:flex;gap:10px;align-items:center}.ai-summary-title>span{width:31px;height:31px;display:grid;place-items:center;background:#008cd6;color:#fff;font-size:11px;font-weight:800}
+.ai-summary-title>div{display:flex;flex-direction:column;gap:2px}.ai-summary-title strong{font-size:13px}.ai-summary-title small{font-size:10px;color:#93989d}
+.ai-summary-card>p{margin:16px 0;color:#59636c;font-size:12px;line-height:1.85}
+.summary-evidence{display:flex;justify-content:space-between;border-top:1px dashed #d4d8dc;padding-top:11px;color:#2782ad;font-size:9px}
+.aside-card{padding:16px}.aside-card h3{font-size:13px;margin:0 0 12px}
+.source-list{display:grid;gap:8px}.source-list>div{display:grid;grid-template-columns:9px 1fr auto;gap:8px;align-items:center;border-bottom:1px solid #eff1f3;padding-bottom:8px;font-size:11px}
+.source-list i{width:7px;height:7px;border-radius:50%;background:#c2c6ca}.source-list i.ready{background:#24a477}.source-list small{font-size:9px;color:#989da2}.source-list i.ready~small{color:#18835f}
+.implementation-card>p{font-size:11px;line-height:1.75;color:#646b72;margin:0 0 14px}.implementation-card>div{background:#f2f7fa;border-left:3px solid #008cd6;padding:9px 10px;display:flex;flex-direction:column;gap:3px}.implementation-card small{font-size:9px;color:#8f959b}.implementation-card strong{font-size:11px;color:#247da8}
+.scope-card{border-left:4px solid #19a176;background:#f1faf6;padding:14px}.scope-card strong{font-size:12px}.scope-card p{font-size:10px;color:#68736e;line-height:1.7;margin:7px 0 0}
+.insight-section{min-height:auto}.insight-list{display:grid;gap:8px}.insight-list>div{min-height:46px;border:1px solid #e0e3e6;background:#fafbfc;display:grid;grid-template-columns:26px 1fr 44px;gap:9px;align-items:center;padding:7px 10px}
+.insight-list b{width:22px;height:22px;display:grid;place-items:center;background:#214d6e;color:#fff;font-size:10px;border-radius:50%}.insight-list span{font-size:11px}.insight-list em{font-style:normal;text-align:center;font-size:9px;padding:4px}.insight-list em.high{color:#c63f46;background:#fbecee}.insight-list em.medium{color:#ad6c16;background:#fff1db}.insight-list em.low{color:#47718b;background:#eaf3f7}
+.report-layout{display:grid;grid-template-columns:220px 1fr;gap:14px}.report-cover{background:linear-gradient(145deg,#075982,#0299c6);color:#fff;padding:18px;min-height:150px;display:flex;flex-direction:column}.report-cover>span{font-size:10px;opacity:.8}.report-cover>strong{font-size:25px;margin:9px 0}.report-cover>p{font-size:10px;line-height:1.7;margin:auto 0 0;opacity:.88}.report-chart .mini-bars{height:150px;border-color:#e0e3e6}.chart-caption{display:flex;justify-content:space-between;color:#92979c;font-size:9px;margin-top:5px}
+.report-chapters{display:grid;grid-template-columns:repeat(5,1fr);margin-top:14px;border:1px solid #e1e4e7}.report-chapters>div{padding:11px;border-right:1px solid #e1e4e7;display:flex;flex-direction:column;gap:4px}.report-chapters>div:last-child{border:0}.report-chapters b{font-size:9px;color:#008cd6}.report-chapters span{font-size:11px;font-weight:700}.report-chapters small{font-size:9px;color:#92979c}
+.qa-input-preview{height:52px;border:1px solid #bfc6cc;background:#fff;display:flex;align-items:center;justify-content:space-between;padding-left:15px;color:#40484f;font-size:12px}.qa-input-preview b{width:34px;height:34px;margin-right:8px;display:grid;place-items:center;background:#008cd6;color:#fff}.qa-suggestions{display:flex;flex-wrap:wrap;gap:7px;margin:10px 0}.qa-suggestions span{padding:6px 9px;background:#eef6fa;color:#317894;border:1px solid #d8e8ef;font-size:9px}.qa-answer-preview{display:grid;grid-template-columns:34px 1fr;gap:11px;margin-top:15px;padding:14px;background:#f8fafb;border-left:3px solid #008cd6}.answer-avatar{width:30px;height:30px;display:grid;place-items:center;background:#008cd6;color:#fff;font-size:10px;font-weight:800}.qa-answer-preview strong{font-size:12px}.qa-answer-preview p{font-size:10px;color:#657078;line-height:1.7;margin:6px 0}.answer-citations{display:flex;gap:6px}.answer-citations span{font-size:9px;color:#347993;background:#e8f3f7;padding:4px 7px}
+.compare-legend{display:flex;justify-content:flex-end;gap:15px;font-size:9px;color:#747b82;margin-bottom:11px}.compare-legend span{display:flex;align-items:center;gap:5px}.compare-legend i{width:14px;height:5px}.compare-legend .current{background:#008cd6}.compare-legend .baseline{background:#cad0d5}
+.compare-row{display:grid;grid-template-columns:110px 1fr 38px;gap:12px;align-items:center;margin:15px 0}.compare-row>strong{font-size:10px;color:#565e66}.compare-row>div{height:25px;position:relative;background:#f0f2f4}.compare-row i{position:absolute;left:0;height:8px}.compare-row i.baseline{top:4px;background:#cad0d5}.compare-row i.current{bottom:4px;background:#008cd6}.compare-row>b{font-size:12px;color:#0083bb}
+.operations-funnel{display:flex;flex-direction:column;align-items:center;gap:5px;margin-top:8px}.operations-funnel>div{height:42px;min-width:55%;background:linear-gradient(90deg,#dff3fa,#78c7e3);display:flex;align-items:center;justify-content:space-between;padding:0 15px;color:#21556b}.operations-funnel span{font-size:10px}.operations-funnel b{font-size:18px}.sla-track{display:grid;grid-template-columns:70px 1fr 36px;gap:8px;align-items:center;margin-top:16px;font-size:10px}.sla-track i{height:7px;background:#e3e7ea}.sla-track i b{display:block;height:100%;background:#20a379}
+.health-layout{display:grid;grid-template-columns:170px 1fr;gap:25px;align-items:center}.health-ring{width:145px;height:145px;border-radius:50%;margin:auto;background:conic-gradient(#20a379 0 86%,#e6eaed 86%);display:grid;place-content:center;text-align:center;position:relative}.health-ring:after{content:"";position:absolute;inset:14px;border-radius:50%;background:#fff}.health-ring strong,.health-ring span{position:relative;z-index:1}.health-ring strong{font-size:38px;color:#16875f}.health-ring span{font-size:10px;color:#6a747b}.health-dimensions{display:grid;gap:10px}.health-dimensions>div,.contribution-list>div{display:grid;grid-template-columns:88px 1fr 32px;gap:8px;align-items:center;font-size:10px}.health-dimensions i,.contribution-list i{height:7px;background:#e7eaed}.health-dimensions i b{display:block;height:100%;background:#20a379}.health-dimensions strong{font-size:10px;text-align:right}
+.event-clusters{display:grid;gap:9px}.event-clusters>div{min-height:55px;border:1px solid #dee2e5;border-left:4px solid #94a0a8;display:grid;grid-template-columns:28px 1fr 36px 65px;gap:10px;align-items:center;padding:8px 11px}.event-clusters>div.active{border-left-color:#df4d54;background:#fff8f8}.event-clusters>div>b{width:24px;height:24px;display:grid;place-items:center;border-radius:50%;background:#314f63;color:#fff;font-size:10px}.event-clusters>div>div{display:flex;flex-direction:column;gap:4px}.event-clusters strong{font-size:11px}.event-clusters small,.event-clusters em{font-size:9px;color:#858d94}.event-clusters>span{font-size:9px;text-align:center;background:#fbecee;color:#c74249;padding:4px}.event-clusters em{font-style:normal;text-align:right}
+.efficiency-layout{display:grid;grid-template-columns:180px 1fr;gap:28px;align-items:center}.efficiency-score{height:150px;background:#eff8f4;border-left:5px solid #20a379;display:flex;flex-direction:column;justify-content:center;padding:20px}.efficiency-score strong{font-size:38px;color:#17855f}.efficiency-score span{font-size:11px;font-weight:700}.efficiency-score small{font-size:9px;color:#b67519;margin-top:8px}.contribution-list{display:grid;gap:15px}.contribution-list i b{display:block;height:100%;background:#efa12b}.contribution-list strong{font-size:10px;text-align:right}
+.strategy-proposal{border:1px solid #bfe4d5;background:#f2faf7;padding:15px;display:flex;justify-content:space-between;gap:12px}.strategy-proposal>div{display:flex;flex-direction:column;gap:5px}.strategy-proposal span{font-size:9px;color:#16815f}.strategy-proposal strong{font-size:13px}.strategy-proposal p{font-size:10px;line-height:1.6;color:#66716c;margin:0}.strategy-proposal>b{height:max-content;white-space:nowrap;background:#dff4eb;color:#16815f;padding:5px 9px;font-size:9px}.strategy-flow{display:grid;grid-template-columns:repeat(6,1fr);margin-top:22px}.strategy-flow>div{position:relative;display:flex;flex-direction:column;align-items:center;gap:6px;color:#92979c;font-size:9px}.strategy-flow>div:not(:last-child):after{content:"";position:absolute;left:62%;right:-38%;top:13px;border-top:2px solid #d8dde1}.strategy-flow b{position:relative;z-index:1;width:27px;height:27px;border-radius:50%;display:grid;place-items:center;background:#e5e9ec;color:#737b82}.strategy-flow .done b{background:#20a379;color:#fff}.strategy-flow .done:not(:last-child):after{border-color:#20a379}
+.incident-banner{display:grid;grid-template-columns:90px 1fr 58px;gap:12px;align-items:center;border:1px solid #efd0d2;border-left:5px solid #df4d54;background:#fff7f7;padding:12px}.incident-banner>span{font-size:9px;background:#fbe7e9;color:#c53f46;padding:5px;text-align:center}.incident-banner strong{font-size:12px}.incident-banner p{font-size:9px;color:#777f86;margin:4px 0}.incident-banner>b{font-size:10px;color:#16815f}.incident-timeline{display:grid;grid-template-columns:repeat(4,1fr);margin-top:24px}.incident-timeline>div{position:relative;border-top:2px solid #cdd3d8;padding:16px 8px 0}.incident-timeline i{position:absolute;width:11px;height:11px;border-radius:50%;background:#efa12b;top:-6px;left:8px}.incident-timeline i.done{background:#20a379}.incident-timeline>div>b{font-size:10px;color:#657078}.incident-timeline>div>span{display:flex;flex-direction:column;gap:3px;margin-top:6px}.incident-timeline strong{font-size:10px}.incident-timeline small{font-size:9px;color:#8a9197}
+.forecast-chart{height:190px;position:relative;display:flex;align-items:flex-end;gap:10px;padding:20px 15px 25px;background:repeating-linear-gradient(to bottom,#fff 0,#fff 39px,#e7eaed 40px);border:1px solid #e0e3e6}.forecast-chart>div:not(.forecast-limit){flex:1;height:100%;display:flex;align-items:flex-end;justify-content:center;position:relative}.forecast-chart i{width:66%;display:block;background:linear-gradient(#55aed3,#008cd6);min-height:8px}.forecast-chart i.risk{background:linear-gradient(#f29a45,#df4d54)}.forecast-chart span{position:absolute;bottom:-17px;font-size:8px;color:#858c92}.forecast-limit{position:absolute;left:15px;right:15px;top:42px;border-top:1px dashed #df4d54;color:#c34148;font-size:8px;text-align:right;z-index:1}.forecast-note{margin-top:11px;display:flex;gap:12px;align-items:center;background:#fff4e7;border-left:3px solid #efa12b;padding:9px 11px}.forecast-note b{font-size:10px;color:#a56b1d}.forecast-note span{font-size:9px;color:#747b82}
+
+@media(max-width:1200px){.scenario-body{grid-template-columns:1fr}.scenario-aside{display:grid;grid-template-columns:repeat(2,1fr)}.scope-card{grid-column:1/-1}.station-kpi-grid{grid-template-columns:repeat(2,1fr)}}
+@media(max-width:980px){.scenario-workspace{grid-column:2}.station-context-bar{grid-template-columns:1fr 1fr}.station-context-bar>div{border-bottom:1px solid #eceeef}.scenario-aside{grid-template-columns:1fr}.scope-card{grid-column:auto}}
+@media(max-width:680px){.scenario-header{padding:16px}.scenario-header-actions{display:none}.station-context-bar{padding:0 14px}.station-context-bar>div{padding:10px 8px}.scenario-body{padding:10px;gap:12px}.station-kpi-grid{grid-template-columns:1fr 1fr;gap:8px}.station-kpi{min-height:88px;padding:12px 10px;border-radius:8px}.station-kpi>strong{font-size:24px}.report-layout,.health-layout,.efficiency-layout{grid-template-columns:1fr;gap:16px}.report-chapters{grid-template-columns:1fr 1fr}.strategy-flow{grid-template-columns:repeat(3,1fr);gap:16px}.incident-timeline{grid-template-columns:1fr}.incident-timeline>div{border-top:0;border-left:2px solid #cdd3d8;padding:4px 8px 14px 18px}.incident-timeline i{left:-6px;top:4px}.scenario-section{padding:13px}.section-heading>small{display:none}}
+
+/* Page-by-page data and calculation specification */
+.design-page{grid-column:2;grid-row:2;min-width:0;min-height:0;overflow:auto;background:#f4f6f8;color:#323233}
+.design-heading{min-height:104px;padding:22px 30px;background:#fff;border-bottom:1px solid #d4d8dc;display:flex;gap:15px;align-items:flex-start}
+.design-heading-icon{width:44px;height:44px;display:grid;place-items:center;background:#008cd6;color:#fff;font-size:17px;font-weight:800}
+.design-heading>div:nth-child(2)>span{display:block;color:#008cd6;font-size:11px;font-weight:700;margin-bottom:3px}
+.design-heading h1{font-size:23px;margin:0;color:#222}.design-heading p{font-size:12px;color:#7a7e83;margin:5px 0 0}
+.design-version{margin-left:auto;min-width:155px;border-left:3px solid #008cd6;background:#f1f8fb;padding:9px 12px;display:flex;flex-direction:column;gap:4px}
+.design-version small{font-size:9px;color:#8a9299}.design-version strong{font-size:11px;color:#287693}
+.design-status-strip{min-height:60px;background:#fff;border-bottom:1px solid #d8dcdf;padding:9px 30px;display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
+.design-status-strip>div{display:grid;grid-template-columns:10px auto 1fr;gap:8px;align-items:center;border-right:1px solid #eceeef;padding-right:12px}
+.design-status-strip>div:last-child{border:0}.design-status-strip span{font-size:10px;color:#7e858b}.design-status-strip strong{font-size:10px;color:#40474d;text-align:right}
+.status-dot{width:8px;height:8px;border-radius:50%}.status-dot.verified{background:#20a379}.status-dot.source{background:#008cd6}.status-dot.external{background:#efa12b}
+.design-layout{max-width:1540px;margin:0 auto;padding:16px 18px 32px;display:grid;grid-template-columns:225px minmax(620px,1fr);gap:14px;align-items:start}
+.design-index{position:sticky;top:0;background:#fff;border:1px solid #d7dadd;max-height:calc(100vh - 102px);overflow:auto}
+.design-index-title{padding:14px 15px;border-bottom:1px solid #d9dde0;display:flex;flex-direction:column;gap:4px}.design-index-title span{font-size:12px;font-weight:700}.design-index-title small{font-size:9px;color:#8b9298}
+.design-index>button{width:100%;min-height:48px;border:0;border-left:3px solid transparent;border-bottom:1px solid #eef0f2;background:#fff;padding:7px 10px;display:grid;grid-template-columns:27px 1fr;gap:8px;align-items:center;text-align:left}
+.design-index>button:hover{background:#f5fafc}.design-index>button.active{border-left-color:#008cd6;background:#eaf6fb}
+.design-index>button>b{font-family:Arial,sans-serif;font-size:10px;color:#9aa0a5}.design-index>button.active>b{color:#008cd6}
+.design-index>button>span{display:flex;min-width:0;flex-direction:column;gap:2px}.design-index>button strong{font-size:11px;color:#40464b}.design-index>button.active strong{color:#007eb8}.design-index>button small{font-size:8px;color:#9a9fa4}
+.design-content{min-width:0}.feasibility-card{min-height:102px;background:#fff;border:1px solid #d7dadd;border-top:3px solid #008cd6;padding:15px 17px;display:flex;gap:13px;align-items:flex-start}
+.feasibility-card>div:nth-child(2){min-width:0}.feasibility-card>div:nth-child(2)>span{font-size:9px;color:#008cd6;font-weight:700}.feasibility-card h2{font-size:17px;margin:3px 0 5px}.feasibility-card p{font-size:11px;color:#606a72;line-height:1.7;margin:0}.feasibility-card>em{margin-left:auto;white-space:nowrap;font-style:normal;background:#eaf6fb;color:#187b9f;padding:6px 9px;font-size:9px}
+.design-method-card{display:grid;grid-template-columns:1fr 1fr;background:#fff;border:1px solid #d7dadd;border-top:0}.design-method-card>div{padding:13px 16px}.design-method-card>div+div{border-left:1px solid #e1e4e7}.design-method-card small,.panel-spec-body small{display:block;color:#0082bb;font-size:9px;font-weight:700;margin-bottom:5px}.design-method-card p,.panel-spec-body p{font-size:10px;line-height:1.7;color:#667078;margin:0}
+.design-section-title{display:flex;justify-content:space-between;align-items:flex-end;margin:20px 2px 9px}.design-section-title span{display:block;color:#008cd6;font:800 9px Arial,sans-serif;letter-spacing:.7px}.design-section-title h2{font-size:16px;margin:3px 0 0}.design-section-title>small{font-size:9px;color:#8d949a}
+.panel-spec-list{display:grid;gap:9px}.panel-spec-card{background:#fff;border:1px solid #d7dadd;display:grid;grid-template-columns:205px minmax(0,1fr)}
+.panel-spec-head{padding:14px;display:grid;grid-template-columns:30px 1fr;gap:9px;align-content:start;background:#f7f9fa;border-right:1px solid #e0e3e6}.panel-spec-head>b{width:28px;height:28px;display:grid;place-items:center;background:#214d6e;color:#fff;font:700 10px Arial,sans-serif}.panel-spec-head>div{min-width:0}.panel-spec-head span{font-size:8px;color:#8b9298}.panel-spec-head h3{font-size:12px;line-height:1.4;margin:3px 0 0;color:#30363b}
+.panel-spec-body{padding:13px 15px;display:grid;grid-template-columns:1fr 1fr;gap:12px 18px}.panel-api-block{grid-column:1/-1;border-top:1px dashed #d8dcdf;padding-top:10px}.panel-api-list{display:grid;gap:5px}.panel-api-list>div{display:grid;grid-template-columns:72px minmax(240px,.8fr) 1fr;gap:8px;align-items:center;background:#f5f7f9;padding:6px 8px}.panel-api-list code{font-family:Consolas,"SFMono-Regular",monospace;font-size:9px;color:#345269;word-break:break-all}.panel-api-list p{font-size:9px;line-height:1.45;color:#7b8389}
+.api-status{padding:4px 5px;text-align:center;font-size:8px;font-weight:700}.api-status.verified{background:#e1f5ed;color:#15815d}.api-status.source{background:#e4f3fa;color:#087da9}.api-status.external{background:#fff0dc;color:#b87318}
+.design-footnote{margin-top:12px;border-left:4px solid #20a379;background:#eff9f5;padding:13px 15px}.design-footnote strong{font-size:11px}.design-footnote p{font-size:10px;line-height:1.7;color:#64716b;margin:5px 0 0}
+@media(max-width:1120px){.design-layout{grid-template-columns:190px minmax(540px,1fr)}.panel-spec-card{grid-template-columns:170px}.panel-api-list>div{grid-template-columns:68px 1fr}.panel-api-list p{grid-column:2}}
+@media(max-width:760px){.design-heading{padding:16px}.design-version{display:none}.design-status-strip{padding:8px 14px;grid-template-columns:1fr}.design-status-strip>div{border-right:0}.design-layout{grid-template-columns:1fr;padding:10px}.design-index{position:static;max-height:none;display:grid;grid-template-columns:repeat(2,1fr)}.design-index-title{grid-column:1/-1}.panel-spec-card{grid-template-columns:1fr}.panel-spec-head{border-right:0;border-bottom:1px solid #e0e3e6}.panel-spec-body,.design-method-card{grid-template-columns:1fr}.design-method-card>div+div{border-left:0;border-top:1px solid #e1e4e7}.panel-api-list>div{grid-template-columns:65px 1fr}.panel-api-list p{grid-column:1/-1}.feasibility-card>em{display:none}}
+
+/* Design guide readability and full-document exports */
+.design-page{font-size:14px}
+.design-page p,.design-page small,.design-page span,.design-page strong,.design-page code,.design-page button,.design-page em,.design-page li{font-size:12px!important;line-height:1.65}
+.design-page h1{font-size:25px}.design-page h2{font-size:18px}.design-page h3{font-size:14px}
+.design-heading{min-height:122px}.design-heading>div:nth-child(2)>span{font-size:12px!important}.design-heading p{margin-top:7px}
+.design-heading-actions{margin-left:auto;display:flex;flex-direction:column;align-items:flex-end;gap:9px;min-width:250px}
+.design-heading-actions .design-version{margin-left:0;width:100%}
+.design-export-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;width:100%}
+.design-export-actions button{height:36px;border:1px solid #008cd6;background:#fff;color:#087da9;font-weight:700;padding:0 13px}
+.design-export-actions button.primary{background:#008cd6;color:#fff}
+.export-notice{width:100%;background:#eaf8f2;border-left:3px solid #20a379;color:#177657;padding:6px 9px;font-size:12px;line-height:1.5}
+.design-status-strip{min-height:72px}.design-status-strip>div{grid-template-columns:12px auto 1fr;gap:9px}.design-status-strip strong{line-height:1.5}
+.design-layout{grid-template-columns:245px minmax(680px,1fr);gap:16px}.design-index>button{min-height:58px;padding:9px 11px}.design-index>button>b{font-size:12px}.design-index>button strong{font-size:13px}.design-index-title{padding:16px}
+.feasibility-card{min-height:120px;padding:17px 19px}.feasibility-card h2{font-size:19px}.feasibility-card p{line-height:1.75}
+.page-purpose-card{display:grid;grid-template-columns:1fr 1fr;gap:1px;margin-top:12px;background:#d7dadd;border:1px solid #d7dadd}
+.purpose-card{min-height:150px;background:#fff;padding:17px;display:grid;grid-template-columns:34px 1fr;gap:12px;align-content:start}
+.purpose-card>span{width:32px;height:32px;display:grid;place-items:center;background:#214d6e;color:#fff;font-weight:700}
+.purpose-card small{display:block;color:#0082bb;font-weight:700}.purpose-card h3{margin:3px 0 7px;color:#30363b}.purpose-card p{margin:0;color:#606b73;line-height:1.75}
+.purpose-card.value>span{background:#20a379}.purpose-card.ai>span{background:#7463e8}.purpose-card.evidence>span{background:#efa12b}
+.design-method-card{margin-top:12px}.design-method-card>div{padding:16px 18px}.design-method-card p,.panel-spec-body p{font-size:12px;line-height:1.75}
+.panel-spec-card{grid-template-columns:225px minmax(0,1fr)}.panel-spec-head{padding:17px}.panel-spec-head>b{width:32px;height:32px;font-size:12px}.panel-spec-head h3{font-size:14px}.panel-spec-body{padding:16px 18px;gap:15px 20px}
+.panel-api-list{gap:7px}.panel-api-list>div{grid-template-columns:90px minmax(300px,.9fr) 1fr;gap:10px;padding:9px 10px}.panel-api-list code{font-size:12px;line-height:1.55}.panel-api-list p{font-size:12px;line-height:1.55}
+.panel-api-list>div>*{min-width:0}.panel-api-list code{white-space:normal;overflow-wrap:anywhere;word-break:break-word}.panel-api-list p{overflow-wrap:anywhere}
+.api-status{font-size:12px;padding:5px 7px}.design-footnote{padding:16px 18px}.design-footnote p{font-size:12px}
+.architecture-chapter{max-width:1504px;margin:18px auto 4px;padding:0 18px;display:grid;gap:14px}
+.design-content>.architecture-chapter{max-width:none;margin:0;padding:0}.design-index>.architecture-index-button{border-top:8px solid #f4f6f8}.design-index>.architecture-index-button.active{border-top-color:#f4f6f8}
+.architecture-heading{background:linear-gradient(120deg,#073e62,#006f9d 62%,#008fc7);color:#fff;padding:24px 27px;display:flex;gap:18px;align-items:flex-start;border-top:4px solid #42d6ce;box-shadow:0 5px 18px rgba(8,52,78,.14)}
+.architecture-heading>div{min-width:0}.architecture-heading span,.architecture-section-title>span{display:block;font:800 12px Arial,sans-serif;letter-spacing:1px}.architecture-heading span{color:#7ce8e1}.architecture-heading h2{font-size:24px;margin:5px 0 7px;color:#fff}.architecture-heading p{margin:0;color:#d8eff7;line-height:1.7;max-width:980px}.architecture-heading>em{margin-left:auto;white-space:nowrap;font-style:normal;border:1px solid rgba(255,255,255,.35);background:rgba(255,255,255,.1);padding:8px 11px}
+.architecture-principles{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.architecture-principles>div{position:relative;background:#fff;border:1px solid #d3dce2;border-top:3px solid #008cd6;padding:18px 18px 17px;display:grid;grid-template-columns:58px 1fr;gap:4px 12px}.architecture-principles>div:nth-child(2){border-top-color:#7463e8}.architecture-principles>div:nth-child(3){border-top-color:#20a379}.architecture-principles b{grid-row:1/3;width:54px;height:54px;display:grid;place-items:center;background:#e7f5fb;color:#087da9;font:800 13px Arial,sans-serif;border-radius:50%}.architecture-principles>div:nth-child(2) b{background:#f0edff;color:#6753dc}.architecture-principles>div:nth-child(3) b{background:#e8f7f1;color:#16825f}.architecture-principles strong{font-size:15px;color:#27343d}.architecture-principles p{grid-column:2;margin:0;color:#66757f;line-height:1.7}
+.architecture-diagram-card,.architecture-catalog,.delivery-roadmap{background:#fff;border:1px solid #d3dce2;padding:22px 24px}.architecture-section-title{margin-bottom:16px}.architecture-section-title>span{color:#008cd6}.architecture-section-title h2{font-size:20px;margin:4px 0 5px;color:#26323b}.architecture-section-title p{margin:0;color:#6c7880;line-height:1.7}
+.architecture-guardrail{display:flex;align-items:center;gap:8px;background:#172f43;color:#fff;padding:10px 12px;margin-bottom:12px}.architecture-guardrail strong{margin-right:auto;color:#70e0d9}.architecture-guardrail span{border:1px solid #486075;background:#23445c;padding:5px 8px;white-space:nowrap}
+.architecture-flow{display:grid;gap:0;background:#eef3f6;border:1px solid #d0dbe1;padding:16px}.architecture-flow-wrap{display:grid;justify-items:center}.architecture-flow-wrap>i{height:24px;color:#1686b6;font-size:20px;font-style:normal;line-height:24px}.architecture-layer{width:100%;display:grid;grid-template-columns:54px minmax(0,1fr);gap:14px;background:#fff;border:1px solid #ccd7dd;border-left:6px solid #008cd6;padding:14px 16px}.architecture-layer.violet{border-left-color:#7463e8}.architecture-layer.blue{border-left-color:#2f7ed8}.architecture-layer.green{border-left-color:#20a379}.architecture-layer.orange{border-left-color:#efa12b}.architecture-layer>b{width:46px;height:46px;display:grid;place-items:center;background:#e8f5fb;color:#087da9;font:800 14px Arial,sans-serif}.architecture-layer.violet>b{background:#f0edff;color:#6654d8}.architecture-layer.green>b{background:#e8f7f1;color:#16825f}.architecture-layer.orange>b{background:#fff2df;color:#b67218}.architecture-layer span{display:block;font-size:16px!important;font-weight:800;color:#263842}.architecture-layer p{margin:3px 0 9px;color:#65747d;line-height:1.65}.architecture-layer div>div{display:flex;flex-wrap:wrap;gap:6px}.architecture-layer em{font-style:normal;background:#edf3f6;color:#47616f;padding:4px 7px;border-radius:2px}.architecture-rule{margin-top:12px;border-left:4px solid #20a379;background:#ebf8f3;padding:12px 14px;display:flex;gap:15px;align-items:center}.architecture-rule strong{color:#14785a;white-space:nowrap}.architecture-rule span{color:#506a61}
+.architecture-boundary{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:#d5dde2;border:1px solid #d5dde2}.architecture-boundary>div{background:#fff;padding:17px}.architecture-boundary span{display:inline-grid;place-items:center;width:30px;height:30px;background:#214d6e;color:#fff;font-weight:800}.architecture-boundary strong{display:block;margin:11px 0 5px;color:#273944}.architecture-boundary p{margin:0;color:#69767e;line-height:1.65}
+.architecture-table{border:1px solid #d4dde2}.architecture-table-head,.architecture-table>article{display:grid;grid-template-columns:220px minmax(190px,.8fr) minmax(280px,1.2fr) 62px}.architecture-table-head{background:#173f5b;color:#fff;font-weight:700}.architecture-table-head span{padding:9px 11px;border-right:1px solid #31566f}.architecture-table>article{background:#fff;border-top:1px solid #dce3e7;align-items:stretch}.architecture-table>article:nth-child(odd){background:#f8fafb}.architecture-table>article>*{margin:0;padding:11px 12px;border-right:1px solid #e0e5e8;line-height:1.65}.architecture-table>article>div{display:flex;flex-direction:column;gap:3px}.architecture-table>article>div b{color:#0083bc;font:800 12px Arial,sans-serif}.architecture-table>article>div strong{color:#2c3942}.architecture-table>article p{color:#65737c}.architecture-table>article em{font-style:normal;color:#087da9;font-weight:800;text-align:center;background:#edf7fb;display:grid;place-items:center}.architecture-table>article>*:last-child,.architecture-table-head>*:last-child{border-right:0}
+.mcp-contract-card{background:#0e2f47;color:#fff;border-left:6px solid #42d6ce;padding:21px 24px;display:grid;grid-template-columns:300px minmax(0,1fr);gap:14px 22px}.mcp-contract-card span{color:#6ce0d9;font-weight:800;letter-spacing:.8px}.mcp-contract-card h2{font-size:20px;color:#fff;margin:5px 0}.mcp-contract-card code{display:block;background:#071f31;border:1px solid #31556c;color:#b8f2ec;padding:14px;overflow-wrap:anywhere}.mcp-contract-card ul{grid-column:1/-1;margin:2px 0 0;padding-left:20px;color:#d5e6ee}.mcp-contract-card li{margin:5px 0;line-height:1.65}
+.delivery-roadmap>div:last-child{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.delivery-roadmap article{border:1px solid #d5dee3;border-top:4px solid #008cd6;padding:15px;display:grid;grid-template-columns:34px 1fr;gap:10px}.delivery-roadmap article:nth-child(2){border-top-color:#20a379}.delivery-roadmap article:nth-child(3){border-top-color:#7463e8}.delivery-roadmap article:nth-child(4){border-top-color:#efa12b}.delivery-roadmap article>b{width:32px;height:32px;display:grid;place-items:center;background:#214d6e;color:#fff}.delivery-roadmap article span{color:#0083bc;font-weight:800}.delivery-roadmap h3{font-size:15px;margin:3px 0 7px}.delivery-roadmap article p{margin:0;color:#66757e;line-height:1.65}
+.architecture-layer.cyan,.architecture-layer.violet,.architecture-layer.blue,.architecture-layer.green,.architecture-layer.orange{background:#fff!important;color:#263842!important}.architecture-layer.cyan span,.architecture-layer.violet span,.architecture-layer.blue span,.architecture-layer.green span,.architecture-layer.orange span{color:#263842!important}.architecture-layer.cyan p,.architecture-layer.violet p,.architecture-layer.blue p,.architecture-layer.green p,.architecture-layer.orange p{color:#65747d!important}.architecture-layer.cyan em,.architecture-layer.violet em,.architecture-layer.blue em,.architecture-layer.green em,.architecture-layer.orange em{background:#edf3f6!important;color:#47616f!important}
+@media(max-width:1250px){.design-layout{grid-template-columns:215px minmax(600px,1fr)}.panel-spec-card{grid-template-columns:190px}.panel-api-list>div{grid-template-columns:85px 1fr}.panel-api-list p{grid-column:2}.page-purpose-card{grid-template-columns:1fr}}
+@media(max-width:1250px){.architecture-boundary,.delivery-roadmap>div:last-child{grid-template-columns:1fr 1fr}.architecture-table-head,.architecture-table>article{grid-template-columns:190px 1fr}.architecture-table-head span:nth-child(3),.architecture-table-head span:nth-child(4){display:none}.architecture-table>article>p:nth-child(3){grid-column:2}.architecture-table>article>em{grid-column:1/-1;display:block;text-align:left;padding:6px 12px}}
+@media(max-width:760px){.design-heading-actions{min-width:0;width:100%;margin-left:0;align-items:stretch}.design-heading{flex-wrap:wrap}.design-heading>div:nth-child(2){flex:1;min-width:220px}.design-version{display:flex!important}.design-layout{grid-template-columns:1fr}.page-purpose-card{grid-template-columns:1fr}.panel-spec-card{grid-template-columns:1fr}.panel-api-list>div{grid-template-columns:82px 1fr}.panel-api-list p{grid-column:1/-1}.architecture-chapter{padding:0 10px;margin-top:10px}.architecture-heading{padding:18px;flex-wrap:wrap}.architecture-heading>em{margin-left:0}.architecture-principles,.architecture-boundary,.delivery-roadmap>div:last-child{grid-template-columns:1fr}.architecture-diagram-card,.architecture-catalog,.delivery-roadmap{padding:16px}.architecture-guardrail{align-items:flex-start;flex-wrap:wrap}.architecture-guardrail strong{width:100%}.architecture-layer{grid-template-columns:42px 1fr;padding:12px}.architecture-layer>b{width:38px;height:38px}.architecture-table-head{display:none}.architecture-table>article{grid-template-columns:1fr}.architecture-table>article>p:nth-child(3),.architecture-table>article>em{grid-column:1}.mcp-contract-card{grid-template-columns:1fr;padding:18px}.mcp-contract-card ul{grid-column:1}.architecture-rule{align-items:flex-start;flex-direction:column;gap:5px}}
+
+/* Live station daily report and asset-group context */
+.asset-group-context select{max-width:100%;height:29px;border:1px solid #bfc8cf;background:#fff;color:#1f4d68;padding:0 30px 0 8px;font-size:12px;font-weight:700;outline:none}
+.asset-group-context select:focus{border-color:#008cd6;box-shadow:0 0 0 2px rgba(0,140,214,.12)}
+.live-daily-report{min-height:auto}
+.daily-report-overview{display:grid;grid-template-columns:250px minmax(0,1fr);gap:14px}
+.live-daily-report .report-cover{min-height:205px}.live-daily-report .report-cover>span{font-size:12px}.live-daily-report .report-cover>strong{font-size:27px}.live-daily-report .report-cover>p{font-size:12px;line-height:1.75}.live-daily-report .report-cover>em{margin-top:12px;padding-top:9px;border-top:1px solid rgba(255,255,255,.28);font-size:12px;font-style:normal;opacity:.9}
+.daily-health-block{min-height:205px;border:1px solid #e0e4e7;background:#fbfcfd;display:grid;grid-template-columns:190px 1fr;gap:22px;align-items:center;padding:18px 24px}
+.daily-health-ring{--health:0%;width:148px;height:148px;border-radius:50%;margin:auto;background:conic-gradient(#20a379 0 var(--health),#e4e8eb var(--health));display:grid;place-content:center;text-align:center;position:relative}
+.daily-health-ring:after{content:"";position:absolute;inset:14px;border-radius:50%;background:#fff}.daily-health-ring strong,.daily-health-ring span{position:relative;z-index:1}.daily-health-ring strong{font:700 37px Arial,sans-serif;color:#187f5d}.daily-health-ring span{font-size:12px;color:#6f777d;margin-top:3px}
+.daily-status-distribution{display:grid;gap:11px}.daily-status-distribution>div{display:grid;grid-template-columns:55px minmax(100px,1fr) 30px;gap:10px;align-items:center}.daily-status-distribution span,.daily-status-distribution strong{font-size:12px}.daily-status-distribution strong{text-align:right}.daily-status-distribution i{height:9px;background:#e8ebee;overflow:hidden}.daily-status-distribution i b{display:block;height:100%;min-width:0}.daily-status-distribution .running{background:#20a379}.daily-status-distribution .warning{background:#efa12b}.daily-status-distribution .fault{background:#df4d54}.daily-status-distribution .offline{background:#7e8991}.daily-status-distribution .unknown{background:#b9c0c5}
+.report-data-boundary{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:13px;border-left:4px solid #008cd6;background:#eef7fb;padding:10px 12px}.report-data-boundary strong{font-size:12px;margin-right:4px}.report-data-boundary span{font-size:12px;background:#e0f3eb;color:#177b5c;padding:5px 8px}.report-data-boundary span.pending{background:#fff0dd;color:#a66a18}
+.daily-exception-section{min-height:auto}.daily-device-table{border:1px solid #dce1e5}.daily-device-table>div{display:grid;grid-template-columns:1.05fr 1.45fr .65fr 60px 1.6fr;gap:10px;align-items:center;min-height:43px;padding:8px 11px;border-bottom:1px solid #edf0f2}.daily-device-table>div:last-child{border-bottom:0}.daily-device-table .daily-device-head{min-height:36px;background:#f2f5f7;color:#747c82;font-weight:700}.daily-device-table span,.daily-device-table strong,.daily-device-table em{font-size:12px;min-width:0}.daily-device-table strong{color:#28343b}.daily-device-table span{color:#69737a}.device-status{font-style:normal;text-align:center;padding:4px 5px;background:#edf0f2;color:#59636a}.device-status.故障{background:#fbe6e8;color:#c43b43}.device-status.告警{background:#fff0dc;color:#b36d13}.device-status.离线{background:#eceff1;color:#59656d}.empty-daily-state{padding:35px;text-align:center;background:#f8fafb}.empty-daily-state strong{font-size:15px}.empty-daily-state p{font-size:12px;color:#777f85}
+@media(max-width:980px){.daily-report-overview{grid-template-columns:1fr}.daily-device-table>div{grid-template-columns:1fr 1fr 60px}.daily-device-table>div>span:nth-child(3),.daily-device-table>div>span:nth-child(5){display:none}}
+@media(max-width:680px){.daily-health-block{grid-template-columns:1fr}.daily-device-table>div{grid-template-columns:1fr 72px;gap:6px 8px}.daily-device-table>div>span:nth-child(2){display:none}.daily-device-table>div>span:nth-child(1){font-size:13px;white-space:normal;word-break:break-word;line-height:1.4}}
+
+/* AI Insight: differentiated visual identity + editable prompt workbench */
+.insight-emphasis{position:relative;border:1px solid #cbc5f1;border-top:4px solid #6553c7;background:linear-gradient(135deg,#fff 0,#fff 62%,#f5f2ff 100%);box-shadow:0 7px 22px rgba(72,55,151,.09);padding:20px}
+.insight-emphasis:before{content:"AI";position:absolute;right:18px;bottom:8px;font:800 64px Arial,sans-serif;color:rgba(91,72,188,.045);pointer-events:none}
+.insight-heading{align-items:center;margin-bottom:15px}.insight-heading>div:first-child>span{display:inline-flex;align-items:center;width:max-content;background:#6652c6;color:#fff!important;padding:5px 9px;font-size:12px!important;letter-spacing:1px;margin-bottom:7px}.insight-heading h2{color:#4f3daf;font-size:19px}.insight-heading p{margin:5px 0 0;color:#766fa0;font-size:12px}
+.insight-heading-actions{display:flex;align-items:center;gap:10px}.insight-heading-actions small{font-size:12px;color:#77718f}.insight-heading-actions button{height:34px;border:1px solid #7766d2;background:#fff;color:#5947b5;padding:0 12px;font-size:12px;font-weight:700}.insight-heading-actions button:hover,.insight-heading-actions button.active{background:#6652c6;color:#fff}
+.insight-emphasis .insight-list{position:relative;z-index:1;gap:9px}.insight-emphasis .insight-list>div{min-height:54px;border-color:#ddd8f4;border-left:4px solid #8a7cdb;background:rgba(255,255,255,.94);grid-template-columns:30px 1fr 52px;padding:9px 12px}.insight-emphasis .insight-list>div:first-child{border-left-color:#df4d54;background:#fffafa}.insight-emphasis .insight-list b{width:25px;height:25px;background:#6251ba;font-size:12px}.insight-emphasis .insight-list span{font-size:13px;line-height:1.55}.insight-emphasis .insight-list em{font-size:12px;padding:5px}
+.prompt-workbench{position:relative;z-index:2;margin:0 0 15px;border:1px solid #cfc8f2;background:#f8f7fd;padding:14px}.prompt-workbench-title{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:9px}.prompt-workbench-title>div{display:flex;flex-direction:column;gap:3px}.prompt-workbench-title strong{font-size:14px;color:#43358f}.prompt-workbench-title span{font-size:12px;color:#77718d}.prompt-workbench-title em{font-style:normal;font-size:12px;background:#e8e3fb;color:#5a48b3;padding:5px 8px}
+.prompt-workbench textarea{display:block;width:100%;min-height:172px;resize:vertical;border:1px solid #bdb5e7;background:#fff;color:#333943;padding:12px 13px;font:12px/1.7 Consolas,"Noto Sans SC",sans-serif;outline:none}.prompt-workbench textarea:focus{border-color:#6652c6;box-shadow:0 0 0 2px rgba(102,82,198,.12)}
+.prompt-workbench-actions{display:flex;align-items:center;gap:8px;margin-top:10px}.prompt-workbench-actions>span{margin-right:auto;color:#77718d;font-size:12px}.prompt-workbench-actions button{height:34px;border:1px solid #b9b1de;background:#fff;color:#5d50a2;padding:0 12px;font-size:12px;font-weight:700}.prompt-workbench-actions button.primary{background:#6652c6;border-color:#6652c6;color:#fff}.prompt-workbench-actions button:disabled{opacity:.6;cursor:wait}
+@media(max-width:680px){.insight-heading{align-items:flex-start}.insight-heading-actions{align-items:flex-end;flex-direction:column}.prompt-workbench-actions{align-items:stretch;flex-direction:column}.prompt-workbench-actions>span{margin:0}.prompt-workbench-actions button{width:100%}}
+
+/* Live station Q&A */
+.station-qa-live{min-height:540px;padding:20px;border-top:4px solid #008cd6}.qa-live-heading{align-items:center}.qa-live-heading>div>span{display:inline-flex!important;width:max-content;background:#e5f5fb;color:#007ba9!important;padding:4px 8px;font-size:12px!important}.qa-live-heading h2{font-size:19px}.qa-live-heading p{font-size:12px;color:#73808a;margin:5px 0 0}.qa-live-heading>small{font-size:12px;background:#e5f5fb;color:#087da9;padding:6px 9px}
+.station-qa-shell{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(300px,.75fr);border:1px solid #d7dde1;min-height:430px}.station-qa-conversation{min-width:0;padding:16px;border-right:1px solid #d7dde1;background:#fff}.qa-scope-strip{min-height:44px;display:flex;align-items:center;gap:9px;background:#eef8fc;border-left:4px solid #008cd6;padding:9px 11px}.qa-scope-strip span,.qa-scope-strip strong,.qa-scope-strip em{font-size:12px}.qa-scope-strip span{color:#68757e}.qa-scope-strip strong{color:#007eaf}.qa-scope-strip em{margin-left:auto;font-style:normal;color:#6d7880}
+.qa-question-box{display:grid;grid-template-columns:1fr 90px;border:1px solid #aebbc4;margin-top:13px;background:#fff}.qa-question-box textarea{min-height:76px;resize:vertical;border:0;outline:0;padding:12px 13px;font-size:13px;line-height:1.6}.qa-question-box button{border:0;background:#008cd6;color:#fff;font-size:13px;font-weight:700;margin:7px;min-height:42px}.qa-question-box button:disabled{background:#9acde2;cursor:wait}
+.qa-quick-questions{display:flex;flex-wrap:wrap;gap:7px;margin:10px 0 16px}.qa-quick-questions button{border:1px solid #c9e2ec;background:#f2fafc;color:#29728f;padding:7px 9px;font-size:12px}.qa-quick-questions button:hover{border-color:#008cd6;background:#e4f5fb}.qa-quick-questions button:disabled{opacity:.6}
+.qa-live-answer{display:grid;grid-template-columns:38px 1fr;gap:11px;border:1px solid #d9e0e4;border-left:4px solid #008cd6;background:#f9fbfc;padding:15px}.qa-live-answer.empty{border-left-color:#9aa7af}.qa-ai-avatar{width:34px;height:34px;display:grid;place-items:center;background:#008cd6;color:#fff;font-size:12px;font-weight:800}.qa-answer-meta{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.qa-answer-meta strong{font-size:13px;color:#2d3a42}.qa-answer-meta span{font-size:12px;color:#16815f;white-space:nowrap}.qa-live-answer p{white-space:pre-wrap;font-size:13px;line-height:1.85;color:#515f67;margin:9px 0 0}.qa-answer-citations{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}.qa-answer-citations span{font-size:12px;color:#28718e;background:#e7f4f8;padding:5px 8px}
+.station-qa-evidence{background:#f7f9fa;padding:15px;min-width:0}.qa-evidence-title{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:1px solid #dce1e4;padding-bottom:10px}.qa-evidence-title span{display:block;color:#008cd6;font:800 12px Arial,sans-serif;letter-spacing:.7px}.qa-evidence-title h3{font-size:15px;margin:3px 0 0}.qa-evidence-title em{font-style:normal;color:#77828a;font-size:12px}.qa-evidence-list{display:grid;gap:7px;margin-top:11px}.qa-evidence-list>div:not(.qa-no-evidence){display:grid;grid-template-columns:10px 1fr 48px;gap:8px;align-items:center;background:#fff;border:1px solid #e0e4e7;padding:8px}.qa-evidence-list>div>div{display:flex;min-width:0;flex-direction:column;gap:3px}.qa-evidence-list strong,.qa-evidence-list span,.qa-evidence-list em{font-size:12px}.qa-evidence-list>div>div span{color:#7b848b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.qa-status-dot{width:8px;height:8px;border-radius:50%;background:#9aa4aa}.qa-status-dot.运行,.qa-status-dot.正常{background:#20a379}.qa-status-dot.告警{background:#efa12b}.qa-status-dot.故障{background:#df4d54}.qa-status-dot.离线{background:#7f8990}.qa-no-evidence{padding:24px 12px;text-align:center;background:#fff;border:1px dashed #cdd4d8;display:flex;flex-direction:column;gap:5px}.qa-no-evidence span{color:#899198}
+.qa-api-trace{margin-top:15px;border-top:1px solid #dce1e4;padding-top:11px}.qa-api-trace>strong{display:block;font-size:13px;margin-bottom:7px}.qa-api-trace>div{display:grid;grid-template-columns:50px 1fr;gap:7px;align-items:center;margin-top:6px}.qa-api-trace span{font-size:12px;text-align:center;padding:3px}.qa-api-trace span.verified{background:#def3ea;color:#15805c}.qa-api-trace span.snapshot{background:#e4f3fa;color:#087da9}.qa-api-trace span.pending{background:#fff0dc;color:#aa6c17}.qa-api-trace code{font:12px/1.4 Consolas,monospace;color:#506270;word-break:break-all}
+@media(max-width:1050px){.station-qa-shell{grid-template-columns:1fr}.station-qa-conversation{border-right:0;border-bottom:1px solid #d7dde1}.qa-scope-strip{flex-wrap:wrap}.qa-scope-strip em{width:100%;margin-left:0}}
+@media(max-width:680px){.qa-question-box{grid-template-columns:1fr}.qa-question-box button{height:40px}.qa-answer-meta{flex-direction:column}.qa-answer-meta span{white-space:normal}.qa-live-answer{grid-template-columns:1fr}.qa-ai-avatar{width:30px;height:30px}}
+
+/* Asset-group switch diagnostics: keep confirmed totals visible without mixing group data. */
+.asset-data-gap{display:flex;align-items:center;gap:12px;margin:12px 28px 0;border:1px solid #e7bd78;border-left:4px solid #e49a27;background:#fff8ec;padding:10px 13px;color:#6e5124}
+.asset-data-gap strong{font-size:13px;white-space:nowrap}.asset-data-gap span{font-size:12px;line-height:1.55}
+@media(max-width:680px){.asset-data-gap{margin:10px 14px 0;align-items:flex-start;flex-direction:column;gap:4px}.asset-data-gap strong{white-space:normal}}
+
+/* Real asset-group month-over-month operations analysis */
+.period-live-panel{display:grid;gap:14px}.period-toolbar{min-height:76px;border:1px solid #d6dce1;border-top:4px solid #6553c7;background:linear-gradient(100deg,#fff 0,#fff 70%,#f4f1ff 100%);padding:14px 16px;display:flex;align-items:center;justify-content:space-between;gap:20px}.period-toolbar>div:first-child>span,.period-card-heading>div>span{display:block;color:#5b49ba;font-size:12px;font-weight:800;letter-spacing:.7px}.period-toolbar h2{margin:3px 0;font-size:20px;color:#34296f}.period-toolbar p{margin:0;color:#727a83;font-size:12px}.period-toolbar-actions{display:flex;align-items:center;gap:11px}.period-toolbar-actions small{max-width:320px;color:#6f7680;font-size:12px;text-align:right}.period-toolbar-actions button,.period-ai-title button{height:34px;border:1px solid #6553c7;background:#6553c7;color:#fff;padding:0 13px;font-size:12px;font-weight:700}.period-toolbar-actions button:disabled,.period-ai-title button:disabled{opacity:.55;cursor:wait}
+.period-state{border:1px solid #d8dde1;background:#fff;padding:18px;display:flex;flex-direction:column;gap:6px}.period-state strong{font-size:14px}.period-state span,.period-state small{font-size:12px;line-height:1.6}.period-state.error{border-left:5px solid #df4d54;background:#fff7f7}.period-state.error strong{color:#bd373e}.period-state.loading{border-left:5px solid #008cd6;background:#f2f9fc}.period-state.loading strong{color:#087aa7}
+.period-scope-status{min-height:54px;border:1px solid #c9ded5;border-left:5px solid #20a379;background:#f2faf7;padding:10px 13px;display:flex;align-items:center;justify-content:space-between;gap:15px}.period-scope-status>div{display:flex;flex-direction:column;gap:4px}.period-scope-status strong{font-size:13px}.period-scope-status span{color:#62706a;font-size:12px}.period-scope-status em{font-style:normal;background:#dff3ea;color:#16815f;padding:5px 8px;font-size:12px;font-weight:800}.period-scope-status.warning{border-color:#ead2a9;border-left-color:#e99b27;background:#fff9ef}.period-scope-status.warning em{background:#fff0d8;color:#a96d18}
+.period-kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.period-kpi-grid article{min-height:112px;border:1px solid #d8dde1;background:#fff;padding:13px;display:flex;flex-direction:column;gap:8px}.period-kpi-grid article>span{color:#6f7780;font-size:12px}.period-kpi-grid article>div{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.period-kpi-grid article strong{font-size:23px;color:#26343e}.period-kpi-grid article small{color:#8a9198;font-size:12px}.period-kpi-grid article>em{width:max-content;font-style:normal;font-size:12px;padding:4px 7px;background:#eef1f3;color:#6c747a}.period-kpi-grid article>em.positive{background:#e4f5ed;color:#17805e}.period-kpi-grid article>em.negative{background:#fce9ea;color:#c23b42}
+.period-analysis-grid{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(310px,.65fr);gap:12px}.period-chart-card,.period-contribution-card,.period-device-card,.period-ai-card,.period-api-trace{border:1px solid #d8dde1;background:#fff;padding:16px}.period-card-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;border-bottom:1px solid #e5e8ea;padding-bottom:10px;margin-bottom:12px}.period-card-heading h3{margin:3px 0 0;font-size:15px}.period-card-heading>small{font-size:12px;color:#808890}.period-chart-card .compare-legend{font-size:12px;margin:2px 0 0}
+.period-compare-row{display:grid;grid-template-columns:150px 1fr 55px;gap:12px;align-items:center;margin:16px 0}.period-compare-row>strong{font-size:12px;color:#555f67}.period-compare-row>div{height:28px;position:relative;background:#f1f3f5}.period-compare-row i{position:absolute;left:0;height:8px}.period-compare-row i.baseline{top:4px;background:#c8cdd2}.period-compare-row i.current{bottom:4px;background:#6553c7}.period-compare-row>span{text-align:right;color:#4e3cac;font-size:13px;font-weight:700}.period-method-note{margin:12px 0 0;border-left:3px solid #8b7bd3;background:#f6f4fc;color:#6d687e;padding:9px 11px;font-size:12px;line-height:1.65}
+.period-contribution-list{display:grid;gap:10px}.period-contribution-list>div:not(.period-empty){display:grid;grid-template-columns:24px 95px 1fr 42px;gap:8px;align-items:center}.period-contribution-list b{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;background:#2e536b;color:#fff;font-size:12px}.period-contribution-list span{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.period-contribution-list i{height:8px;background:#eceff1}.period-contribution-list i strong{display:block;height:100%;background:#efa12b}.period-contribution-list em{font-style:normal;text-align:right;font-size:12px;color:#6f777d}.period-empty{border:1px dashed #ccd3d8;background:#fafbfc;color:#848c93;padding:20px;text-align:center;font-size:12px}
+.period-device-table{display:grid}.period-device-table>div{min-height:38px;border-bottom:1px solid #edf0f2;display:grid;grid-template-columns:1.1fr 1.8fr 90px 90px;gap:10px;align-items:center;padding:7px 9px}.period-device-table .head{background:#f2f5f7;color:#68727a;font-size:12px;font-weight:700}.period-device-table strong,.period-device-table span,.period-device-table em,.period-device-table b{font-size:12px}.period-device-table span{color:#737c83}.period-device-table em{font-style:normal;color:#5d49b8}.period-device-table b{color:#c43e45}.period-device-table .period-empty{display:block;min-height:auto}
+.period-ai-card{border-top:4px solid #6553c7;background:linear-gradient(135deg,#fff,#f7f5ff)}.period-ai-title{display:flex;align-items:center;gap:10px}.period-ai-title>span{width:34px;height:34px;display:grid;place-items:center;background:#6553c7;color:#fff;font-size:12px;font-weight:800}.period-ai-title>div{display:flex;flex-direction:column;gap:3px}.period-ai-title strong{font-size:14px;color:#443493}.period-ai-title small{font-size:12px;color:#80799d}.period-ai-title button{margin-left:auto}.period-ai-card>p{white-space:pre-wrap;margin:13px 0 0;color:#515b64;font-size:13px;line-height:1.8}
+.period-api-trace>strong{display:block;font-size:13px;margin-bottom:10px}.period-api-trace>div{display:grid;gap:6px}.period-api-trace span{display:grid;grid-template-columns:78px 1fr 55px;gap:9px;align-items:center;background:#f7f9fa;padding:7px 9px}.period-api-trace b,.period-api-trace code,.period-api-trace em{font-size:12px}.period-api-trace b{color:#16815f}.period-api-trace span.bad b{color:#c43e45}.period-api-trace code{font-family:Consolas,monospace;color:#4e5c66;word-break:break-all}.period-api-trace em{font-style:normal;text-align:right;color:#79828a}
+@media(max-width:1150px){.period-kpi-grid{grid-template-columns:1fr 1fr}.period-analysis-grid{grid-template-columns:1fr}}
+@media(max-width:680px){.period-toolbar{align-items:flex-start;flex-direction:column}.period-toolbar-actions{width:100%;align-items:flex-start;flex-direction:column}.period-toolbar-actions small{text-align:left}.period-kpi-grid{grid-template-columns:1fr}.period-compare-row{grid-template-columns:110px 1fr 42px}.period-device-table>div{grid-template-columns:1fr 60px}.period-device-table>div>span,.period-device-table>div>b{display:none}.period-api-trace span{grid-template-columns:70px 1fr}.period-api-trace em{display:none}}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644 (file)
index 0000000..9b67590
--- /dev/null
@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./index.css";
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>
+);
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
new file mode 100644 (file)
index 0000000..11f02fe
--- /dev/null
@@ -0,0 +1 @@
+/// <reference types="vite/client" />
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644 (file)
index 0000000..17f43b1
--- /dev/null
@@ -0,0 +1,21 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx",
+    "strict": true,
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["src"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644 (file)
index 0000000..97ede7e
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true,
+    "strict": true
+  },
+  "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644 (file)
index 0000000..aa114ee
--- /dev/null
@@ -0,0 +1,17 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+// 前端 dev 端口 5173(避开你常用 1060/1061/2341/4999)
+// /api 代理到当前项目 FastAPI 后端(8000)
+export default defineConfig({
+  plugins: [react()],
+  server: {
+    port: 5173,
+    proxy: {
+      "/api": {
+        target: "http://localhost:8000",
+        changeOrigin: true,
+      },
+    },
+  },
+});
diff --git a/备案信息.md b/备案信息.md
new file mode 100644 (file)
index 0000000..fb7f9c5
--- /dev/null
@@ -0,0 +1,76 @@
+# StationAIDemo 备案信息
+
+> 用途:记录 StationAIDemo 的部署与备案相关事实,供后续 ICP 备案或填写页面备案号使用。
+> 维护规则:本站所有任务均指 StationAIDemo,与「学习山峰」等其他项目无关。
+
+## 1. 项目基本信息
+
+| 项 | 内容 |
+|----|------|
+| 项目名称 | StationAIDemo |
+| 定位 | 基于 `desk-desk-cluster01.cn.wise-insightapm.com` 站点真实 API 的「场站 AI 分析 Demo」 |
+| 技术栈 | Vite + React + TypeScript(前端) / FastAPI + uvicorn(后端) |
+| 本地开发 | 前端 5173,后端 8000 |
+| 代码仓库 | SSH:`root@gitweb.acesimba.cloud:/var/lib/git/StationAIDemo.git`(分支 main) |
+| 部署形态 | 形态 B:Vite+React 前端 + FastAPI 后端 |
+
+## 2. 部署信息
+
+| 项 | 内容 |
+|----|------|
+| 服务器 | 腾讯云 Lighthouse,公网 IP `124.222.157.124` |
+| 部署方式 | 云端 git 拉取后由 codebuddy CLI 同步 |
+| 前端/后端端口 | 项目内用 1060/1061/2341/4999 系列;本地开发 5173 / 8000 |
+| 域名 | 暂未绑定自有域名,目前以 IP 直连 + wise-insightapm.com 子应用形式访问 |
+| 访问入口 | 站点为 wise-insightapm.com 的子应用(WISE-IoT 门户) |
+
+## 3. 备案现状
+
+| 项 | 状态 |
+|----|------|
+| ICP 备案(工信部) | ✅ 陕ICP备2026022239号-2(已写入前端 .env,页脚已显示) |
+| 公安备案(联网备案) | ⬜ 待办理 |
+| 域名 | 待定(需先有自有域名再备案) |
+| 备案主体 | 待填(见第 5 节占位) |
+
+> 说明:若仅以 IP 提供境外/境内服务,境内服务器对外提供 Web 服务仍需 ICP 备案。
+> 若后续绑定自有域名 `xxx.cn`,必须先在域名服务商处将域名实名 + 在接入商(腾讯云)提交 ICP 备案,备案通过前域名不可指向该服务器提供网页服务。
+
+## 4. ICP 备案所需材料清单
+
+- [ ] 域名证书 / 域名持有证明(已完成域名注册与实名认证)
+- [ ] 主办单位证件(企业:营业执照;个人:身份证)
+- [ ] 主办单位负责人身份证正反面
+- [ ] 负责人手机号 + 邮箱(需可接收核验短信/电话)
+- [ ] 真实性核验单(腾讯云后台下载,需签字/盖章)
+- [ ] 网站负责人人脸核验(腾讯云 APP 刷脸)
+- [ ] 服务器所在地与备案省份一致(腾讯云 Lighthouse 选对应地域)
+- [ ] 备案幕布/背景照片(按接入商要求)
+- [ ] 若涉及前置审批行业(新闻/医疗/教育等),需对应许可证
+
+## 5. 备案主体信息(占位,待填)
+
+| 项 | 内容(占位,勿留空) |
+|----|------|
+| 主办单位名称 | 「填写企业全称或个人姓名」 |
+| 主办单位性质 | 企业 / 个人 |
+| 证件号码 | 「填写统一社会信用代码或身份证号」 |
+| 负责人姓名 | 「填写」 |
+| 负责人手机 | 「填写」 |
+| 负责人邮箱 | 「填写」 |
+| 备案域名 | 「填写,如 stationaidemo.cn」 |
+| 服务器接入商 | 腾讯云(Lighthouse) |
+
+## 6. 风险与注意事项
+
+- 备案通过前,自有域名不得解析到 124.222.157.124 提供网页服务(会被接入商阻断)。
+- 当前以 wise-insightapm.com 子应用形态嵌入,不涉及自有域名备案,但该站点本身是否已完成 ICP 备案需向其运营方确认。
+- 公安备案(联网备案)需在网站开通后 30 日内另行办理,独立于 ICP 备案。
+- 主体信息属敏感实名数据,本文档仅留占位,真实信息请在线下安全渠道提交,勿明文入库泄露。
+
+## 7. 下一步
+
+1. 确定是否申请自有域名;若否,则维持站内子应用形态,不涉及自有备案。
+2. 若要自有域名 + 对外 Web,按第 4 节准备材料,在腾讯云提交 ICP 备案。
+3. 备案通过后,在页面页脚添加 `ICP 备案号` 与 `公安备案号` 链接(可后续在前端加)。
+4. 将真实主体信息在线下填写,不在此文档留存明文敏感字段。