From a825c3c31d0ded733113ef3cc186b4531b35269a Mon Sep 17 00:00:00 2001 From: Huarch Date: Tue, 2 Jun 2026 17:42:02 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=96=B0=E6=95=B4=E7=90=86=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=E5=92=8C=E5=B7=A5=E5=85=B7=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- .opencode/agents/instruction.md | 107 ++++-- .opencode/skills/SKILL.md | 48 ++- .opencode/skills/examples.md | 35 +- .opencode/skills/workflow/SKILL.md | 70 ---- .../workflow/bottleneck-analysis/SKILL.md | 106 ------ .../workflow/simulation-diagnosis/SKILL.md | 210 ----------- .../scripts/diagnose_simulation.py | 341 ------------------ .../source-service-area-analysis/SKILL.md | 113 ------ .../scripts/compute_service_areas.py | 281 --------------- .opencode/tools/fetch_result_ref.ts | 49 --- .opencode/tools/render_junctions.ts | 2 +- .opencode/tools/skill_manager.ts | 2 +- .opencode/tools/tjwater_cli.ts | 4 +- 14 files changed, 126 insertions(+), 1246 deletions(-) delete mode 100644 .opencode/skills/workflow/SKILL.md delete mode 100644 .opencode/skills/workflow/bottleneck-analysis/SKILL.md delete mode 100644 .opencode/skills/workflow/simulation-diagnosis/SKILL.md delete mode 100644 .opencode/skills/workflow/simulation-diagnosis/scripts/diagnose_simulation.py delete mode 100644 .opencode/skills/workflow/source-service-area-analysis/SKILL.md delete mode 100644 .opencode/skills/workflow/source-service-area-analysis/scripts/compute_service_areas.py delete mode 100644 .opencode/tools/fetch_result_ref.ts diff --git a/.gitignore b/.gitignore index 12b8fb5..fb94631 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ node_modules/ +__pycache__/ .opencode/node_modules/ .local.env .vscode docker-compose.yml data/ logs/ -cli/ -*.pyc \ No newline at end of file +cli/ \ No newline at end of file diff --git a/.opencode/agents/instruction.md b/.opencode/agents/instruction.md index ae0e838..7560413 100644 --- a/.opencode/agents/instruction.md +++ b/.opencode/agents/instruction.md @@ -4,31 +4,86 @@ mode: primary model: deepseek/deepseek-v4-pro temperature: 0.2 --- -您是运行在 opencode 上的默认 TJWater Agent,运用水力相关知识,使用简体中文回复用户的问题。 +您是 TJWater 供水管网分析 Agent,运用水力专业知识,使用简体中文,回复简洁准确。 -按照以下规则操作: +## 工作流生命周期 -1. 使用 `.opencode/skills/tjwater-skills` 作为 TJWater 技能树,仅在任务需要复杂多步工作流时才加载对应 workflow skill。对分析类问题,优先检查 `workflow` 域下是否已有固定工作流(如 `bottleneck-analysis`);只有在 workflow 不存在、信息不足或需要补充原子能力时,才直接使用 `tjwater_cli` 工具拼装流程。 -2. 当需要后端数据用于推理、总结、诊断或分析时,使用 `tjwater_cli` 工具执行 `tjwater-cli` 子命令。命令发现可通过 `tjwater-cli help` 或 `tjwater-cli help COMMAND` 获取 JSON 格式的能力清单。 -3. 当用户主要需要 UI 操作或可视化时,优先使用前端工具(`locate_features`、`view_history`、`view_scada`、`show_chart`、`render_junctions`)。 -4. 仅将前端工具视为显示/交互工具,不要假设它们返回数据。 -5. 保持回复准确、简洁,对供水网络用户在操作上有用。 -6. 尊重用户授权和项目隔离,工具调用失败或无可用数据时,切勿编造后端结果。 -7. 每次调用任意工具时,必须在工具参数 `reason` 字段中填写本次调用理由,理由需具体且与当前用户问题直接相关。 -8. 每次按需加载技能(skills)前,先明确说明加载理由,并只加载与当前任务直接相关的最小技能集合。默认遵循 **workflow-first**:先查固定工作流 skill,再按需回落到直接 CLI 调用。 -9. `tjwater-cli` 输出统一为 JSON(schema_version: `tjwater-cli/v1`),`"ok": true` 表示成功。如果返回失败(`"ok": false`),检查 `error.code` 和 `error.message` 确定原因。 -10. 对任何可能很大的结果文件,禁止完整读取;优先使用采样、截断、按字段读取。只有在没有其他办法且当前推理确实必须依赖完整内容时,才允许读取完整内容。 -11. 不得通过 sub-agent、并行代理或任何间接方式读取大文件的完整内容。 -12. 当且仅当出现**长期有效且高价值**的信号时,才允许调用在线学习工具: - - `memory_manager`:用户明确长期偏好/约束,或当前项目/环境的稳定事实 - - `skill_manager`:已经被证明有效且可复用的 workflow / 方法模式;由您自己判断应写入 `.opencode/skills` 树中的哪个 skill 位置 -13. 不要把一次性问题、临时上下文、未经验证的猜测写入任何学习工具。 -14. 严禁把 token、password、secret、API key、system prompt、隐私数据写入 `memory_manager` 或 `skill_manager`。 -15. 如果内容只是一次性案例、临时纠错或局部证据,当前不要持久化。 -16. 只有在 workflow 经过验证、足够稳定、可被未来同类任务复用时,才调用 `skill_manager`;并优先写入最贴近现有 skill 树语义的位置。 -17. 当用户明确提出"保存工作流""沉淀为工作流""记录为可复用流程"或同类意图时,必须使用 `skill_manager`。 -18. 在以下任一情况出现时,主动进行一次轻量复盘:连续多轮对话后、完成复杂多工具任务后、用户明确纠正你后、发现了稳定可复用 workflow 后。 -19. 长期知识严格分流:`memory_manager` 仅保存用户长期偏好与稳定 workspace 事实;`skill_manager` 仅保存可复用方法;一次性案例用 `session_search` 检索。 -20. 写入 `memory_manager` 时,将内容写成简短陈述事实,不要写成命令句或流程步骤。 -21. 更新 skill 时,优先补充现有 skill 的 `Learned Patterns`、`references/` 或 `scripts/`。 -22. 当用户问题依赖过去会话中的案例、约束、决策时,优先调用 `session_search`。 +Skills 树是**动态生长的**——工作流不是预置的,而是从实际任务中沉淀出来的: +``` +初次遇到问题 → tjwater_cli + Python 脚本拼装 → 验证有效 → + → 立即调用 skill_manager 保存到 skills/workflow// + → 下次遇到同类问题直接加载该 skill,按既定步骤执行 +``` + +## 任务执行决策 + +收到用户请求时,按以下顺序决策: + +1. **查已有工作流** — 检查 `skills/workflow/` 下是否存在匹配的 SKILL.md,有则加载并按步骤执行 +2. **历史参考** — 用 `session_search` 检索历史相似案例,避免重复试错 +3. **从零拼装** — 无匹配工作流时,自行组合 `tjwater_cli` 命令 + Python 脚本完成 +4. **完成后复盘** — 判断当前流程是否稳定、可复用,决定是否沉淀为 workflow + +## 工具选择 + +| 场景 | 工具 | +|------|------| +| 获取后端数据(数据源、推理、分析) | `tjwater_cli` | +| 发现可用命令 | `tjwater_cli(command="help")` | +| UI 操作 / 可视化 | `locate_features`、`view_scada`、`show_chart`、`render_junctions`、`view_history`、`apply_layer_style` | +| 持久化渲染数据 | `store_render_ref` → `render_junctions` | + +**前端工具仅做显示,不返回数据**,不要假设其返回内容。 + +## 执行约束 + +1. 每次工具调用必须在 `reason` 字段填写具体理由 +2. `tjwater-cli` 输出为 JSON(`schema_version: tjwater-cli/v1`),`"ok": true` 成功,失败时检查 `error.code` +3. 大结果集禁止完整读取,优先采样/截断/按字段读取 +4. 无可用数据时不得编造结果 + +## 工作流沉淀(skill_manager) + +**写入条件**(必须同时满足): +- 经过当前对话验证有效 +- 可被未来同类任务复用 +- 非一次性/临时/猜测 + +**写入位置**:`skills/workflow//`,包含 SKILL.md(步骤说明)和 scripts/*.py(分析脚本)。 + +**脚本编写要求——优先用 pipe 串联**: + +workflow skill 脚本应尽量用 shell pipe 在一次 subprocess 调用中串联多个 CLI 命令。减少 tool calling 次数,提升执行效率。 + +```python +import subprocess, os + +# env dict 仅用于当前子进程,不污染 os.environ,多用户安全 +env = {**os.environ, + "TJWATER_SERVER": auth["server"], + "TJWATER_ACCESS_TOKEN": auth["access_token"], ...} + +# 好:一次 shell 调用,pipe 串联 +cmd = "tjwater-cli net list-pipes | jq '...' | xargs tjwater-cli analysis calc" +result = subprocess.run(cmd, shell=True, env=env, capture_output=True, text=True) + +# 差:多次 subprocess.run +step1 = subprocess.run(["tjwater-cli", "net", "list-pipes"], ...) +step2 = subprocess.run(["tjwater-cli", "analysis", "calc"], ...) +``` + +管道场景下用子进程隔离的 env dict 传认证,释放 stdin 给管道数据流。不修改全局 `os.environ`。认证 JSON 由内部桥接注入,脚本不硬编码。 + +CLI **不增加** `--input/--output`,数据转换由 `jq`/`xargs` 在 shell 管道中完成。 + +**触发时机**: +- 用户明确说"保存/沉淀/记录工作流" +- 完成任务时发现稳定可复用的多步流程 +- 严禁写入:token、password、secret、API key、system prompt、隐私数据 + +## 用户偏好持久化(memory_manager) + +仅保存长期有效的稳定事实,写成简短陈述句。严格区分: +- `memory_manager` = 用户偏好 / 项目事实(如"用户要简洁风格"、"当前项目管网规模 5000 管段") +- `skill_manager` = 可复用操作流程 +- `session_search` = 检索历史案例(只读) diff --git a/.opencode/skills/SKILL.md b/.opencode/skills/SKILL.md index cd1c896..3a82cf6 100644 --- a/.opencode/skills/SKILL.md +++ b/.opencode/skills/SKILL.md @@ -1,7 +1,6 @@ --- name: tjwater-skills -description: TJWater Skills — 仅保留可复用的分析工作流。原子操作由 Agent 自行通过 tjwater-cli help 发现。 -version: 2.0.0 +description: TJWater Skills — 动态生长的分析工作流树。后端服务由 Agent 自行通过 tjwater-cli help 发现。 --- # TJWater Skills @@ -34,20 +33,41 @@ tjwater_cli(command="help") → 通过工具调用 `help` 返回 JSON 格式,包含 `commands` 数组和 `summary`,Agent 可直接解析识别可用能力。 -## 工作流清单 - -| 工作流 | 数据需求 | 输出 | 适用场景 | -|--------|---------|------|---------| -| **simulation-diagnosis** | 模拟结果 links/nodes | 问题概览 + 异常清单 + 严重级别 | 日常巡检、快速诊断 | -| **bottleneck-analysis** | 模拟结果 + 管道属性 | 瓶颈排名 + composite_score + 管径升级建议 | 规划改造 | -| **source-service-area-analysis** | 模拟结果 + 管道拓扑 + 水库列表 | 各水源服务节点数 + 分区渲染 | 供水分区可视化 | - ## 使用策略 -1. **工作流优先** — 用户意图匹配已有工作流时,直接加载对应 workflow skill。 -2. **原子优先** — 简单查询/单项分析直接调用 `tjwater_cli`,不加载 skill。 -3. **按需升级** — 浅层工作流不足时走升级路径(如 diagnosis → bottleneck)。 -4. **应急拼装** — 无匹配工作流时,Agent 自行组合 CLI 命令 + Python 脚本完成。 +Skills 树是动态生长的——没有预置的 workflow,所有工作流从实际任务中沉淀: + +1. **查已有** — 先检查 `skills/workflow/` 下是否有匹配的 workflow skill +2. **从零拼装** — 无匹配时,Agent 自行组合 `tjwater_cli` 命令 + Python 脚本完成 +3. **沉淀复用** — 任务完成后复盘,如果流程稳定可复用,用 `skill_manager` 保存到 `skills/workflow//`(含 SKILL.md + scripts/*.py) +4. **原子操作** — 简单查询直接调用 `tjwater_cli`,不走 skill + +## Workflow 脚本编写规范 + +**原则:尽量用 pipe 串联 CLI 调用,减少 tool calling 次数。** + +workflow skill 的 Python 脚本应在一次 `subprocess.run(shell=True)` 中用 shell pipe 串联多个 CLI 命令,而非多次 `subprocess.run` 逐次调用。中间数据转换由 `jq`/`xargs` 完成,CLI 不增加 `--input/--output`。 + +``` +subprocess 次数: 1(pipe 串联) < N(逐次调用) +tool calling 次数: 1 次 skill call < N 次 tjwater_cli 调用 +``` + +**环境变量认证**(管道场景,多用户安全): + +```python +# env dict 仅用于当前子进程,不污染 os.environ,多用户并发隔离 +env = {**os.environ} +env["TJWATER_SERVER"] = auth["server"] +env["TJWATER_ACCESS_TOKEN"] = auth["access_token"] +env["TJWATER_PROJECT_ID"] = auth["project_id"] +env["TJWATER_NETWORK"] = auth.get("network", "") + +cmd = "tjwater-cli cmd1 | jq '...' | xargs tjwater-cli cmd2" +result = subprocess.run(cmd, shell=True, env=env, capture_output=True, text=True) +``` + +单次 CLI 调用仍用 `--auth-stdin`(桥接端点场景)。管道场景用子进程 env dict,两个场景各司其职。认证 JSON 由内部桥接注入,脚本不硬编码 token/server/project。 ## 参考 diff --git a/.opencode/skills/examples.md b/.opencode/skills/examples.md index 394b613..6201ab8 100644 --- a/.opencode/skills/examples.md +++ b/.opencode/skills/examples.md @@ -13,19 +13,7 @@ opencode agent 调用 `tjwater_cli`: } ``` -## 示例 2:工作流 — 模拟诊断 - -用户消息:"跑一下水力计算看看管网有什么问题" - -典型链路: -1. agent 加载 workflow `simulation-diagnosis` -2. 执行 `tjwater_cli(command="simulation run --start-time ... --duration 30")` -3. 执行 `tjwater_cli(command="data timeseries realtime links --start-time ... --end-time ...")` -4. 执行 `tjwater_cli(command="data timeseries realtime nodes --start-time ... --end-time ...")` -5. 将结果写入 JSON,运行 `python scripts/diagnose_simulation.py result.json` -6. 解读脚本输出,组织 Markdown 报告 - -## 示例 3:前端工具 — 地图定位 +## 示例 2:前端工具 — 地图定位 用户消息:"帮我找到管道 P-001 和 P-002" @@ -38,7 +26,7 @@ opencode agent 直接调用前端工具 `locate_features`: } ``` -## 示例 4:对话内图表 +## 示例 3:对话内图表 用户消息:"展示节点 J-001 最近一天的压力变化曲线" @@ -47,7 +35,7 @@ opencode agent 直接调用前端工具 `locate_features`: 2. 处理返回的 pressure 数据为 x_data + series 格式 3. 调用 `show_chart` 渲染 ECharts 图表 -## 示例 5:前端工具 — SCADA 监测面板 +## 示例 4:前端工具 — SCADA 监测面板 用户消息:"我想看看 J-001 的监测数据" @@ -61,7 +49,7 @@ opencode agent 调用工具 `view_scada`: } ``` -## 示例 6:命令发现 +## 示例 5:命令发现 opencode agent 需要了解可用的 CLI 命令时: @@ -72,7 +60,7 @@ tjwater_cli(command="help analysis") → 获取 analysis 子命令详情 帮助输出 JSON 格式,含 `commands` 数组和 `summary`。 -## 示例 7:记住用户偏好 +## 示例 6:记住用户偏好 用户消息:"以后回答尽量简洁,先给结论再解释。" @@ -86,16 +74,3 @@ opencode agent 调用 `memory_manager`: } ``` -## 示例 8:沉淀可复用 workflow 模式 - -用户消息:"这套瓶颈分析流程之后可以复用。" - -opencode agent 调用 `skill_manager`: -```json -{ - "action": "append_pattern", - "reason": "本轮已验证一套稳定可复用的瓶颈分析 workflow", - "skill_path": "workflow/bottleneck-analysis", - "pattern": "当瓶颈分析依赖大体量属性数据和模拟结果时,先用 tjwater_cli 获取 links 结果,再逐管段查询属性,最后合并排序。" -} -``` diff --git a/.opencode/skills/workflow/SKILL.md b/.opencode/skills/workflow/SKILL.md deleted file mode 100644 index ec6925d..0000000 --- a/.opencode/skills/workflow/SKILL.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -name: tjwater-workflow -description: 分析类工作流技能。所有工作流遵循 "CLI 获取 → 本地分析 → 报告输出" 模式。 -version: 3.0.0 ---- - -# Workflow Domain Skill - -## 简介 -负责分析场景下的工作流组织与调用入口。前端工具仅用于渲染。 - -## 使用策略 - -1. **优先匹配固定工作流** — 用户问题属于已有覆盖场景时,直接走 workflow -2. **按需升级** — 浅层工作流满足不了时,沿升级路径进入深层工作流 -3. **缺少 workflow 时** — Agent 自行组合 CLI 命令 + Python 脚本 - ---- - -## 工作流决策树 - -``` -用户请求 - │ - ├─ 含"服务范围/供水分区/水源范围/分区渲染"? - │ └─ 是 → 走 source-service-area-analysis - │ - ├─ 含"改管径/瓶颈/扩容"? - │ └─ 是 → 直接走 bottleneck-analysis - │ - └─ 否 → 走 simulation-diagnosis(快速诊断) - │ - ├─ 全局异常?(断流/未收敛/系统性低压) - │ └─ 直接输出针对性诊断,结束 - │ - ├─ 少量告警、无紧急项? - │ └─ 输出报告,结束 - │ - └─ ≥5 条紧急超速 或 存在多维度交叉异常? - └─ 输出快速诊断报告 + 建议升级 bottleneck-analysis - + 传递 TOP 问题管段 ID 列表 -``` - ---- - -## 工作流清单 - -| 工作流 | 数据需求 | 典型步骤 | -|--------|---------|---------| -| **simulation-diagnosis** | `simulation run` + `data timeseries realtime links/nodes` | 1-2 次 CLI + 诊断脚本 | -| **bottleneck-analysis** | 模拟结果 + `network get-link-properties` 逐管道查询 | 多次 CLI + 合并分析脚本 | -| **source-service-area-analysis** | 模拟结果 + 管道拓扑 + 水库列表 | 多次 CLI + 图遍历脚本 | - ---- - -## 衔接规则 - -`simulation-diagnosis` → `bottleneck-analysis` 升级条件: - -| 信号 | 阈值 | -|------|------| -| 紧急超速管段数 | ≥ 5 条流速 > 1.5 m/s | -| 多维度交叉异常 | 同一管段同时满足高速 + 高水损 + 大流量 | -| 高水损地理聚集 | TOP 水损管段集中在同一区域 | - -## 子模块索引 - -- **source-service-area-analysis**: `./source-service-area-analysis/SKILL.md` -- **simulation-diagnosis**: `./simulation-diagnosis/SKILL.md` -- **bottleneck-analysis**: `./bottleneck-analysis/SKILL.md` diff --git a/.opencode/skills/workflow/bottleneck-analysis/SKILL.md b/.opencode/skills/workflow/bottleneck-analysis/SKILL.md deleted file mode 100644 index e9a5920..0000000 --- a/.opencode/skills/workflow/bottleneck-analysis/SKILL.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -name: tjwater-workflow-bottleneck-analysis -description: 水力瓶颈分析工作流。 -version: 2.0.0 ---- - -# bottleneck-analysis Workflow Skill - -## 简介 -结合管道属性与水力模拟结果,识别管网中超负荷、高流速、高水头损失的瓶颈管道,计算综合评分并给出分级改造建议。 - -## 前置依赖 - -### 依赖 1:管道属性数据 -``` -tjwater_cli: - command: "network get-link-properties --link P1" -``` -逐条查询所需管道 ID 的属性(id、diameter、length、roughness、node1、node2)。 -> 首批 CLI 仅支持按 ID 单项查询,需对目标管段逐个调用。 - -### 依赖 2:水力模拟结果 -``` -tjwater_cli: - command: "data timeseries realtime links --start-time TIME --end-time TIME" - timeout: 300 -``` -获取全部管段的 flow(LPS)、velocity(m/s)、headloss(m)、status 等。 - ---- - -## 工作流步骤 - -### 第 1 步:获取管道属性 -从 `simulation-diagnosis` 输出的 TOP 问题管段 ID 列表出发,逐条调用 `network get-link-properties --link ID` 获取管径/长度/粗糙度。 - -### 第 2 步:获取模拟结果 -调用 `data timeseries realtime links` 获取对应时间窗口的全部管道水力结果。 - -### 第 3 步:合并数据 -Python 脚本将管道属性 `id` 与模拟结果的 `link_id` 关联,构建合并数据集: - -| 字段 | 来源 | 说明 | -|------|------|------| -| id | 关联键 | 管道 ID | -| flow | 模拟 | 流量 (LPS) | -| velocity | 模拟 | 流速 (m/s) | -| headloss | 模拟 | 水头损失 (m) | -| diameter | 管道属性 | 管径 (mm) | -| length | 管道属性 | 长度 (m) | -| roughness | 管道属性 | 粗糙度系数 | -| node1 / node2 | 管道属性 | 起端/终端节点 | -| unit_headloss | 计算 | headloss / length (m/m) | -| capacity_ratio | 计算 | \|flow\| / (π×(d/2000)²×1000) | - -### 第 4 步:多维度瓶颈识别 - -| 维度 | 筛选条件 | 指示含义 | -|------|---------|---------| -| 高流速 | velocity > 1.2 m/s | 管径不足 | -| 主干管高流量 | diameter ≥ 300mm 且 velocity > 0.5 m/s | 传输瓶颈 | -| 高水头损失 | headloss > 5m 且 0.3 < velocity < 1.5 | 能耗瓶颈 | -| 高单位水头损失 | unit_headloss > 1.0 m/m | 严重局部瓶颈 | -| 超负荷 | capacity_ratio > 1.0 | 超过设计能力 | - -排除极短管道(length < 0.5m)。 - -### 第 5 步:综合评分 - -``` -composite_score = (velocity / max_velocity) × 0.4 - + (headloss / max_headloss) × 0.3 - + (capacity_ratio / max_capacity_ratio) × 0.3 -``` - -取 TOP 10~20 作为最严重瓶颈管道。 - -### 第 6 步:分级改造建议 - -| 级别 | 评分范围 | 行动 | -|------|---------|------| -| 🚨 紧急 | > 0.3 | 立即安排管径升级 | -| ⚡ 重点 | 0.15~0.3 | 纳入近期改造计划 | -| 📋 关注 | 0.05~0.15 | 持续监测 | - -建议管径公式: -``` -建议管径(mm) = 2 × 1000 × sqrt(|flow| / (π × target_velocity × 1000)) -``` -DN<300 目标流速 1.0 m/s,DN≥300 目标流速 1.2 m/s。 - -### 第 7 步:可视化 -- `show_chart`:流速分布柱状图 -- `locate_features`:地图定位 TOP 瓶颈管道 - ---- - -## 证据约束 -- 关键数据不完整时不得输出最终瓶颈结论 -- 改造建议必须区分"数据直接支持"和"工程经验推断" - -## Learned Patterns -- 先按"属性数据获取 → 模拟结果获取 → 本地关联 → 多指标筛选 → 分级建议"拆解工作流。 -- 关联前统一字段:`link_id`、`diameter(mm)`、`length(m)`、`flow(LPS)`、`pressure(KPA)`。 -- `unit_headloss`、`capacity_ratio` 等衍生指标应在过滤异常数据后再计算。 -- 常见坑点:短管导致单位水头损失虚高、节点链路映射缺失、模拟结果不完整误当全量结论。 diff --git a/.opencode/skills/workflow/simulation-diagnosis/SKILL.md b/.opencode/skills/workflow/simulation-diagnosis/SKILL.md deleted file mode 100644 index 188bb64..0000000 --- a/.opencode/skills/workflow/simulation-diagnosis/SKILL.md +++ /dev/null @@ -1,210 +0,0 @@ ---- -name: tjwater-workflow-simulation-diagnosis -description: 单步长水力计算与管网问题诊断工作流。 -version: 3.0.0 ---- - -# Simulation Diagnosis Workflow Skill - -## 简介 -`simulation run` → 查询模拟结果 → 快速综合诊断 → 结构化报告。覆盖超流速、低压区、高水损、死水管段、Closed 阀门等常见运行问题。 - -**仅依赖模拟结果**(无需管径/长度/粗糙度等属性),适用于快速巡检、初筛、事故后评估。如需结合管径做详细瓶颈评分与改造建议,走 `bottleneck-analysis` 工作流。 - ---- - -## 执行速览(Golden Path) - -"跑一下水力计算看看管网有什么问题" 的标准执行路径: - -``` -┌──────────────────────────────────────────────────────────────┐ -│ 1. tjwater_cli(command="simulation run --start-time ... │ -│ --duration 30") │ -│ ↓ 模拟结果写入服务端时序库 │ -│ 2. tjwater_cli(command="data timeseries realtime links │ -│ --start-time ... --end-time ...") │ -│ ↓ (写为本地 JSON) │ -│ 3. python diagnose_simulation.py result.json │ -│ ↓ (stdout 结构化报告) │ -│ 4. 解读脚本输出 → 按严重级别组织 Markdown 报告 │ -│ ↓ (可选) │ -│ 5. show_chart / locate_features / view_scada │ -└──────────────────────────────────────────────────────────────┘ -``` - -> **关键原则**:排序/筛选/聚合全部在 Python 脚本或本地完成。前端工具仅用于渲染。 - ---- - -## 适用场景 - -| 场景 | 说明 | -|------|------| -| "跑一次水力计算看看管网有什么问题" | 直接适用,走 Golden Path | -| 管道属性接口不可用或数据不全 | 仅依赖模拟结果,无需属性数据 | -| 快速巡检 / 定期健康检查 | 低开销,脚本一秒给出问题概览 | -| 爆管/事故后的快速评估 | 单步稳态模拟,快速定位异常区域 | -| 作为 bottleneck-analysis 的前置筛查 | 先定位问题区域,再深入分析 | - ---- - -## 阶段一:触发模拟运行 - -### CLI 调用 - -``` -tjwater_cli: - command: "simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30" -``` - -- `--start-time`:模拟起始绝对时间,必须显式带时区(UTC+8) -- `--duration`:模拟持续分钟数 -- 结果写入服务端时序库,不直接返回 - -### 查询模拟结果 - -``` -tjwater_cli: - command: "data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00" - timeout: 300 -``` - -``` -tjwater_cli: - command: "data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00" - timeout: 300 -``` - -### 返回数据结构 - -输出为 JSON,`data` 字段包含结果数组。 - -**links** — 管段水力状态: -| 字段 | 单位 | 说明 | -|------|------|------| -| link_id | — | 管段 ID | -| flow | LPS | 流量(正负表示流向) | -| velocity | m/s | 流速 | -| headloss | m | 水头损失 | -| status | — | Open / Closed / Critical | - -**nodes** — 节点水力状态: -| 字段 | 单位 | 说明 | -|------|------|------| -| node_id | — | 节点 ID | -| demand | LPS | 需水量 | -| head | m | 水头 | -| pressure | KPA | 压力 | - ---- - -## 阶段二:全局异常检查(优先!) - -**在进入局部分析之前**,必须检查以下全局信号——如果命中则直接输出针对性诊断: - -| 全局信号 | 判定条件 | 含义 | 处理 | -|---------|---------|------|------| -| **模拟断流** | 所有 link velocity ≈ 0 | 阀门全关 / 水源缺失 / 模型断连 | 检查模型配置,不做局部分析 | -| **模拟未收敛** | 所有 node pressure = 0 | 水力方程未收敛 | 检查 INP 设定,不做局部分析 | -| **系统性低压** | >70% 节点 pressure < 28 KPA | 水源水头不足 / 泵站扬程不够 | 输出"全局性低压"结论,排查水源/泵站 | -| **结果不完整** | links 或 nodes 结果为空 | 接口异常或模型损坏 | 报告数据不可用,提示重试 | - ---- - -## 阶段三:自动诊断 - -### 使用 `diagnose_simulation.py` - -脚本路径:`scripts/diagnose_simulation.py` - -```bash -# 基本用法 -python scripts/diagnose_simulation.py /tmp/opencode/sim_result.json - -# 自定义阈值 -python scripts/diagnose_simulation.py /tmp/opencode/sim_result.json \ - --velocity-warn 1.2 \ - --velocity-critical 2.0 \ - --pressure-warn 40 \ - --pressure-critical 28 \ - --headloss-top 30 \ - --flow-top 20 - -# 仅输出摘要 -python scripts/diagnose_simulation.py /tmp/opencode/sim_result.json --summary-only -``` - -脚本自动完成: -- 全局统计(管段/节点总数,流速/压力/流量/水损的 max/mean/分位数) -- 超流速管段 TOP 20 -- 高水损管段 TOP 30 -- 低压节点统计 -- 零流量管段、Closed 管段 -- 高流量主干管 TOP 20 - ---- - -## 阶段四:报告生成与可视化 - -### 报告结构 - -``` ---- -## 管网运行诊断报告 - -### 一、模拟概况 -- 时间窗口 / 管网规模 / 单位 - -### 二、主要发现(按严重程度排序) -🚨 紧急 / ⚡ 重点 / 📋 关注 -每项含:问题标题 + 受影响数 + TOP 示例(3-5条含数据) + 可能原因 + 建议 - -### 三、数据局限 -- 区分"数据直接支持的结论"和"工程经验的推测" -- 标注管径数据是否缺失及其对结论的影响 -``` - -### 可视化(可选) - -- `show_chart`:流速/压力分布柱状图 -- `locate_features`:地图定位 TOP 问题管段 -- `view_scada`:问题区域实时监测 -- `render_junctions` + `store_render_ref`:按压力区间着色 - ---- - -## 诊断指标参考阈值 - -| 指标 | 阈值 | 依据 | -|------|------|------| -| 流速安全上限 | 1.2 m/s | 行业管道冲刷防护上限 | -| 流速紧急上限 | 2.0 m/s | 严重超速临界值 | -| 最低供水压力 | 28 KPA | 国标 GB 50268 | -| 正常压力下限 | 40 KPA | 居民供水压力下限 | - -> 所有阈值为可调启发式,脚本支持参数覆写。 - ---- - -## 下游衔接:何时升级到 bottleneck-analysis - -| 诊断发现 | 行动 | -|---------|------| -| 系统性低压(>70% 节点 <28 KPA) | **不**进入 bottleneck-analysis;排查水源/泵站 | -| TOP 20 超速管段中 ≥5 条流速 >1.5 m/s | 升级到 bottleneck-analysis | -| 存在多维度交叉异常管段 | 升级分析 | -| 仅少量告警、无紧急项 | 结束,不升级 | - -**数据移交**:从 diagnosis 结果提取 TOP 问题管段 ID 列表,传入 bottleneck-analysis。 - ---- - -## Learned Patterns - -- [d1f826184d8709db1988b00a] **渐进式 Top-K 分析**:大结果集先算全局统计量定位异常范围,再对 Top-K 做明细检查。 -- [a19477bebe442e5d87bc78bc] **多准则交叉印证**:同时分析 velocity / headloss / flow / pressure 四个维度,紧急管段需满足多项异常条件。 -- [cf50cc317b9bfe254426482d] **API 数据降级**:管道属性不可用时不停流程,用 velocity / headloss 作为代理指标。 -- [1cd621a60d7481a1c072a54f] **系统性低压优先诊断**:全网 >70% 节点 pressure<28KPA 时定位为全局性问题。 -- [a7c30102cf5c2a5c0e8cc8dd] **诊断脚本标准化**:`diagnose_simulation.py` 是分析主力,支持自定义阈值。 -- [8b533e5bfc43c014ebcc90cf] **压力单位验证**:验证 pressure 字段实际单位(KPA vs m H₂O),阈值随单位调整。 diff --git a/.opencode/skills/workflow/simulation-diagnosis/scripts/diagnose_simulation.py b/.opencode/skills/workflow/simulation-diagnosis/scripts/diagnose_simulation.py deleted file mode 100644 index 6a825d1..0000000 --- a/.opencode/skills/workflow/simulation-diagnosis/scripts/diagnose_simulation.py +++ /dev/null @@ -1,341 +0,0 @@ -#!/usr/bin/env python3 -""" -diagnose_simulation.py — 水力模拟结果快速诊断脚本 - -用途:对 runprojectreturndict 返回的 link_results + node_results 做多维度分析。 -输出排序后的问题清单与统计摘要,供仿真诊断工作流使用。 - -用法: - python diagnose_simulation.py [options] - -输入 JSON 格式(runprojectreturndict 返回结构): - { - "link_results": [ - {"link_id": "...", "flow": float, "velocity": float, - "headloss": float, "status": "...", "friction": float}, - ... - ], - "node_results": [ - {"node_id": "...", "demand": float, "head": float, "pressure": float}, - ... - }, - ... // 其他字段(如 tanks, pumps, valves 等)可选 - } - -Options: - --velocity-warn FLOAT 超速预警阈值 (默认 1.2 m/s) - --velocity-critical FLOAT 超速严重阈值 (默认 2.0 m/s) - --pressure-warn FLOAT 低压预警阈值 (默认 40 KPA) - --pressure-critical FLOAT 低压严重阈值 (默认 28 KPA) - --headloss-top INT 高水损 TOP N (默认 30) - --flow-top INT 高流量主干管 TOP N (默认 20) - --output FILE 输出结果到 JSON 文件 (可选) - --summary-only 只输出统计摘要,不输出明细列表 - -输出: - stdout 打印结构化诊断报告,含各维度问题清单和全局统计。 -""" - -import json -import sys -import argparse -from collections import Counter - -# ── 默认阈值 ────────────────────────────────────────────── -DEFAULT_VELOCITY_WARN = 1.2 # m/s -DEFAULT_VELOCITY_CRITICAL = 2.0 # m/s -DEFAULT_PRESSURE_WARN = 40.0 # KPA -DEFAULT_PRESSURE_CRITICAL = 28.0 # KPA -DEFAULT_HEADLOSS_TOP = 30 -DEFAULT_FLOW_TOP = 20 - - -def load_input(path: str) -> dict: - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - - -def analyze_links(link_results: list, args): - """分析管段维度:超流速、高水损、零流量、Closed 状态。""" - issues = {} - - # ── 超流速管段 ── - over_velocity_warn = [] - over_velocity_critical = [] - zero_flow = [] - closed_links = [] - - for link in link_results: - v = link.get("velocity", 0) or 0 - q = abs(link.get("flow", 0) or 0) - status = (link.get("status") or "").strip().lower() - hl = link.get("headloss", 0) or 0 - - # 流速分级 - if v > args.velocity_critical: - over_velocity_critical.append(link) - elif v > args.velocity_warn: - over_velocity_warn.append(link) - - # 零流量(仅统计 Open 状态的) - if q < 0.001 and status == "open": - zero_flow.append(link) - - # Closed 管段 - if status != "open": - closed_links.append(link) - - # 按流速降序排序 - over_velocity_critical.sort(key=lambda x: x.get("velocity", 0), reverse=True) - over_velocity_warn.sort(key=lambda x: x.get("velocity", 0), reverse=True) - - issues["over_velocity_critical"] = { - "count": len(over_velocity_critical), - "threshold": f">{args.velocity_critical} m/s", - "items": over_velocity_critical[:20], - "top_velocity": over_velocity_critical[0]["velocity"] if over_velocity_critical else 0, - } - issues["over_velocity_warn"] = { - "count": len(over_velocity_warn), - "threshold": f">{args.velocity_warn} m/s", - "items": over_velocity_warn[:20], - } - - # ── 高水损管段(按绝对值降序) ── - high_headloss = sorted(link_results, key=lambda x: abs(x.get("headloss", 0) or 0), reverse=True) - issues["high_headloss"] = { - "count": len([x for x in link_results if abs(x.get("headloss", 0) or 0) > 0.5]), - "items": high_headloss[:args.headloss_top], - } - - # ── 零流量管段 ── - issues["zero_flow_open"] = { - "count": len(zero_flow), - } - - # ── Closed 管段 ── - issues["closed_links"] = { - "count": len(closed_links), - "items": closed_links[:20], - } - - # ── 高流量主干管 ── - high_flow = sorted(link_results, key=lambda x: abs(x.get("flow", 0) or 0), reverse=True) - issues["high_flow_trunk"] = { - "items": high_flow[:args.flow_top], - } - - return issues - - -def analyze_nodes(node_results: list, args): - """分析节点维度:低压、高压。""" - issues = {} - - pressure_critical = [] - pressure_warn = [] - high_pressure = [] - - for node in node_results: - p = node.get("pressure", 0) or 0 - if p < args.pressure_critical: - pressure_critical.append(node) - elif p < args.pressure_warn: - pressure_warn.append(node) - if p > 200: - high_pressure.append(node) - - pressure_critical.sort(key=lambda x: x.get("pressure", 0)) - pressure_warn.sort(key=lambda x: x.get("pressure", 0)) - - total = len(node_results) - - issues["pressure_critical"] = { - "count": len(pressure_critical), - "pct": round(len(pressure_critical) / total * 100, 1) if total else 0, - "threshold": f"<{args.pressure_critical} KPA", - "items": pressure_critical[:20], - } - issues["pressure_warn"] = { - "count": len(pressure_warn), - "pct": round(len(pressure_warn) / total * 100, 1) if total else 0, - "threshold": f"<{args.pressure_warn} KPA (>= {args.pressure_critical})", - "items": pressure_warn[:20], - } - issues["high_pressure"] = { - "count": len(high_pressure), - "items": high_pressure[:20], - } - - return issues, total - - -def global_stats(link_results, node_results): - """计算全局统计量。""" - velocities = [x.get("velocity", 0) or 0 for x in link_results] - flows = [abs(x.get("flow", 0) or 0) for x in link_results] - headlosses = [abs(x.get("headloss", 0) or 0) for x in link_results] - pressures = [x.get("pressure", 0) or 0 for x in node_results] - demands = [x.get("demand", 0) or 0 for x in node_results] - - # 状态分布 - status_counter = Counter((x.get("status") or "").strip().lower() for x in link_results) - - stats = { - "link_count": len(link_results), - "node_count": len(node_results), - "velocity": { - "max": round(max(velocities), 4), - "mean": round(sum(velocities) / len(velocities), 4) if velocities else 0, - "p95": sorted(velocities)[int(len(velocities) * 0.95)] if velocities else 0, - }, - "pressure": { - "max": round(max(pressures), 2), - "min": round(min(pressures), 2), - "mean": round(sum(pressures) / len(pressures), 2) if pressures else 0, - "p5": sorted(pressures)[int(len(pressures) * 0.05)] if pressures else 0, - }, - "flow": { - "max": round(max(flows), 4), - }, - "headloss": { - "max": round(max(headlosses), 4), - }, - "status_distribution": dict(status_counter.most_common()), - } - return stats - - -def format_report(stats, link_issues, node_issues, node_total, summary_only=False): - """输出格式化的诊断报告。""" - lines = [] - lines.append("=" * 60) - lines.append(" 水力模拟诊断报告") - lines.append("=" * 60) - lines.append("") - - # ── 全局统计 ── - lines.append("【一、全局统计】") - lines.append(f" 管段总数: {stats['link_count']}") - lines.append(f" 节点总数: {stats['node_count']}") - lines.append(f" 流速: 最大 {stats['velocity']['max']} m/s, " - f"平均 {stats['velocity']['mean']} m/s, P95 {stats['velocity']['p95']} m/s") - lines.append(f" 压力: 最大 {stats['pressure']['max']} KPA, " - f"最小 {stats['pressure']['min']} KPA, " - f"平均 {stats['pressure']['mean']} KPA, P5 {stats['pressure']['p5']} KPA") - lines.append(f" 最大流量: {stats['flow']['max']} LPS") - lines.append(f" 最大水损: {stats['headloss']['max']} m") - lines.append(f" 状态分布: {stats['status_distribution']}") - lines.append("") - - # ── 超流速 ── - crit_v = link_issues["over_velocity_critical"] - warn_v = link_issues["over_velocity_warn"] - lines.append("【二、超流速管段】") - lines.append(f" 🔴 严重超速 (> {crit_v['threshold']}): {crit_v['count']} 条, " - f"最高 {crit_v['top_velocity']} m/s") - if crit_v["count"] > 0 and not summary_only: - for item in crit_v["items"][:10]: - lines.append(f" - {item['link_id']}: {item['velocity']} m/s, " - f"flow={item.get('flow', 'N/A')} LPS, status={item.get('status', 'N/A')}") - lines.append(f" 🟡 流速预警 (> {warn_v['threshold']}): {warn_v['count']} 条") - lines.append("") - - # ── 高水损 ── - hl = link_issues["high_headloss"] - lines.append("【三、高水损管段 TOP】") - lines.append(f" 水损 > 0.5m 的管段: {hl['count']} 条") - if hl["items"] and not summary_only: - for item in hl["items"][:10]: - lines.append(f" - {item['link_id']}: headloss={item.get('headloss', 0)} m, " - f"velocity={item.get('velocity', 0)} m/s") - lines.append("") - - # ── 低压节点 ── - pc = node_issues["pressure_critical"] - pw = node_issues["pressure_warn"] - lines.append("【四、低压节点】") - lines.append(f" 🔴 严重低压 ({pc['threshold']}): {pc['count']} 个 " - f"({pc['pct']}%)") - if pc["count"] > 0 and not summary_only: - for item in pc["items"][:10]: - lines.append(f" - {item['node_id']}: {item['pressure']} KPA, " - f"demand={item.get('demand', 0)} LPS, head={item.get('head', 0)} m") - lines.append(f" 🟡 低压预警 ({pw['threshold']}): {pw['count']} 个 " - f"({pw['pct']}%)") - lines.append("") - - # ── 其他发现 ── - lines.append("【五、其他发现】") - zf = link_issues["zero_flow_open"] - cl = link_issues["closed_links"] - hp = node_issues["high_pressure"] - lines.append(f" - 零流量 (Open 状态但 flow≈0): {zf['count']} 条") - lines.append(f" - Closed 管段/阀门: {cl['count']} 条") - if hp["count"] > 0: - lines.append(f" - 超压节点 (> 200 KPA): {hp['count']} 个") - lines.append("") - - # ── 高流量主干 ── - hft = link_issues["high_flow_trunk"] - lines.append("【六、高流量主干管 TOP】") - if hft["items"] and not summary_only: - for item in hft["items"][:10]: - lines.append(f" - {item['link_id']}: |flow|={abs(item.get('flow', 0))} LPS, " - f"velocity={item.get('velocity', 0)} m/s") - lines.append("") - - lines.append("=" * 60) - return "\n".join(lines) - - -def main(): - parser = argparse.ArgumentParser(description="水力模拟结果快速诊断") - parser.add_argument("input", help="输入 JSON 文件路径 (runprojectreturndict 结果)") - parser.add_argument("--velocity-warn", type=float, default=DEFAULT_VELOCITY_WARN) - parser.add_argument("--velocity-critical", type=float, default=DEFAULT_VELOCITY_CRITICAL) - parser.add_argument("--pressure-warn", type=float, default=DEFAULT_PRESSURE_WARN) - parser.add_argument("--pressure-critical", type=float, default=DEFAULT_PRESSURE_CRITICAL) - parser.add_argument("--headloss-top", type=int, default=DEFAULT_HEADLOSS_TOP) - parser.add_argument("--flow-top", type=int, default=DEFAULT_FLOW_TOP) - parser.add_argument("--output", help="输出 JSON 结果到文件") - parser.add_argument("--summary-only", action="store_true", help="仅输出统计摘要") - args = parser.parse_args() - - data = load_input(args.input) - link_results = data.get("link_results", []) - node_results = data.get("node_results", []) - - if not link_results and not node_results: - print("错误: 输入数据中既无 link_results 也无 node_results", file=sys.stderr) - sys.exit(1) - - # 执行分析 - stats = global_stats(link_results, node_results) - link_issues = analyze_links(link_results, args) - node_issues, node_total = analyze_nodes(node_results, args) - - # 输出报告 - report = format_report(stats, link_issues, node_issues, node_total, args.summary_only) - print(report) - - # 输出 JSON(可选) - if args.output: - output_data = { - "stats": stats, - "link_issues": { - k: {kk: vv for kk, vv in v.items() if kk != "items"} - for k, v in link_issues.items() - }, - "node_issues": { - k: {kk: vv for kk, vv in v.items() if kk != "items"} - for k, v in node_issues.items() - }, - } - with open(args.output, "w", encoding="utf-8") as f: - json.dump(output_data, f, ensure_ascii=False, indent=2) - print(f"详细结果已输出到: {args.output}") - - -if __name__ == "__main__": - main() diff --git a/.opencode/skills/workflow/source-service-area-analysis/SKILL.md b/.opencode/skills/workflow/source-service-area-analysis/SKILL.md deleted file mode 100644 index 618b988..0000000 --- a/.opencode/skills/workflow/source-service-area-analysis/SKILL.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -name: tjwater-workflow-source-service-area-analysis -description: 水源服务范围分区分析工作流。 -version: 2.0.0 ---- - -# Source Service Area Analysis Workflow Skill - -## 简介 -基于水力模拟结果的流向数据构建有向图,通过下游遍历确定每个节点的供水水源归属,生成分区渲染数据并在地图上以不同颜色展示各水源的服务范围。 - -## 前置依赖 - -### 依赖 1:水库(水源)列表 -从模拟结果的 node_results 中识别 type=reservoir 的节点,或逐条查询已知水库节点。 - -### 依赖 2:管道拓扑数据 -``` -tjwater_cli: - command: "network get-link-properties --link P1" -``` -逐条获取管道 ID 的 node1(起端)、node2(终端),构建管网拓扑。 - -### 依赖 3:水力模拟结果 -``` -tjwater_cli: - command: "data timeseries realtime links --start-time TIME --end-time TIME" - timeout: 300 -``` -获取各管段的 `flow`(LPS) 和 `status`,确定水流方向。 -- `flow > 0` → node1 → node2 -- `flow < 0` → node2 → node1 -- `flow ≈ 0` 或 `status != "Open"` → 忽略 - ---- - -## 工作流步骤 - -### 第 1 步:获取三大数据源 -并行调用 CLI 命令获取模拟结果、管道拓扑和水库信息,写入本地临时 JSON 文件。 - -### 第 2 步:构建有向图并计算服务范围 - -```bash -python scripts/compute_service_areas.py \ - /tmp/opencode/sim_result.json \ - /tmp/opencode/pipes.json \ - /tmp/opencode/reservoirs.json \ - --output /tmp/opencode/service_area_result.json -``` - -脚本核心逻辑: -1. **构建无向拓扑图**:从管道属性提取 `(node1, node2)`,构建邻接表 -2. **确定流向**:从模拟结果读取 flow 和 status - - flow > 0 → node1 → node2 - - flow < 0 → node2 → node1 - - flow ≈ 0 或 status != "Open" → 忽略 -3. **获取水源集合**:从水库列表提取所有水库 ID 作为 source nodes -4. **多源 BFS 下游遍历**:从所有水源节点同时出发,首次访问归属策略 -5. **统计输出**:各水源服务节点数、占比、未分配节点数 -6. **生成渲染数据**:`node_area_map: { node_id: area_id }`,每个水源分配唯一颜色 - -### 第 3 步:结果验证 -- **服务覆盖率**:正常管网应 > 80% -- **水源孤立**:某水源服务节点数为 0,检查出口管道是否 Closed -- **大区未分配**:检查区域与水源之间管道是否 Closed - -### 第 4 步:前端可视化 -1. `store_render_ref` 将 `node_area_map` 迁移为受控 `render_ref` -2. `render_junctions` 地图着色 -3. `locate_features` 高亮水源位置 -4. `show_chart` 各水源服务节点数柱状图(可选) - -### 第 5 步:报告输出 - -``` ---- -## 水源服务范围分析报告 - -### 一、管网概况 -- 总节点数 / 总管段数 / 水源数 / 覆盖率 - -### 二、各水源服务范围 -| 水源 ID | 服务节点数 | 占比 | 颜色 | - -### 三、未分配节点分析 -- 数量、可能原因 - -### 四、假设与局限 -- 流向基于稳态模拟,不代表所有工况 -- 有向图忽略零流量管段 -``` - ---- - -## 颜色调色板 -脚本内置 20 色: -``` -#1f77b4, #ff7f0e, #2ca02c, #d62728, #9467bd, #8c564b, #e377c2, -#7f7f7f, #bcbd22, #17becf, #aec7e8, #ffbb78, #98df8a, #ff9896, -#c5b0d5, #c49c94, #f7b6d2, #c7c7c7, #dbdb8d, #9edae5 -``` - -## 证据约束 -- 计算脚本的输出是唯一数据真相源 -- 未完成数据回读时不得执行分析 -- 零流量管段视为断连,需在报告中说明 - -## Learned Patterns -- 核心数据流:模拟结果(流向) + 管道属性(拓扑) + 水库列表(起点) → 有向图 BFS → 分区渲染 -- 有向图遍历前务必过滤 Closed 和零流量管段 -- 渲染数据必须通过 `store_render_ref` 迁移再传给 `render_junctions` -- 多水源 BFS 首次访问归属策略隐含"距离最近水源"语义 diff --git a/.opencode/skills/workflow/source-service-area-analysis/scripts/compute_service_areas.py b/.opencode/skills/workflow/source-service-area-analysis/scripts/compute_service_areas.py deleted file mode 100644 index 201fafb..0000000 --- a/.opencode/skills/workflow/source-service-area-analysis/scripts/compute_service_areas.py +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env python3 -""" -compute_service_areas.py — 水源服务范围分析核心脚本 - -输入: - sim_result.json — runprojectreturndict 的水力模拟结果 - pipes.json — getallpipeproperties 的管道属性列表 - reservoirs.json — getallreservoirproperties 的水库属性列表 - -输出: - service_area_result.json — 包含 node_area_map 和统计信息 - -算法: - 1. 从 pipes 构建无向拓扑图 (node1 <-> node2) - 2. 从 sim link_results 确定流向,生成有向图 - 3. 从 reservoirs 获取所有水源节点 - 4. 多源 BFS 下游遍历,首次访问即归属性 - 5. 生成渲染数据与统计 - -用法: - python compute_service_areas.py sim_result.json pipes.json reservoirs.json \ - --output service_area_result.json --network tjwater -""" - -import argparse -import json -import os -import sys -from collections import deque - -# ── 高区分度 20 色调色板 ────────────────────────────────────────── -PALETTE = [ - "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", - "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf", - "#aec7e8", "#ffbb78", "#98df8a", "#ff9896", "#c5b0d5", - "#c49c94", "#f7b6d2", "#c7c7c7", "#dbdb8d", "#9edae5", -] - - -def load_json(path: str) -> dict | list: - """加载 JSON 文件,支持引用格式(含 result_ref 字段)或列表。""" - with open(path, "r", encoding="utf-8") as f: - data = json.load(f) - # 如果顶层是引用包装,展开到内部的列表 - if isinstance(data, dict) and "data" in data: - data = data["data"] - if isinstance(data, dict) and "items" in data: - data = data["items"] - return data - - -def build_undirected_graph(pipes: list) -> tuple[dict, dict]: - """ - 从管道列表构建无向拓扑图。 - 返回: - adj: { node_id: set(neighbor_node_ids) } - edge_map: { (node1, node2): pipe_id } 用于查找管道 ID - """ - adj = {} - edge_map = {} - for p in pipes: - pipe_id = str(p.get("id", p.get("pipe_id", p.get("name", "")))) - n1 = str(p.get("node1", "")) - n2 = str(p.get("node2", "")) - if not n1 or not n2: - continue - adj.setdefault(n1, set()).add(n2) - adj.setdefault(n2, set()).add(n1) - edge_map[(n1, n2)] = pipe_id - edge_map[(n2, n1)] = pipe_id - return adj, edge_map - - -def build_directed_graph(adj: dict, edge_map: dict, link_results: list) -> dict: - """ - 基于模拟流向构建有向图。 - link_results 中每条记录含 link_id, flow, status。 - 流向判定: - flow > 0 → node1 → node2 - flow < 0 → node2 → node1 - flow ≈ 0 或 status != "Open" → 忽略(视为断连) - - 注意: link_results 中没有 node1/node2,需要通过 link_id 反查 edge_map。 - 由于 edge_map 只有 (node1,node2) → pipe_id 的映射, - 这里需要先建立 pipe_id → (node1, node2) 的反向索引。 - """ - # 建立 pipe_id → (node1, node2) 反向索引 - # 注意:edge_map 含有 (n1,n2) 和 (n2,n1) 两个方向,只保留第一个以避免反向覆写 - pipe_to_nodes = {} - for (n1, n2), pid in edge_map.items(): - if pid not in pipe_to_nodes: - pipe_to_nodes[pid] = (n1, n2) - - digraph = {} - missing = 0 - closed_or_zero = 0 - - for link in link_results: - lid = str(link.get("link_id", link.get("id", ""))) - flow = link.get("flow", 0) - status = str(link.get("status", "Open")) - - # 忽略非 Open 管段和零流量管段(大小写不敏感) - if status.upper() != "OPEN": - closed_or_zero += 1 - continue - if abs(flow) < 1e-6: - closed_or_zero += 1 - continue - - # 查找管段对应的节点对 - pair = pipe_to_nodes.get(lid) - if pair is None: - missing += 1 - continue - - n1, n2 = pair - if flow > 0: - digraph.setdefault(n1, set()).add(n2) - else: - digraph.setdefault(n2, set()).add(n1) - - if missing > 0: - print(f"[WARN] {missing} 条模拟管段未在管道拓扑中找到对应节点对", file=sys.stderr) - if closed_or_zero > 0: - print(f"[INFO] 忽略 {closed_or_zero} 条 Closed/零流量管段", file=sys.stderr) - - return digraph - - -def bfs_from_sources(sources: list, digraph: dict) -> dict: - """ - 多水源 BFS 下游遍历。首次访问的节点归属到对应水源。 - 返回: { node_id: source_id } - """ - node_area = {} - queue = deque() - - # 初始化:所有水源入队,归属自身 - for src in sources: - node_area[src] = src - queue.append(src) - - while queue: - cur = queue.popleft() - cur_source = node_area[cur] - for nb in digraph.get(cur, set()): - if nb not in node_area: - node_area[nb] = cur_source - queue.append(nb) - - return node_area - - -def generate_render_data(node_area: dict, sources: list) -> dict: - """ - 生成 render_junctions 所需的渲染数据。 - 返回: - node_area_map: { node_id: area_id } - area_ids: 水源 ID 列表 - area_colors: { area_id: color_hex } - """ - # 为每个水源分配颜色 - area_colors = {} - for i, src in enumerate(sources): - area_colors[src] = PALETTE[i % len(PALETTE)] - - return { - "node_area_map": node_area, - "area_ids": sources, - "area_colors": area_colors, - } - - -def compute_stats(node_area: dict, sources: list, all_nodes: set) -> dict: - """计算各水源服务节点数和覆盖率统计。""" - source_counts = {} - for src in sources: - source_counts[src] = sum(1 for v in node_area.values() if v == src) - - total = len(all_nodes) - assigned = len(node_area) - unassigned = total - assigned - coverage = (assigned / total * 100) if total > 0 else 0 - - return { - "total_nodes": total, - "assigned_nodes": assigned, - "unassigned_nodes": unassigned, - "coverage_pct": round(coverage, 2), - "source_counts": source_counts, - } - - -def main(): - parser = argparse.ArgumentParser( - description="水源服务范围分析 — 基于水力模拟流向的多源 BFS 分区" - ) - parser.add_argument("sim_file", help="runprojectreturndict 模拟结果 JSON") - parser.add_argument("pipes_file", help="getallpipeproperties 管道属性 JSON") - parser.add_argument("reservoirs_file", help="getallreservoirproperties 水库属性 JSON") - parser.add_argument("--output", "-o", default="/tmp/opencode/service_area_result.json", - help="输出 JSON 文件路径") - parser.add_argument("--network", "-n", default="tjwater", - help="管网名称(仅用于报告标注)") - args = parser.parse_args() - - # ── 1. 加载数据 ── - print(f"[INFO] 加载模拟结果: {args.sim_file}") - sim_data = load_json(args.sim_file) - link_results = sim_data.get("link_results", []) - - print(f"[INFO] 加载管道属性: {args.pipes_file}") - pipes = load_json(args.pipes_file) - - print(f"[INFO] 加载水库列表: {args.reservoirs_file}") - reservoirs = load_json(args.reservoirs_file) - - # 提取水源 ID 列表 - sources = [] - if isinstance(reservoirs, list): - sources = [str(r.get("id", r.get("name", ""))) for r in reservoirs if r.get("id") or r.get("name")] - elif isinstance(reservoirs, dict): - # 可能是 { data: [...] } 格式 - for key in ["data", "items", "reservoirs"]: - if key in reservoirs and isinstance(reservoirs[key], list): - sources = [str(r.get("id", r.get("name", ""))) for r in reservoirs[key] if r.get("id") or r.get("name")] - break - if not sources: - # 也可能是 { "551656": {...}, ... } 格式 - sources = [str(k) for k in reservoirs if isinstance(reservoirs[k], dict)] - - if not sources: - print("[ERROR] 未能从水库数据中提取水源 ID 列表", file=sys.stderr) - sys.exit(1) - - print(f"[INFO] 水源数: {len(sources)}") - print(f"[INFO] 水源列表: {sources}") - - # ── 2. 构建无向图 ── - adj, edge_map = build_undirected_graph(pipes) - all_nodes = set(adj.keys()) - - # ── 3. 构建有向图 ── - digraph = build_directed_graph(adj, edge_map, link_results) - - # ── 4. BFS 下游遍历 ── - print("[INFO] 执行多源 BFS 下游遍历...") - node_area = bfs_from_sources(sources, digraph) - - # ── 5. 统计 ── - stats = compute_stats(node_area, sources, all_nodes) - print(f"[INFO] 总节点数: {stats['total_nodes']}") - print(f"[INFO] 已分配: {stats['assigned_nodes']} ({stats['coverage_pct']:.1f}%)") - print(f"[INFO] 未分配: {stats['unassigned_nodes']}") - - print("\n各水源服务节点数 (按数量降序):") - for src, count in sorted(stats["source_counts"].items(), key=lambda x: -x[1]): - pct = (count / stats["assigned_nodes"] * 100) if stats["assigned_nodes"] > 0 else 0 - print(f" {src}: {count} ({pct:.1f}%)") - - # ── 6. 生成渲染数据 ── - render = generate_render_data(node_area, sources) - - # ── 7. 输出 ── - output = { - "network": args.network, - "stats": stats, - "render": render, - "sources": sources, - } - - os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True) - with open(args.output, "w", encoding="utf-8") as f: - json.dump(output, f, ensure_ascii=False, indent=2) - - print(f"\n[OK] 结果已写入: {args.output}") - - -if __name__ == "__main__": - main() diff --git a/.opencode/tools/fetch_result_ref.ts b/.opencode/tools/fetch_result_ref.ts deleted file mode 100644 index 80d52b5..0000000 --- a/.opencode/tools/fetch_result_ref.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { ToolSessionContextStore } from "../../src/session/toolContextStore.js"; - -const internalBaseUrl = process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; -const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; -const toolContextStore = new ToolSessionContextStore(); -const initializePromise = toolContextStore.initialize(); - -export default tool({ - description: - "回读持久化 result_ref。适用于大结果只返回 preview 时,按需读取完整或截断后的数据。仅用于仍使用 referenced 模式的遗留接口。", - args: { - reason: tool.schema - .string() - .describe("Why the stored result needs to be read for the current user request."), - result_ref: tool.schema.string().describe("The result_ref handle."), - max_items: tool.schema - .number() - .int() - .positive() - .optional() - .describe("Optional maximum number of top-level items or fields to return."), - }, - async execute(args, context) { - await initializePromise; - const sessionContext = await toolContextStore.read(context.sessionID); - if (!sessionContext) { - throw new Error(`session context not found for ${context.sessionID}`); - } - const response = await fetch(`${internalBaseUrl}/internal/tools/fetch-result-ref`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-agent-internal-token": internalToken, - }, - body: JSON.stringify({ - sessionScopeKey: sessionContext.sessionScopeKey, - result_ref: args.result_ref, - max_items: args.max_items, - }), - }); - - const text = await response.text(); - if (!response.ok) { - throw new Error(text); - } - return text; - }, -}); diff --git a/.opencode/tools/render_junctions.ts b/.opencode/tools/render_junctions.ts index 1c71afc..b92792f 100644 --- a/.opencode/tools/render_junctions.ts +++ b/.opencode/tools/render_junctions.ts @@ -10,7 +10,7 @@ export default tool({ render_ref: tool.schema .string() .describe( - "渲染引用 ID。必须是持久化结果引用(res-...)。前端会按该引用读取完整 payload.data 并渲染,不需要先用 fetch_result_ref 提取完整数据。render_ref 对应的数据结构必须是 { node_area_map: { [junctionId]: areaId }, area_ids?: string[], area_colors?: { [areaId]: color } };node_area_map 必填,area_ids / area_colors 可选。", + "渲染引用 ID。必须是持久化结果引用(res-...)。前端会按该引用读取完整 payload.data 并渲染。render_ref 对应的数据结构必须是 { node_area_map: { [junctionId]: areaId }, area_ids?: string[], area_colors?: { [areaId]: color } };node_area_map 必填,area_ids / area_colors 可选。", ), }, async execute() { diff --git a/.opencode/tools/skill_manager.ts b/.opencode/tools/skill_manager.ts index da199b7..2f22b57 100644 --- a/.opencode/tools/skill_manager.ts +++ b/.opencode/tools/skill_manager.ts @@ -30,7 +30,7 @@ export default tool({ skill_path: tool.schema .string() .describe( - "Target skill directory path relative to .opencode/skills, for example analytics/simulation-analysis/leakage or platform/governance-observability/meta.", + "Target skill directory path relative to .opencode/skills.", ), pattern: tool.schema .string() diff --git a/.opencode/tools/tjwater_cli.ts b/.opencode/tools/tjwater_cli.ts index 7473267..9b315d0 100644 --- a/.opencode/tools/tjwater_cli.ts +++ b/.opencode/tools/tjwater_cli.ts @@ -8,7 +8,7 @@ const initializePromise = toolContextStore.initialize(); export default tool({ description: - "通过本地 Agent 桥接调用 tjwater-cli 命令访问 TJWater 后端。提供 CLI 子命令和参数。", + "通过本地 Agent 桥接调用 tjwater-cli 命令访问 TJWater 后端服务。提供 CLI 子命令和参数。", args: { reason: tool.schema .string() @@ -21,7 +21,7 @@ export default tool({ timeout: tool.schema .number() .optional() - .describe("超时秒数,默认 120。大结果集建议设 300+。"), + .describe("超时秒数,默认 60。大结果集建议设 120。"), }, async execute(args, context) { await initializePromise;