diff --git a/.dockerignore b/.dockerignore index 9b6cca3..88b74f6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,11 @@ .git -.gitignore node_modules .opencode/node_modules +.env +.env.* .local.env +data/ +logs/ dist .vscode *.log diff --git a/.gitea/workflows/package.yml b/.gitea/workflows/package.yml index a15666b..33be51c 100644 --- a/.gitea/workflows/package.yml +++ b/.gitea/workflows/package.yml @@ -14,7 +14,7 @@ jobs: dockerfile: Dockerfile build_context: . cache_image: gitea.waternetwork.cn/orgtjwater/tjwateragent:ci-cache - test_target: build + test_target: test deploy_service: agent deploy_host: 192.168.1.114 secrets: diff --git a/.gitignore b/.gitignore index a440ec2..a4d32b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ node_modules/ +__pycache__/ .opencode/node_modules/ +.opencode/skills/ .local.env .vscode docker-compose.yml data/ logs/ +AGENT_HARNESS_REPORT.md +HARNESS_INTRODUCTION.md diff --git a/.opencode/agents/instruction.md b/.opencode/agents/instruction.md index 69c1f56..5eb4daf 100644 --- a/.opencode/agents/instruction.md +++ b/.opencode/agents/instruction.md @@ -1,34 +1,99 @@ --- description: TJWater Agent,用于供水网络分析和操作员工作流 mode: primary -model: deepseek/deepseek-v4-pro +model: deepseek/deepseek-v4-flash temperature: 0.2 --- -您是运行在 opencode 上的默认 TJWater Agent,运用水力相关知识,使用简体中文回复用户的问题。 +你是 TJWater 供水管网分析 Agent,运用水力专业知识,回复用户时使用简体中文,内容要求简洁准确。 -按照以下规则操作: +## 工作流生命周期 -1. 使用 `.opencode/skills/tjwater-skills-root-index` 作为 TJWater 技能树,仅在任务需要该领域知识时加载特定技能。对分析类问题,优先检查 `workflow` 域下是否已有固定工作流(例如 `bottleneck-analysis`);只有在 workflow 不存在、信息不足或需要补充原子能力时,才继续查询其他 API / action skills。 -2. 当您需要后端数据用于推理、总结、诊断或分析时,优先使用 `dynamic_http_call`。 -3. 当用户主要需要 UI 操作或可视化时,优先使用前端工具(`locate_features`、`view_history`、`view_scada`、`show_chart`)。 -4. 仅将前端工具视为显示/交互工具,不要假设它们返回数据。 -5. 保持回复准确、简洁,对供水网络用户在操作上有用。 -6. 尊重用户授权和项目隔离,工具调用失败或无可用数据时,切勿编造后端结果。 -7. 每次调用任意工具时,必须在工具参数 `reason` 字段中填写本次调用理由,理由需具体且与当前用户问题直接相关。 -8. 每次按需加载技能(skills)前,先明确说明加载理由,并只加载与当前任务直接相关的最小技能集合。默认遵循 **workflow-first**:先查固定工作流 skill,再按需回落到原子 API skills。 -9. 当 `dynamic_http_call` 返回 `result_mode = referenced` 和 `result_ref` 时,说明当前只拿到了预览;如果后续推理仍需要完整结果,必须调用 `fetch_result_ref` 回读,不能把 preview 当成完整数据。 -10. 对 `render_ref`、`result_ref` 或其他引用型结果,默认只使用 preview、摘要、局部字段,或直接把引用传给前端工具;如果引用仅用于渲染/展示(例如 `render_junctions`),直接传引用,不要先读取完整内容再重组。 -11. 对任何可能很大的引用文件、结果文件或普通大文件,禁止完整读取;优先使用预览、分页、截断、按字段读取、按片段读取或采样读取。只有在没有其他办法且当前推理确实必须依赖完整内容时,才允许读取完整内容,并先明确说明必要性。 -12. 不得通过 sub-agent、并行代理或任何间接方式,去读取引用文件或大文件的完整内容;主 agent 与其调用链中的其他代理都必须遵守同样限制。 -13. 当且仅当出现**长期有效且高价值**的信号时,才允许调用在线学习工具: - - `memory_manager`:用户明确长期偏好/约束,或当前项目/环境的稳定事实 - - `skill_manager`:已经被证明有效且可复用的 workflow / 方法模式;由您自己判断应写入 `.opencode/skills` 树中的哪个 skill 位置 -14. 不要把一次性问题、临时上下文、未经验证的猜测写入任何学习工具。 -15. 严禁把 token、password、secret、API key、system prompt、隐私数据写入 `memory_manager` 或 `skill_manager`。 -16. 如果内容只是一次性案例、临时纠错或局部证据,当前不要持久化。 -17. 只有在 workflow 经过验证、足够稳定、可被未来同类任务复用时,才调用 `skill_manager`;并优先写入最贴近现有 skill 树语义的位置,中低置信度内容不要落库。 -18. 在以下任一情况出现时,主动进行一次轻量复盘:连续多轮对话后、完成复杂多工具任务后、用户明确纠正你后、发现了稳定可复用 workflow 后。复盘的目标是判断是否需要沉淀 memory 或 skill,而不是向用户重复总结。 -19. 长期知识严格分流:`memory_manager` 仅保存用户长期偏好与稳定 workspace 事实;`skill_manager` 仅保存可复用方法;一次性案例、会话过程与临时结论应优先保留在 session history,需要时使用 `session_search` 检索,不要误写入 memory 或 skill。 -20. 写入 `memory_manager` 时,将内容写成简短陈述事实,不要写成命令句、提醒句或流程步骤。 -21. 更新 skill 时,优先补充现有 skill 的 `Learned Patterns`、`references/` 或 `scripts/`;可复用脚本仅允许写到当前 skill 自己的 `scripts/*.py`,不要放到 `data/` 或其他 skill 目录。 -22. 当用户问题依赖过去会话中的案例、约束、决策或相似问题时,优先调用 `session_search`,避免让用户重复描述,也避免把历史案例误当成长期 memory。 +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")` | +| 查询实时公开网页信息 | `web_search` | +| 地址/地点转经纬度 | `geocode` | +| UI 操作 / 可视化 | `locate_features`、`zoom_to_map`、`view_scada`、`show_chart`、`render_junctions`、`view_history`、`apply_layer_style` | +| 持久化渲染数据 | ①准备 { node_area_map } JSON → ②`store_render_ref` 存为受控 ref → ③`render_junctions` 渲染到前端 | + +**前端工具仅做显示,不返回数据**,不要假设其返回内容。 + +## 执行约束 + +1. 每次工具调用必须在 `reason` 字段填写具体理由 +2. `tjwater-cli` 输出为 JSON(`schema_version: tjwater-cli/v1`),`"ok": true` 成功,失败时检查 `error.code` +3. 大结果集禁止完整读取,优先采样/截断/按字段读取 +4. 避免直接用 `Read` 或 `cat` 读取结果文件,尤其是大文件;优先用 `head`/`tail`/`rg` 截断查看,或用 Python 只向 stdout 输出精简 JSON,避免大文件冲击 stdin/stdout +5. 无可用数据时不得编造结果 +6. 禁止使用 `task` 子代理;当前前端无法观测和干预子代理的具体工作过程 + +## 工作流沉淀(skill_manager) + +**写入条件**(必须同时满足): +- 经过当前对话验证有效 +- 可被未来同类任务复用 +- 非一次性/临时/猜测 + +**写入位置**:`skills/workflow//`,包含 SKILL.md(步骤说明)、references/*.md(参考材料)和 scripts/*.py(分析脚本)。 + +**工具动作**:`write_skill / remove_skill` 维护主 SKILL.md;`append_pattern / remove_pattern` 维护 `## Learned Patterns`;`write_reference / remove_reference` 维护 references/*.md;`write_script / remove_script` 维护 scripts/*.py。`write_skill` 可创建或覆盖完整 SKILL.md。 + +目录入口也通过 `skill_manager` 维护:更新 `skills/workflow/SKILL.md` 时使用 `write_skill(skill_path="workflow", ...)`,更新根入口 `skills/SKILL.md` 时使用 `write_skill(skill_path="__root__", ...)`。 + +**脚本编写要求——优先用 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 管道中完成。 + +**触发时机**: +- 用户明确说"保存/沉淀/记录工作流" +- 任务完成且所有工具调用已结束、产生最终结果后,再判断当前流程是否稳定可复用 +- **禁止**在规划任务未完成、工具调用链中间(即仍有 pending 步骤时)触发沉淀 +- 严禁写入:token、password、secret、API key、system prompt、隐私数据 + +## 用户偏好持久化(memory_manager) + +仅保存长期有效的稳定事实,写成简短陈述句。严格区分: +- `memory_manager` = 用户偏好 / 项目事实(如"用户要简洁风格"、"当前项目管网规模 5000 管段") +- `skill_manager` = 可复用操作流程 +- `session_search` = 检索历史案例(只读) +- 修改 memory 前先 `list` 当前 scope 的已有内容,先通读,再决定 `add / replace / remove` diff --git a/.opencode/bun.lock b/.opencode/bun.lock index 1765292..fbdc461 100644 --- a/.opencode/bun.lock +++ b/.opencode/bun.lock @@ -4,7 +4,7 @@ "workspaces": { "": { "dependencies": { - "@opencode-ai/plugin": "1.14.41", + "@opencode-ai/plugin": "1.18.13", }, "devDependencies": { "@types/node": "^24.7.2", @@ -13,11 +13,23 @@ }, }, "packages": { - "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@opencode-ai/plugin": ["@opencode-ai/plugin@1.14.41", "", { "dependencies": { "@opencode-ai/sdk": "1.14.41", "effect": "4.0.0-beta.59", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.2.2", "@opentui/solid": ">=0.2.2" }, "optionalPeers": ["@opentui/core", "@opentui/solid"] }, "sha512-Q/QdDKSfHyYX+Xqd79o4XgyZKqF8h5qgqgfmOQbKVLhbduc9zMYdpV2yvWT6gaJPrpOftpka/kpr56PCqzetYQ=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], - "@opencode-ai/sdk": ["@opencode-ai/sdk@1.14.41", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-RYb2dCUv0TWIvBNnnO6ANbAPYri6rKuWizSoVFw/Pw+SCDj9ASHM5gAZ+jkskp8gYMfLLHe/Fpkun/9mr8m0IQ=="], + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], + + "@opencode-ai/plugin": ["@opencode-ai/plugin@1.18.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "1.18.13", "effect": "4.0.0-beta.83", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.4.5", "@opentui/keymap": ">=0.4.5", "@opentui/solid": ">=0.4.5" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-2H9YT80M1PYElpG+lmd/9kGqsNouiJIBCUhLblmgFwoSrB4wyahgkCS6NcFQR/AYXNH4I1Yd3lmQcVaEPu1qNg=="], + + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.13", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-JY9etiVcu1G/pZjaH2vjK/b8z54ujxaWCD1GziO4ADUhRM6m6zm2332bPGcxEfA6TwweiJfNlK6wVZQ0f/X4KQ=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -27,21 +39,23 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "effect": ["effect@4.0.0-beta.59", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA=="], + "effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="], "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], - "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], - "msgpackr": ["msgpackr@1.11.12", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg=="], + "msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="], - "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], + "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], @@ -61,7 +75,7 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "uuid": ["uuid@13.0.2", "", { "bin": "dist-node/bin/uuid" }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="], + "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], diff --git a/.opencode/package.json b/.opencode/package.json index 4fe4bd6..3db9d87 100644 --- a/.opencode/package.json +++ b/.opencode/package.json @@ -4,7 +4,7 @@ "typecheck": "tsc --noEmit -p tsconfig.json" }, "dependencies": { - "@opencode-ai/plugin": "1.14.41" + "@opencode-ai/plugin": "1.18.13" }, "devDependencies": { "@types/node": "^24.7.2", diff --git a/.opencode/skills/SKILL.md b/.opencode/skills/SKILL.md deleted file mode 100644 index 57f6b6f..0000000 --- a/.opencode/skills/SKILL.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: tjwater-skills-root-index -description: TJWater Skills 分层索引(Domain -> Scenario -> Action)。 -version: 1.2.0 ---- - -# TJWater Skills - -## 简介 - -按“领域 (Domain) -> 场景 (Scenario) -> 操作 (Action)”组织技能文档,逐层进入具体能力。 - -## 子模块索引 (渐进式引导) - -- **analytics**: 见 `./analytics/SKILL.md` -- **business**: 见 `./business/SKILL.md` -- **data**: 见 `./data/SKILL.md` -- **platform**: 见 `./platform/SKILL.md` -- **workflow**: 见 `./workflow/SKILL.md` - -## 加载策略 - -- 先按用户问题判断最可能的 Domain,再进入最小必要的 Scenario / Action。 -- 对分析、诊断、建议类问题,优先检查 `workflow/` 下是否已有固定工作流 skill;若存在,可先按 workflow 执行,再回补所需原子 skills。 -- 如果当前节点已经足以指导工具选择,不继续下钻到更多 skill。 -- 如果 workflow 已覆盖主要步骤,则不要先从大量 API skills 开始拼装流程;仅在 workflow 缺失、步骤不全或需要额外原子能力时,才继续下钻。 -- 优先更新已有 skill,而不是为一次性问题新增新的 skill 目录。 -- learned pattern 应写成可复用的方法或坑点,不应写成某次会话的流水账。 -- 某个 workflow 反复验证过的私有辅助脚本,应放在该 skill 目录下的 `scripts/*.py`,并随 skill 一起维护;不要写入 `data/`。 - -## 参考 - -- 示例:`./examples.md` -- 运行手册:`./runbook.md` diff --git a/.opencode/skills/analytics/SKILL.md b/.opencode/skills/analytics/SKILL.md deleted file mode 100644 index 69de451..0000000 --- a/.opencode/skills/analytics/SKILL.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: tjwater-domain-analytics -description: 负责仿真分析、SCADA 分析等计算类 API 能力。 -version: 3.0.0 ---- - -# Analytics Domain Skill - -## 简介 -负责仿真分析、SCADA 分析等计算类 API 能力。 - -## 子模块索引 (渐进式引导) -- **scada-operations**: 见 `./scada-operations/SKILL.md` -- **simulation-analysis**: 见 `./simulation-analysis/SKILL.md` - diff --git a/.opencode/skills/analytics/scada-operations/SKILL.md b/.opencode/skills/analytics/scada-operations/SKILL.md deleted file mode 100644 index 8ebfe91..0000000 --- a/.opencode/skills/analytics/scada-operations/SKILL.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: tjwater-scenario-analytics-scada-operations -description: 负责 SCADA 设备与数据操作。 -version: 3.0.0 ---- - -# scada-operations Scenario Skill - -## 简介 -负责 SCADA 设备与数据操作。 - -## 子模块索引 (渐进式引导) -- **scada**: 见 `./scada/SKILL.md` - diff --git a/.opencode/skills/analytics/scada-operations/scada/SKILL.md b/.opencode/skills/analytics/scada-operations/scada/SKILL.md deleted file mode 100644 index 1963207..0000000 --- a/.opencode/skills/analytics/scada-operations/scada/SKILL.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: tjwater-action-analytics-scada-operations-scada -description: analytics/scada-operations 下 scada 操作技能。 -version: 3.0.0 ---- - -# scada Action Skill - -## 简介 -负责 `analytics/scada-operations` 场景下 `scada` 的具体接口调用,分为**设备配置(静态元数据)**、**时序监测数据(TimescaleDB)**、**实时模拟数据**、**方案数据**和**复合查询**五类。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 - -### SCADA 设备配置(静态元数据) -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/getscadadeviceschema/` | 获取SCADA设备架构 | network (query) | - | -| GET | `/api/v1/getscadadevice/` | 获取SCADA设备 | network (query), id (query) | - | -| GET | `/api/v1/getallscadadeviceids/` | 获取所有SCADA设备ID | network (query) | - | -| GET | `/api/v1/getallscadadevices/` | 获取所有SCADA设备 | network (query) | - | -| POST | `/api/v1/addscadadevice/` | 添加SCADA设备 | network (query) | - | -| POST | `/api/v1/setscadadevice/` | 更新SCADA设备 | network (query) | - | -| POST | `/api/v1/deletescadadevice/` | 删除SCADA设备 | network (query) | - | -| POST | `/api/v1/cleanscadadevice/` | 清空SCADA设备表 | network (query) | - | -| GET | `/api/v1/getscadadevicedataschema/` | 获取SCADA设备数据架构 | network (query) | - | -| GET | `/api/v1/getscadadevicedata/` | 获取SCADA设备数据 | network (query), device_id (query) | - | -| POST | `/api/v1/addscadadevicedata/` | 添加SCADA设备数据 | network (query) | - | -| POST | `/api/v1/setscadadevicedata/` | 更新SCADA设备数据 | network (query) | - | -| POST | `/api/v1/deletescadadevicedata/` | 删除SCADA设备数据 | network (query) | - | -| POST | `/api/v1/cleanscadadevicedata/` | 清空SCADA设备数据表 | network (query) | - | -| GET | `/api/v1/getscadaelementschema/` | 获取SCADA元素架构 | network (query) | - | -| GET | `/api/v1/getscadaelement/` | 获取单个SCADA元素映射 | network (query), id (query) | - | -| GET | `/api/v1/getscadaelements/` | 获取所有SCADA元素映射 | network (query) | - | -| POST | `/api/v1/addscadaelement/` | 添加SCADA元素映射 | network (query) | - | -| POST | `/api/v1/setscadaelement/` | 更新SCADA元素映射 | network (query) | - | -| POST | `/api/v1/deletescadaelement/` | 删除SCADA元素映射 | network (query) | - | -| POST | `/api/v1/cleanscadaelement/` | 清空SCADA元素映射表 | network (query) | - | -| GET | `/api/v1/getscadainfoschema/` | 获取SCADA信息架构 | network (query) | - | -| GET | `/api/v1/getscadainfo/` | 获取SCADA信息 | network (query), id (query) | - | -| GET | `/api/v1/getallscadainfo/` | 获取所有SCADA信息 | network (query) | - | -| GET | `/api/v1/getscadaproperties/` | 获取SCADA属性 | network (query), scada (query) | - | -| GET | `/api/v1/getallscadaproperties/` | 获取所有SCADA属性 | network (query) | - | - -### SCADA 时序监测数据(TimescaleDB) -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/scada/batch` | 批量插入SCADA监测数据 | data (body) | - | -| GET | `/api/v1/scada/by-ids-time-range` | 按设备ID和时间范围查询SCADA数据 | start_time (query), end_time (query), device_ids (query) | - | -| GET | `/api/v1/scada/by-ids-field-time-range` | 按设备ID、字段和时间范围查询SCADA数据 | start_time (query), end_time (query), field (query), device_ids (query) | - | -| PATCH | `/api/v1/scada/{device_id}/field` | 更新SCADA设备字段 | device_id (path), time (query), field (query), value (query) | - | -| DELETE | `/api/v1/scada/by-id-time-range` | 按设备ID和时间范围删除SCADA数据 | device_id (query), start_time (query), end_time (query) | - | - -### 实时模拟数据(TimescaleDB - Realtime) -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/realtime/links/batch` | 批量插入实时管道数据 | data (body) | - | -| GET | `/api/v1/realtime/links` | 查询实时管道数据 | start_time (query), end_time (query) | link_ids (query) | -| DELETE | `/api/v1/realtime/links` | 删除实时管道数据 | start_time (query), end_time (query) | - | -| PATCH | `/api/v1/realtime/links/{link_id}/field` | 更新实时管道字段 | link_id (path), time (query), field (query), value (query) | - | -| POST | `/api/v1/realtime/nodes/batch` | 批量插入实时节点数据 | data (body) | - | -| GET | `/api/v1/realtime/nodes` | 查询实时节点数据 | start_time (query), end_time (query) | node_ids (query) | -| DELETE | `/api/v1/realtime/nodes` | 删除实时节点数据 | start_time (query), end_time (query) | - | -| POST | `/api/v1/realtime/simulation/store` | 存储实时模拟结果 | data (body) | - | -| GET | `/api/v1/realtime/query/by-time-property` | 按时间和属性查询实时数据 | time (query), property (query) | - | -| GET | `/api/v1/realtime/query/by-id-time` | 按ID和时间查询实时模拟数据 | element_id (query), time (query) | - | - -### 方案模拟数据(TimescaleDB - Scheme) -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/scheme/links/batch` | 批量插入方案管道数据 | data (body) | - | -| GET | `/api/v1/scheme/links` | 查询方案管道数据 | scheme_type (query) | link_ids (query) | -| GET | `/api/v1/scheme/links/{link_id}/field` | 查询方案管道字段数据 | link_id (path), scheme_type (query), field (query) | - | -| PATCH | `/api/v1/scheme/links/{link_id}/field` | 更新方案管道字段 | link_id (path), scheme_type (query), field (query), value (query) | - | -| DELETE | `/api/v1/scheme/links` | 删除方案管道数据 | scheme_type (query) | - | -| POST | `/api/v1/scheme/nodes/batch` | 批量插入方案节点数据 | data (body) | - | -| GET | `/api/v1/scheme/nodes/{node_id}/field` | 查询方案节点字段数据 | node_id (path), scheme_type (query), field (query) | - | -| PATCH | `/api/v1/scheme/nodes/{node_id}/field` | 更新方案节点字段 | node_id (path), scheme_type (query), field (query), value (query) | - | -| DELETE | `/api/v1/scheme/nodes` | 删除方案节点数据 | scheme_type (query) | - | -| POST | `/api/v1/scheme/simulation/store` | 存储方案模拟结果 | scheme_type (query), data (body) | - | -| GET | `/api/v1/scheme/query/by-id-time` | 按ID和时间查询方案模拟数据 | element_id (query), scheme_type (query), time (query) | - | - -### 复合查询(TimescaleDB - Composite) -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/composite/scada-simulation` | 获取SCADA关联的模拟数据 | network (query), start_time (query) | end_time (query) | -| GET | `/api/v1/composite/element-simulation` | 获取管网元素的模拟数据 | network (query), element_id (query), start_time (query) | end_time (query) | -| GET | `/api/v1/composite/element-scada` | 获取管网元素关联的SCADA监测数据 | element_id (query), start_time (query) | end_time (query) | -| POST | `/api/v1/composite/clean-scada` | 清洗SCADA监测数据 | data (body) | - | -| GET | `/api/v1/composite/pipeline-health-prediction` | 预测管道健康状况 | network (query), time (query) | - | - -- 覆盖方法:`DELETE, GET, PATCH, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /scada/by-ids-time-range` | 查询多个设备在指定时间范围内的所有监测字段数据,device_ids 为逗号分隔的ID字符串 | -| `GET /scada/by-ids-field-time-range` | 查询多个设备在指定时间范围内的特定字段数据(如只查压力或只查流量) | -| `POST /realtime/simulation/store` | 将水力模拟结果以实时数据形式存入TimescaleDB,供前端实时展示 | -| `GET /realtime/query/by-time-property` | 按特定时间点和属性名查询管网实时模拟结果 | -| `GET /composite/scada-simulation` | 同时返回指定管网的SCADA监测数据和对应的水力模拟数据,便于对比分析 | -| `GET /composite/element-scada` | 查询特定管网元素(管道或节点)关联的SCADA监测时序数据 | -| `GET /composite/pipeline-health-prediction` | 基于历史SCADA数据和模型预测管道健康状态 | -| `POST /composite/clean-scada` | 对指定设备的SCADA原始数据进行清洗处理(去异常值等),支持传 'all' 清洗所有设备 | diff --git a/.opencode/skills/analytics/simulation-analysis/SKILL.md b/.opencode/skills/analytics/simulation-analysis/SKILL.md deleted file mode 100644 index 2eacfc1..0000000 --- a/.opencode/skills/analytics/simulation-analysis/SKILL.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: tjwater-scenario-analytics-simulation-analysis -description: 负责仿真、风险、漏损与爆管分析。 -version: 3.0.0 ---- - -# simulation-analysis Scenario Skill - -## 简介 -负责仿真、风险、漏损与爆管分析。 - -## 子模块索引 (渐进式引导) -- **burst_detection**: 见 `./burst_detection/SKILL.md` -- **burst_location**: 见 `./burst_location/SKILL.md` -- **leakage**: 见 `./leakage/SKILL.md` -- **risk**: 见 `./risk/SKILL.md` -- **simulation**: 见 `./simulation/SKILL.md` - diff --git a/.opencode/skills/analytics/simulation-analysis/burst-detection/SKILL.md b/.opencode/skills/analytics/simulation-analysis/burst-detection/SKILL.md deleted file mode 100644 index 2c3ebb8..0000000 --- a/.opencode/skills/analytics/simulation-analysis/burst-detection/SKILL.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: tjwater-action-analytics-simulation-analysis-burst-detection -description: analytics/simulation-analysis 下 burst-detection 操作技能。 -version: 3.0.0 ---- - -# burst-detection Action Skill - -## 简介 -负责 `analytics/simulation-analysis` 场景下 `burst-detection` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/burst-detection/detect/` | 执行爆管检测 | data (body) | - | -| GET | `/api/v1/burst-detection/schemes/` | 查询爆管检测方案列表 | network (query) | query_date (query) | -| GET | `/api/v1/burst-detection/schemes/{scheme_name}` | 获取爆管检测方案详情 | network (query), scheme_name (path) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `POST /detect/` | 基于压力观测数据执行爆管检测分析。使用异常检测算法(隔离森林 IsolationForest)识别压力时间序列中的异常,判定为潜在爆管事件。请求体支持列式字典、逐时刻对象数组、二维数组三种格式的压力数据,可指定数据来源(monitoring 监测 / simulation 模拟)。 | -| `GET /schemes/` | 获取指定管网的所有爆管检测方案列表,可通过 query_date 按日期筛选。 | -| `GET /schemes/{scheme_name}` | 获取指定名称的爆管检测方案详细配置信息,包含传感器节点、算法参数等。 | - -## 请求体关键字段(POST /detect/) - -| 字段 | 类型 | 说明 | -|---|---|---| -| `network` | str | 管网名称(数据库名) | -| `observed_pressure_data` | dict/list/null | 压力观测数据,支持列式字典 `{sensor_id: [values]}` 或逐行数组 | -| `points_per_day` | int | 每天数据点数,默认1440 | -| `mu` | int | 异常值检测参数,默认100 | -| `iforest_params` | dict/null | 隔离森林算法参数,可选 | -| `scada_start` / `scada_end` | datetime/null | 从SCADA数据库查询的时间范围 | -| `sensor_nodes` | list/null | 指定传感器节点,null为全部 | -| `data_source` | str | 数据来源:`monitoring`(监测)或 `simulation`(模拟),默认monitoring | diff --git a/.opencode/skills/analytics/simulation-analysis/burst-location/SKILL.md b/.opencode/skills/analytics/simulation-analysis/burst-location/SKILL.md deleted file mode 100644 index b37650b..0000000 --- a/.opencode/skills/analytics/simulation-analysis/burst-location/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: tjwater-action-analytics-simulation-analysis-burst-location -description: analytics/simulation-analysis 下 burst-location 操作技能。 -version: 3.0.0 ---- - -# burst-location Action Skill - -## 简介 -负责 `analytics/simulation-analysis` 场景下 `burst-location` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/burst-location/locate/` | 执行爆管定位 | data (body) | - | -| GET | `/api/v1/burst-location/schemes/` | 查询爆管定位方案列表 | network (query) | query_date (query) | -| GET | `/api/v1/burst-location/schemes/{scheme_name}` | 获取爆管定位方案详情 | network (query), scheme_name (path) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `POST /locate/` | 基于压力和流量SCADA数据定位管网中的爆管位置。通过对比爆管时与正常状态下的压力/流量差异,计算最可能的爆管节点。 | -| `GET /schemes/` | 获取指定管网的所有爆管定位方案列表,可通过 query_date 按日期筛选。 | -| `GET /schemes/{scheme_name}` | 获取指定名称的爆管定位方案详细配置,包含传感器布置、阈值参数等。 | - -## 请求体关键字段(POST /locate/) - -| 字段 | 类型 | 说明 | -|---|---|---| -| `network` | str | 管网名称(数据库名) | -| `data_source` | str | 数据来源:`monitoring`(监测)或 `simulation`(模拟),默认monitoring | -| `pressure_scada_ids` | list/null | 压力SCADA传感器ID列表 | -| `burst_pressure` | dict/list/null | 爆管时的压力数据 | -| `normal_pressure` | dict/list/null | 正常时的压力数据 | -| `burst_leakage` | float | 爆管时的漏水量(必填) | -| `flow_scada_ids` | list/null | 流量SCADA传感器ID列表 | -| `burst_flow` / `normal_flow` | dict/list/null | 爆管/正常时的流量数据 | -| `min_dpressure` | float | 最小压力差(bar),默认2.0 | -| `basic_pressure` | float | 基准压力(bar),默认10.0 | -| `scada_burst_start` / `scada_burst_end` | datetime/null | 从SCADA数据库查询的爆管时间范围 | -| `use_scada_flow` | bool | 是否使用SCADA流量数据,默认false | diff --git a/.opencode/skills/analytics/simulation-analysis/leakage/SKILL.md b/.opencode/skills/analytics/simulation-analysis/leakage/SKILL.md deleted file mode 100644 index b77b513..0000000 --- a/.opencode/skills/analytics/simulation-analysis/leakage/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: tjwater-action-analytics-simulation-analysis-leakage -description: analytics/simulation-analysis 下 leakage 操作技能。 -version: 3.0.0 ---- - -# leakage Action Skill - -## 简介 -负责 `analytics/simulation-analysis` 场景下 `leakage` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/leakage/identify/` | 执行漏损识别 | data (body) | - | -| GET | `/api/v1/leakage/schemes/` | 查询漏损识别方案列表 | network (query) | query_date (query) | -| GET | `/api/v1/leakage/schemes/{scheme_name}` | 获取漏损识别方案详情 | network (query), scheme_name (path) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `POST /identify/` | 基于压力观测数据和遗传算法识别管网中的漏损位置和大小。通过对比模型计算与实测压力数据,迭代优化找到最匹配的漏损节点和漏水量。 | -| `GET /schemes/` | 获取指定管网的所有漏损识别方案列表,可通过 query_date 按日期筛选。 | -| `GET /schemes/{scheme_name}` | 获取指定名称的漏损识别方案详细配置,包含传感器节点、算法参数等。 | - -## 请求体关键字段(POST /identify/) - -| 字段 | 类型 | 说明 | -|---|---|---| -| `network` | str | 管网名称(数据库名) | -| `observed_pressure_data` | str/dict/list/null | 观测压力数据 | -| `start_time` | float | 起始时间(小时),默认0 | -| `duration` | float | 持续时间(小时),默认24 | -| `timestep` | float | 时间步长(分钟),默认5 | -| `q_sum` | float | 总流量(m³/s),默认0.2 | -| `pop_size` | int | 遗传算法种群大小,默认50 | -| `max_gen` | int | 遗传算法最大代数,默认100 | -| `n_workers` | int | 并行工作线程数,默认CPU数-1(最大4) | -| `scada_start` / `scada_end` | datetime/null | 从SCADA数据库查询的时间范围 | -| `sensor_nodes` | list/null | 传感器节点列表,null为全部 | diff --git a/.opencode/skills/analytics/simulation-analysis/risk/SKILL.md b/.opencode/skills/analytics/simulation-analysis/risk/SKILL.md deleted file mode 100644 index 2fcb4e4..0000000 --- a/.opencode/skills/analytics/simulation-analysis/risk/SKILL.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: tjwater-action-analytics-simulation-analysis-risk -description: analytics/simulation-analysis 下 risk 操作技能。 -version: 3.0.0 ---- - -# risk Action Skill - -## 简介 -负责 `analytics/simulation-analysis` 场景下 `risk` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/getpiperiskprobabilitynow/` | 获取管道当前风险概率 | network (query), pipe_id (query) | - | -| GET | `/api/v1/getpiperiskprobability/` | 获取管道风险概率历史 | network (query), pipe_id (query) | - | -| GET | `/api/v1/getpipesriskprobability/` | 批量获取多条管道风险概率 | network (query), pipe_ids (query) | - | -| GET | `/api/v1/getnetworkpiperiskprobabilitynow/` | 获取整个网络的管道风险概率 | network (query) | - | -| GET | `/api/v1/getpiperiskprobabilitygeometries/` | 获取管道风险几何信息 | network (query) | - | - -- 覆盖方法:`GET` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getpiperiskprobabilitynow/` | 查询指定管道在当前时刻的风险概率值 | -| `GET /getpiperiskprobability/` | 查询指定管道的历史风险概率时间序列数据 | -| `GET /getpipesriskprobability/` | 批量查询多条管道的风险概率,pipe_ids为逗号分隔的ID字符串(如 `pipe1,pipe2,pipe3`) | -| `GET /getnetworkpiperiskprobabilitynow/` | 查询整个管网中所有管道的当前风险概率,返回列表 | -| `GET /getpiperiskprobabilitygeometries/` | 查询管网中管道的地理位置和风险相关几何数据,适合地图可视化 | diff --git a/.opencode/skills/analytics/simulation-analysis/simulation/SKILL.md b/.opencode/skills/analytics/simulation-analysis/simulation/SKILL.md deleted file mode 100644 index d036539..0000000 --- a/.opencode/skills/analytics/simulation-analysis/simulation/SKILL.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -name: tjwater-action-analytics-simulation-analysis-simulation -description: analytics/simulation-analysis 下 simulation 操作技能。 -version: 3.0.0 ---- - -# simulation Action Skill - -## 简介 -负责 `analytics/simulation-analysis` 场景下 `simulation` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/runproject/` | 运行项目模拟 | network (query) | - | -| GET | `/api/v1/runprojectreturndict/` | 运行项目模拟(返回字典) | network (query) | - | -| GET | `/api/v1/runinp/` | 运行INP文件 | network (query) | - | -| GET | `/api/v1/dumpoutput/` | 导出模拟输出 | output (query) | - | -| GET | `/api/v1/burstanalysis/` | 爆管分析(基础) | network (query), pipe_id (query), start_time (query), end_time (query), burst_flow (query) | - | -| GET | `/api/v1/burst_analysis/` | 爆管分析(高级) | network (query), modify_pattern_start_time (query), burst_ID (query), burst_size (query), modify_total_duration (query), scheme_name (query) | - | -| GET | `/api/v1/valvecloseanalysis/` | 阀门关闭分析(基础) | network (query), valve_id (query), start_time (query), end_time (query) | - | -| GET | `/api/v1/valve_close_analysis/` | 阀门关闭分析(高级) | network (query), start_time (query), valves (query) | duration (query) | -| GET | `/api/v1/valve_isolation_analysis/` | 阀门隔离分析 | network (query), accident_element (query) | disabled_valves (query) | -| GET | `/api/v1/flushinganalysis/` | 冲洗分析(基础) | network (query), pipe_id (query), start_time (query), duration (query), flow (query) | - | -| GET | `/api/v1/flushing_analysis/` | 冲洗分析(高级) | network (query), start_time (query), valves (query), valves_k (query), drainage_node_ID (query) | flush_flow (query), duration (query), scheme_name (query) | -| GET | `/api/v1/contaminant_simulation/` | 污染物模拟 | network (query), start_time (query), source (query), concentration (query), duration (query) | scheme_name (query), pattern (query) | -| GET | `/api/v1/ageanalysis/` | 水龄分析(基础) | network (query) | - | -| GET | `/api/v1/age_analysis/` | 水龄分析(高级) | network (query), start_time (query), end_time (query), duration (query) | - | -| GET | `/api/v1/pressureregulation/` | 压力调节(基础) | network (query), target_node (query), target_pressure (query) | - | -| POST | `/api/v1/pressure_regulation/` | 压力调节(高级) | data (body) | - | -| GET | `/api/v1/projectmanagement/` | 项目管理(基础) | network (query) | - | -| POST | `/api/v1/project_management/` | 项目管理(高级) | data (body) | - | -| POST | `/api/v1/scheduling_analysis/` | 排程分析 | data (body) | - | -| POST | `/api/v1/daily_scheduling_analysis/` | 日排程分析 | data (body) | - | -| POST | `/api/v1/network_project/` | 导入网络项目 | file (file) | - | -| GET | `/api/v1/networkupdate/` | 管网更新(基础) | network (query) | - | -| POST | `/api/v1/network_update/` | 管网更新(高级) | file (file) | - | -| POST | `/api/v1/pump_failure/` | 泵故障管理 | data (body) | - | -| GET | `/api/v1/pressuresensorplacementsensitivity/` | 压力传感器放置-灵敏度分析(基础) | name (query), scheme_name (query), sensor_number (query), min_diameter (query), username (query) | - | -| POST | `/api/v1/pressure_sensor_placement_sensitivity/` | 压力传感器放置-灵敏度分析(高级) | data (body) | - | -| GET | `/api/v1/pressuresensorplacementkmeans/` | 压力传感器放置-KMeans聚类分析(基础) | name (query), scheme_name (query), sensor_number (query), min_diameter (query), username (query) | - | -| POST | `/api/v1/pressure_sensor_placement_kmeans/` | 压力传感器放置-KMeans聚类分析(高级) | data (body) | - | -| POST | `/api/v1/sensorplacementscheme/create` | 传感器放置方案创建 | network (query), scheme_name (query), sensor_type (query), method (query), sensor_count (query), user_name (query) | min_diameter (query) | -| POST | `/api/v1/runsimulationmanuallybydate/` | 手动运行日期指定模拟 | data (body) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /runproject/` | 运行标准水力模拟,返回纯文本格式的模拟报告 | -| `GET /runprojectreturndict/` | 运行标准水力模拟,返回JSON字典(含节点/管段结果数据),适合程序处理;结果可达30MB+ | -| `GET /runinp/` | 运行指定INP文件(文件放在inp文件夹中,参数为不含扩展名的文件名)进行水力模拟 | -| `GET /dumpoutput/` | 导出指定绝对路径的模拟输出文件内容 | -| `GET /burstanalysis/` | 基础爆管分析:对指定管道指定时间范围内的爆管事件进行分析,评估对压力/流量的影响 | -| `GET /burst_analysis/` | 高级爆管分析:支持在指定时间点修改泵控制模式和阀门开度,分析干预措施对爆管影响的作用;支持固定泵和变速泵独立控制 | -| `GET /valve_close_analysis/` | 高级阀门关闭分析:支持同时关闭多个阀门,指定持续时间,返回纯文本格式结果 | -| `GET /valve_isolation_analysis/` | 阀门隔离分析:分析突发事件时通过关闭指定阀门进行隔离,确定必须关闭阀门、可选关闭阀门及隔离可行性 | -| `GET /flushing_analysis/` | 高级冲洗分析:支持同时开启多个阀门冲洗,指定排污节点,设置固定冲洗流量,返回纯文本结果 | -| `GET /contaminant_simulation/` | 污染物模拟:评估污染源对管网的影响范围和浓度分布,支持指定污染位置、浓度和扩散模式 | -| `GET /age_analysis/` | 高级水龄分析:在指定时间点分析水体停留时间,支持自定义模拟持续时间,返回纯文本结果 | -| `POST /pressure_regulation/` | 高级压力调节:通过JSON体提供详细控制参数(固定泵/变速泵独立控制、水箱初始水位等)进行压力优化 | -| `POST /project_management/` | 高级项目管理:通过JSON体提供详细参数(泵控制策略、水箱水位、区域需水量控制)进行管网管理 | -| `POST /scheduling_analysis/` | 排程分析:优化泵运行时间和出水流量,平衡水厂出水、水箱进出水,满足用户需求 | -| `POST /daily_scheduling_analysis/` | 日排程分析:优化水库、水厂、水箱和用户需求协调,制定合理的每日排程方案 | -| `POST /pump_failure/` | 泵故障管理:记录故障发生时间和受影响的泵列表,更新泵状态日志 | -| `POST /pressure_sensor_placement_sensitivity/` | 高级传感器放置(灵敏度法):通过JSON体提供详细参数,基于灵敏度矩阵确定最优放置位置 | -| `POST /pressure_sensor_placement_kmeans/` | 高级传感器放置(KMeans法):通过JSON体提供详细参数,基于聚类算法确定最优放置位置 | -| `POST /sensorplacementscheme/create` | 创建传感器放置方案:支持 sensitivity 和 kmeans 两种算法,自动计算最优传感器位置并存储方案 | -| `POST /runsimulationmanuallybydate/` | 按日期手动运行模拟:根据指定日期、开始时间和持续时间查询管网参数并执行水力模拟 | \ No newline at end of file diff --git a/.opencode/skills/business/SKILL.md b/.opencode/skills/business/SKILL.md deleted file mode 100644 index 27df977..0000000 --- a/.opencode/skills/business/SKILL.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: tjwater-domain-business -description: 负责业务逻辑、业务对象与项目侧 API 能力。 -version: 3.0.0 ---- - -# Business Domain Skill - -## 简介 -负责业务逻辑、业务对象与项目侧 API 能力。 - -## 子模块索引 (渐进式引导) -- **component-config**: 见 `./component-config/SKILL.md` -- **identity-access**: 见 `./identity-access/SKILL.md` -- **network-assets**: 见 `./network-assets/SKILL.md` -- **project-workspace**: 见 `./project-workspace/SKILL.md` - diff --git a/.opencode/skills/business/component-config/SKILL.md b/.opencode/skills/business/component-config/SKILL.md deleted file mode 100644 index b2bb076..0000000 --- a/.opencode/skills/business/component-config/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: tjwater-scenario-business-component-config -description: 负责组件配置与参数管理。 -version: 3.0.0 ---- - -# component-config Scenario Skill - -## 简介 -负责组件配置与参数管理。 - -## 子模块索引 (渐进式引导) -- **controls**: 见 `./controls/SKILL.md` -- **curves**: 见 `./curves/SKILL.md` -- **options**: 见 `./options/SKILL.md` -- **patterns**: 见 `./patterns/SKILL.md` -- **quality**: 见 `./quality/SKILL.md` -- **visuals**: 见 `./visuals/SKILL.md` - diff --git a/.opencode/skills/business/component-config/controls/SKILL.md b/.opencode/skills/business/component-config/controls/SKILL.md deleted file mode 100644 index c471358..0000000 --- a/.opencode/skills/business/component-config/controls/SKILL.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: tjwater-action-business-component-config-controls -description: business/component-config 下 controls 操作技能。 -version: 3.0.0 ---- - -# controls Action Skill - -## 简介 -负责 `business/component-config` 场景下 `controls` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/getcontrolproperties/` | 获取控制属性 | network (query) | - | -| GET | `/api/v1/getcontrolschema/` | 获取控制架构 | network (query) | - | -| GET | `/api/v1/getruleproperties/` | 获取规则属性 | network (query) | - | -| GET | `/api/v1/getruleschema/` | 获取规则架构 | network (query) | - | -| POST | `/api/v1/setcontrolproperties/` | 设置控制属性 | network (query) | - | -| POST | `/api/v1/setruleproperties/` | 设置规则属性 | network (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getcontrolschema/` | 返回简单控制(Control)数据模型的字段定义 | -| `GET /getcontrolproperties/` | 获取管网中所有简单控制规则的属性列表 | -| `GET /getruleschema/` | 返回规则控制(Rule)数据模型的字段定义 | -| `GET /getruleproperties/` | 获取管网中所有基于规则的复杂控制条件列表 | -| `POST /setcontrolproperties/` | 设置/更新简单控制规则的属性 | -| `POST /setruleproperties/` | 设置/更新规则控制的属性 | diff --git a/.opencode/skills/business/component-config/curves/SKILL.md b/.opencode/skills/business/component-config/curves/SKILL.md deleted file mode 100644 index 61349ed..0000000 --- a/.opencode/skills/business/component-config/curves/SKILL.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: tjwater-action-business-component-config-curves -description: business/component-config 下 curves 操作技能。 -version: 3.0.0 ---- - -# curves Action Skill - -## 简介 -负责 `business/component-config` 场景下 `curves` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/addcurve/` | 添加曲线 | network (query), curve (query) | - | -| POST | `/api/v1/deletecurve/` | 删除曲线 | network (query), curve (query) | - | -| GET | `/api/v1/getcurveproperties/` | 获取曲线属性 | network (query), curve (query) | - | -| GET | `/api/v1/getcurves/` | 获取所有曲线 | network (query) | - | -| GET | `/api/v1/getcurveschema` | 获取曲线架构 | network (query) | - | -| GET | `/api/v1/iscurve/` | 检查曲线存在性 | network (query), curve (query) | - | -| POST | `/api/v1/setcurveproperties/` | 设置曲线属性 | network (query), curve (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getcurveschema` | 返回曲线(Curve)数据模型的字段定义(水泵特性曲线、效率曲线等) | -| `GET /getcurves/` | 获取管网中所有曲线的ID列表 | -| `GET /getcurveproperties/` | 查询指定曲线的详细属性(类型、控制点数据等) | -| `POST /addcurve/` | 向管网添加一条新曲线 | -| `POST /deletecurve/` | 从管网删除指定曲线 | -| `POST /setcurveproperties/` | 设置/更新曲线的属性(控制点坐标等) | diff --git a/.opencode/skills/business/component-config/options/SKILL.md b/.opencode/skills/business/component-config/options/SKILL.md deleted file mode 100644 index 349ec79..0000000 --- a/.opencode/skills/business/component-config/options/SKILL.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: tjwater-action-business-component-config-options -description: business/component-config 下 options 操作技能。 -version: 3.0.0 ---- - -# options Action Skill - -## 简介 -负责 `business/component-config` 场景下 `options` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/getenergyproperties/` | 获取能耗选项属性 | network (query) | - | -| GET | `/api/v1/getenergyschema/` | 获取能耗选项架构 | network (query) | - | -| GET | `/api/v1/getoptionproperties/` | 获取选项属性 | network (query) | - | -| GET | `/api/v1/getoptionschema/` | 获取选项架构 | network (query) | - | -| GET | `/api/v1/getpumpenergyproperties/` | 获取泵能耗属性 | - | - | -| GET | `/api/v1/getpumpenergyschema/` | 获取泵能耗选项架构 | network (query) | - | -| GET | `/api/v1/gettimeproperties/` | 获取时间选项属性 | network (query) | - | -| GET | `/api/v1/gettimeschema` | 获取时间选项架构 | network (query) | - | -| POST | `/api/v1/setenergyproperties/` | 设置能耗选项属性 | network (query) | - | -| POST | `/api/v1/setoptionproperties/` | 设置选项属性 | network (query) | - | -| GET | `/api/v1/setpumpenergyproperties/` | 设置泵能耗属性 | - | - | -| POST | `/api/v1/settimeproperties/` | 设置时间选项属性 | network (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getoptionschema/` | 返回模拟选项(Options)数据模型的字段定义 | -| `GET /getoptionproperties/` | 获取管网模拟参数设置(时步、精度、单位系统等) | -| `GET /getenergyschema/` | 返回能耗选项(Energy)数据模型的字段定义 | -| `GET /getenergyproperties/` | 获取全局能耗设置(电价、效率等) | -| `GET /getpumpenergyproperties/` | 获取单台水泵的能耗参数 | -| `POST /setoptionproperties/` | 设置管网模拟参数 | -| `POST /setenergyproperties/` | 设置全局能耗参数 | diff --git a/.opencode/skills/business/component-config/patterns/SKILL.md b/.opencode/skills/business/component-config/patterns/SKILL.md deleted file mode 100644 index 97eca7f..0000000 --- a/.opencode/skills/business/component-config/patterns/SKILL.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: tjwater-action-business-component-config-patterns -description: business/component-config 下 patterns 操作技能。 -version: 3.0.0 ---- - -# patterns Action Skill - -## 简介 -负责 `business/component-config` 场景下 `patterns` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/addpattern/` | 添加模式 | network (query), pattern (query) | - | -| POST | `/api/v1/deletepattern/` | 删除模式 | network (query), pattern (query) | - | -| GET | `/api/v1/getpatternproperties/` | 获取模式属性 | network (query), pattern (query) | - | -| GET | `/api/v1/getpatterns/` | 获取所有模式 | network (query) | - | -| GET | `/api/v1/getpatternschema` | 获取模式架构 | network (query) | - | -| GET | `/api/v1/ispattern/` | 检查模式存在性 | network (query), pattern (query) | - | -| POST | `/api/v1/setpatternproperties/` | 设置模式属性 | network (query), pattern (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getpatternschema` | 返回模式(Pattern)数据模型的字段定义 | -| `GET /getpatterns/` | 获取管网中所有时间模式的ID列表 | -| `GET /getpatternproperties/` | 查询指定模式的属性(时间序列乘数值等) | -| `POST /addpattern/` | 向管网添加一个新时间模式 | -| `POST /deletepattern/` | 从管网删除指定时间模式 | -| `POST /setpatternproperties/` | 设置/更新时间模式的属性(乘数序列) | diff --git a/.opencode/skills/business/component-config/quality/SKILL.md b/.opencode/skills/business/component-config/quality/SKILL.md deleted file mode 100644 index f69b8a4..0000000 --- a/.opencode/skills/business/component-config/quality/SKILL.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: tjwater-action-business-component-config-quality -description: business/component-config 下 quality 操作技能。 -version: 3.0.0 ---- - -# quality Action Skill - -## 简介 -负责 `business/component-config` 场景下 `quality` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/addmixing/` | 添加混合 | network (query) | - | -| POST | `/api/v1/addsource/` | 添加水源 | network (query) | - | -| POST | `/api/v1/deletemixing/` | 删除混合 | network (query) | - | -| POST | `/api/v1/deletesource/` | 删除水源 | network (query), node (query) | - | -| GET | `/api/v1/getemitterproperties/` | 获取发射器属性 | network (query), junction (query) | - | -| GET | `/api/v1/getemitterschema` | 获取发射器架构 | network (query) | - | -| GET | `/api/v1/getmixing/` | 获取混合属性 | network (query), tank (query) | - | -| GET | `/api/v1/getmixingschema/` | 获取混合架构 | network (query) | - | -| GET | `/api/v1/getpipereaction/` | 获取管道反应属性 | network (query), pipe (query) | - | -| GET | `/api/v1/getpipereactionschema/` | 获取管道反应架构 | network (query) | - | -| GET | `/api/v1/getqualityproperties/` | 获取水质属性 | network (query), node (query) | - | -| GET | `/api/v1/getqualityschema/` | 获取水质架构 | network (query) | - | -| GET | `/api/v1/getreaction/` | 获取反应属性 | network (query) | - | -| GET | `/api/v1/getreactionschema/` | 获取反应架构 | network (query) | - | -| GET | `/api/v1/getsource/` | 获取水源属性 | network (query), node (query) | - | -| GET | `/api/v1/getsourcechema/` | 获取水源架构 | network (query) | - | -| GET | `/api/v1/gettankreaction/` | 获取水池反应属性 | network (query), tank (query) | - | -| GET | `/api/v1/gettankreactionschema/` | 获取水池反应架构 | network (query) | - | -| POST | `/api/v1/setemitterproperties/` | 设置发射器属性 | network (query), junction (query) | - | -| POST | `/api/v1/setmixing/` | 设置混合属性 | network (query) | - | -| POST | `/api/v1/setpipereaction/` | 设置管道反应属性 | network (query) | - | -| POST | `/api/v1/setqualityproperties/` | 设置水质属性 | network (query) | - | -| POST | `/api/v1/setreaction/` | 设置反应属性 | network (query) | - | -| POST | `/api/v1/setsource/` | 设置水源属性 | network (query) | - | -| POST | `/api/v1/settankreaction/` | 设置水池反应属性 | network (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getemitterproperties/` | 查询节点处发射器(用于模拟管漏)的属性 | -| `GET /getmixingproperties/` | 查询水箱水质混合模型属性 | -| `GET /getsourceproperties/` | 查询水质污染源的属性(位置、类型、浓度等) | -| `GET /getreactionproperties/` | 获取全局水质反应参数(管网反应系数等) | -| `GET /getwaterqualityresult/` | 查询水质模拟结果 | -| `POST /addsource/` | 向管网添加一个水质污染源 | -| `POST /deletesource/` | 删除指定水质污染源 | -| `POST /addmixing/` | 为水箱添加水质混合模型 | -| `POST /deletemixing/` | 删除水箱水质混合模型 | -| `POST /setemitter*/` | 设置发射器属性(流量系数等) | diff --git a/.opencode/skills/business/component-config/visuals/SKILL.md b/.opencode/skills/business/component-config/visuals/SKILL.md deleted file mode 100644 index ee00b4a..0000000 --- a/.opencode/skills/business/component-config/visuals/SKILL.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: tjwater-action-business-component-config-visuals -description: business/component-config 下 visuals 操作技能。 -version: 3.0.0 ---- - -# visuals Action Skill - -## 简介 -负责 `business/component-config` 场景下 `visuals` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/addlabel/` | 添加标签 | network (query) | - | -| POST | `/api/v1/addvertex/` | 添加图形元素 | network (query) | - | -| POST | `/api/v1/deletelabel/` | 删除标签 | network (query) | - | -| POST | `/api/v1/deletevertex/` | 删除图形元素 | network (query) | - | -| GET | `/api/v1/getallvertexlinks/` | 获取所有图形元素链接 | network (query) | - | -| GET | `/api/v1/getallvertices/` | 获取所有图形元素 | network (query) | - | -| GET | `/api/v1/getbackdropproperties/` | 获取背景属性 | network (query) | - | -| GET | `/api/v1/getbackdropschema/` | 获取背景架构 | network (query) | - | -| GET | `/api/v1/getlabelproperties/` | 获取标签属性 | network (query), x (query), y (query) | - | -| GET | `/api/v1/getlabelschema/` | 获取标签架构 | network (query) | - | -| GET | `/api/v1/getvertexproperties/` | 获取图形元素属性 | network (query), link (query) | - | -| GET | `/api/v1/getvertexschema/` | 获取图形元素架构 | network (query) | - | -| POST | `/api/v1/setbackdropproperties/` | 设置背景属性 | network (query) | - | -| POST | `/api/v1/setlabelproperties/` | 设置标签属性 | network (query) | - | -| POST | `/api/v1/setvertexproperties/` | 设置图形元素属性 | network (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getallvertexlinks/` | 获取所有管线的折点列表(用于地图还原管线真实走向) | -| `GET /getvertexlink/` | 获取单条管线的折点坐标序列 | -| `POST /addvertex/` | 为管线添加一个折点(改变管线显示路径) | -| `POST /deletevertex/` | 删除管线上的指定折点 | -| `POST /addlabel/` | 在地图上添加文字标注 | -| `POST /deletelabel/` | 删除地图文字标注 | diff --git a/.opencode/skills/business/identity-access/SKILL.md b/.opencode/skills/business/identity-access/SKILL.md deleted file mode 100644 index fc3aeac..0000000 --- a/.opencode/skills/business/identity-access/SKILL.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: tjwater-scenario-business-identity-access -description: 负责认证、用户与权限相关操作。 -version: 3.0.0 ---- - -# identity-access Scenario Skill - -## 简介 -负责认证、用户与权限相关操作。 - -## 子模块索引 (渐进式引导) -- **auth**: 见 `./auth/SKILL.md` -- **user_management**: 见 `./user_management/SKILL.md` -- **users**: 见 `./users/SKILL.md` - diff --git a/.opencode/skills/business/identity-access/auth/SKILL.md b/.opencode/skills/business/identity-access/auth/SKILL.md deleted file mode 100644 index 17369aa..0000000 --- a/.opencode/skills/business/identity-access/auth/SKILL.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: tjwater-action-business-identity-access-auth -description: business/identity-access 下 auth 操作技能。 -version: 3.0.0 ---- - -# auth Action Skill - -## 简介 -负责 `business/identity-access` 场景下 `auth` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/auth/login` | 用户登录 | form_data (body) | - | -| POST | `/api/v1/auth/login/simple` | 简化版登录 | username (query), password (query) | - | -| GET | `/api/v1/auth/me` | 获取当前用户信息 | - | - | -| POST | `/api/v1/auth/refresh` | 刷新AccessToken | refresh_token (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `POST /login` | OAuth2标准格式登录,提交form-data(username+password),返回JWT Access Token和Refresh Token | -| `POST /login/simple` | 简化版登录,直接通过query参数传递username和password,保持向后兼容 | -| `GET /me` | 返回当前已登录用户的详细信息(需携带Access Token) | -| `POST /refresh` | 使用Refresh Token换取新的Access Token,延续会话 | diff --git a/.opencode/skills/business/identity-access/user-management/SKILL.md b/.opencode/skills/business/identity-access/user-management/SKILL.md deleted file mode 100644 index ebfdec3..0000000 --- a/.opencode/skills/business/identity-access/user-management/SKILL.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: tjwater-action-business-identity-access-user-management -description: business/identity-access 下 user-management 操作技能。 -version: 3.0.0 ---- - -# user-management Action Skill - -## 简介 -负责 `business/identity-access` 场景下 `user-management` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/users/` | 列出所有用户 | - | skip (query), limit (query) | -| DELETE | `/api/v1/users/{user_id}` | 删除用户 | user_id (path) | - | -| GET | `/api/v1/users/{user_id}` | 获取用户详情 | user_id (path) | - | -| PUT | `/api/v1/users/{user_id}` | 更新用户信息 | user_id (path) | user_update (body) | -| POST | `/api/v1/users/{user_id}/activate` | 激活用户 | user_id (path) | - | -| POST | `/api/v1/users/{user_id}/deactivate` | 停用用户 | user_id (path) | - | - -- 覆盖方法:`DELETE, GET, POST, PUT` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /users/` | 列出系统中所有用户(管理员权限),支持分页(skip/limit) | -| `GET /users/{user_id}` | 按用户ID查询单个用户的详细信息 | -| `PUT /users/{user_id}` | 更新指定用户的信息(邮箱、角色、密码等),请求体为 user_update 对象 | -| `DELETE /users/{user_id}` | 删除指定用户(软删除或硬删除) | -| `POST /users/{user_id}/activate` | 激活指定用户账号(管理员操作) | -| `POST /users/{user_id}/deactivate` | 停用指定用户账号(禁止登录,管理员操作) | diff --git a/.opencode/skills/business/identity-access/users/SKILL.md b/.opencode/skills/business/identity-access/users/SKILL.md deleted file mode 100644 index 0ee62c9..0000000 --- a/.opencode/skills/business/identity-access/users/SKILL.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: tjwater-action-business-identity-access-users -description: business/identity-access 下 users 操作技能。 -version: 3.0.0 ---- - -# users Action Skill - -## 简介 -负责 `business/identity-access` 场景下 `users` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/getallusers/` | 获取所有用户 | network (query) | - | -| GET | `/api/v1/getuser/` | 获取单个用户 | network (query), user_name (query) | - | -| GET | `/api/v1/getuserschema/` | 获取用户模式 | network (query) | - | - -- 覆盖方法:`GET` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getallusers/` | 获取指定管网下的所有用户列表(旧版接口,返回管网级别用户信息) | -| `GET /getuser/` | 按用户名查询指定管网下的单个用户信息 | -| `GET /getuserschema/` | 获取用户数据模型的字段定义(Schema) | diff --git a/.opencode/skills/business/network-assets/SKILL.md b/.opencode/skills/business/network-assets/SKILL.md deleted file mode 100644 index 3e2a023..0000000 --- a/.opencode/skills/business/network-assets/SKILL.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: tjwater-scenario-business-network-assets -description: 负责管网资产与拓扑对象操作。 -version: 3.0.0 ---- - -# network-assets Scenario Skill - -## 简介 -负责管网资产与拓扑对象操作。 - -## 子模块索引 (渐进式引导) -- **demands**: 见 `./demands/SKILL.md` -- **general**: 见 `./general/SKILL.md` -- **geometry**: 见 `./geometry/SKILL.md` -- **junctions**: 见 `./junctions/SKILL.md` -- **pipes**: 见 `./pipes/SKILL.md` -- **pumps**: 见 `./pumps/SKILL.md` -- **regions**: 见 `./regions/SKILL.md` -- **reservoirs**: 见 `./reservoirs/SKILL.md` -- **tags**: 见 `./tags/SKILL.md` -- **tanks**: 见 `./tanks/SKILL.md` -- **valves**: 见 `./valves/SKILL.md` - diff --git a/.opencode/skills/business/network-assets/demands/SKILL.md b/.opencode/skills/business/network-assets/demands/SKILL.md deleted file mode 100644 index 1d2daf6..0000000 --- a/.opencode/skills/business/network-assets/demands/SKILL.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: tjwater-action-business-network-assets-demands -description: business/network-assets 下 demands 操作技能。 -version: 3.0.0 ---- - -# demands Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `demands` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/calculatedemandtonetwork/` | 计算需水量到整网分配 | network (query), demand (query) | - | -| GET | `/api/v1/calculatedemandtonodes/` | 计算需水量到节点分配 | network (query) | - | -| GET | `/api/v1/calculatedemandtoregion/` | 计算需水量到区域分配 | network (query) | - | -| GET | `/api/v1/getdemandproperties/` | 获取需水量属性 | network (query), junction (query) | - | -| GET | `/api/v1/getdemandschema` | 获取需水量属性架构 | network (query) | - | -| POST | `/api/v1/setdemandproperties/` | 设置需水量属性 | network (query), junction (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getdemandschema` | 返回需水量(Demand)数据模型的字段定义 | -| `GET /getdemandproperties/` | 查询指定节点的需水量属性(基础需水量、模式等) | -| `POST /setdemandproperties/` | 设置节点的需水量属性 | -| `POST /calculatedemandtonodes/` | 将指定总需水量计算分配到各节点 | -| `POST /calculatedemandtoregion/` | 将指定总需水量计算分配到指定区域内的节点 | -| `POST /calculatedemandtonetwork/` | 将指定总需水量按比例分配到整个管网的所有节点 | diff --git a/.opencode/skills/business/network-assets/general/SKILL.md b/.opencode/skills/business/network-assets/general/SKILL.md deleted file mode 100644 index 12e64ec..0000000 --- a/.opencode/skills/business/network-assets/general/SKILL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: tjwater-action-business-network-assets-general -description: business/network-assets 下 general 操作技能。 -version: 3.0.0 ---- - -# general Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `general` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/deletelink/` | 删除管线 | network (query), link (query) | - | -| POST | `/api/v1/deletenode/` | 删除节点 | network (query), node (query) | - | -| GET | `/api/v1/getallscadaproperties/` | 获取所有SCADA点属性 | network (query) | - | -| GET | `/api/v1/getelementproperties/` | 获取元素属性 | network (query), element (query) | - | -| GET | `/api/v1/getelementpropertieswithtype/` | 获取指定类型元素属性 | network (query), elementtype (query), element (query) | - | -| GET | `/api/v1/getelementtype/` | 获取元素类型 | network (query), element (query) | - | -| GET | `/api/v1/getelementtypevalue/` | 获取元素类型值 | network (query), element (query) | - | -| GET | `/api/v1/getlinkproperties/` | 获取管线属性 | network (query), link (query) | - | -| GET | `/api/v1/getlinks/` | 获取所有管线 | network (query) | - | -| GET | `/api/v1/getlinktype/` | 获取管线类型 | network (query), link (query) | - | -| GET | `/api/v1/getnodelinks/` | 获取节点的关联管线 | network (query), node (query) | - | -| GET | `/api/v1/getnodeproperties/` | 获取节点属性 | network (query), node (query) | - | -| GET | `/api/v1/getnodes/` | 获取所有节点 | network (query) | - | -| GET | `/api/v1/getnodetype/` | 获取节点类型 | network (query), node (query) | - | -| GET | `/api/v1/getscadaproperties/` | 获取SCADA点属性 | network (query), scada (query) | - | -| GET | `/api/v1/getstatus/` | 获取管线状态 | network (query), link (query) | - | -| GET | `/api/v1/getstatusschema` | 获取状态属性架构 | network (query) | - | -| GET | `/api/v1/gettitle/` | 获取水网标题属性 | network (query) | - | -| GET | `/api/v1/gettitleschema/` | 获取标题属性架构 | network (query) | - | -| GET | `/api/v1/isjunction/` | 检查是否为接点 | network (query), node (query) | - | -| GET | `/api/v1/islink/` | 检查管线有效性 | network (query), link (query) | - | -| GET | `/api/v1/isnode/` | 检查节点有效性 | network (query), node (query) | - | -| GET | `/api/v1/ispipe/` | 检查是否为管道 | network (query), link (query) | - | -| GET | `/api/v1/ispump/` | 检查是否为泵 | network (query), link (query) | - | -| GET | `/api/v1/isreservoir/` | 检查是否为水源 | network (query), node (query) | - | -| GET | `/api/v1/istank/` | 检查是否为蓄水池 | network (query), node (query) | - | -| GET | `/api/v1/isvalve/` | 检查是否为阀门 | network (query), link (query) | - | -| POST | `/api/v1/setstatus/` | 设置管线状态 | network (query), link (query) | - | -| GET | `/api/v1/settitle/` | 设置水网标题属性 | network (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getelementtype/` | 查询指定ID元素的类型(Junction/Pipe/Pump/Tank/Valve/Reservoir) | -| `GET /getelementtypevalue/` | 查询指定ID元素的类型编码值 | -| `GET /getelementproperties/` | 查询指定ID元素的所有属性(自动识别类型) | -| `GET /getelementpropertieswithtype/` | 查询指定类型和ID的元素属性 | -| `GET /getlinkproperties/` | 查询管线(Pipe/Pump/Valve)的属性 | -| `GET /getnodeproperties/` | 查询节点(Junction/Tank/Reservoir)的属性 | -| `GET /settitle/` | 设置管网标题属性 | -| `POST /deletelink/` | 删除管线(管道/水泵/阀门) | -| `POST /deletenode/` | 删除节点(节点/水箱/水库) | diff --git a/.opencode/skills/business/network-assets/geometry/SKILL.md b/.opencode/skills/business/network-assets/geometry/SKILL.md deleted file mode 100644 index e976aa7..0000000 --- a/.opencode/skills/business/network-assets/geometry/SKILL.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: tjwater-action-business-network-assets-geometry -description: business/network-assets 下 geometry 操作技能。 -version: 3.0.0 ---- - -# geometry Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `geometry` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/getmajornodecoords/` | 获取主要节点坐标 | network (query), diameter (query) | - | -| GET | `/api/v1/getmajorpipenodes/` | 获取主要管道节点 | network (query), diameter (query) | - | -| GET | `/api/v1/getnetworkgeometries/` | 获取完整网络几何信息 | network (query) | - | -| GET | `/api/v1/getnetworkinextent/` | 获取范围内的网络元素 | network (query), x1 (query), y1 (query), x2 (query), y2 (query) | - | -| GET | `/api/v1/getnetworklinknodes/` | 获取网络管线节点 | network (query) | - | -| GET | `/api/v1/getnodecoord/` | 获取节点坐标 | network (query), node (query) | - | - -- 覆盖方法:`GET` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getnodecoord/` | 查询单个节点(任意类型)的坐标(x, y) | -| `GET /getmajornodecoords/` | 获取管网主要节点(干管节点)的坐标列表,用于快速渲染 | -| `GET /getmajorpipenodes/` | 获取主要管道的起终节点列表 | -| `GET /getnetworklinknodes/` | 获取管网中所有管线的起终节点信息 | -| `GET /getnetworkgeometries/` | 获取整个管网的完整几何信息(节点坐标 + 管线折点),适合地图绘制 | -| `GET /getnetworkinextent/` | 查询指定地理范围(bbox)内的管网节点和管线 | diff --git a/.opencode/skills/business/network-assets/junctions/SKILL.md b/.opencode/skills/business/network-assets/junctions/SKILL.md deleted file mode 100644 index a629e10..0000000 --- a/.opencode/skills/business/network-assets/junctions/SKILL.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: tjwater-action-business-network-assets-junctions -description: business/network-assets 下 junctions 操作技能。 -version: 3.0.0 ---- - -# junctions Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `junctions` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/addjunction/` | 添加节点 | network (query), junction (query), x (query), y (query), z (query) | - | -| POST | `/api/v1/deletejunction/` | 删除节点 | network (query), junction (query) | - | -| GET | `/api/v1/getalljunctionproperties/` | 获取所有节点属性 | network (query) | - | -| GET | `/api/v1/getjunctioncoord/` | 获取节点坐标 | network (query), junction (query) | - | -| GET | `/api/v1/getjunctiondemand/` | 获取节点需水量 | network (query), junction (query) | - | -| GET | `/api/v1/getjunctionelevation/` | 获取节点标高 | network (query), junction (query) | - | -| GET | `/api/v1/getjunctionpattern/` | 获取节点需水模式 | network (query), junction (query) | - | -| GET | `/api/v1/getjunctionproperties/` | 获取节点属性 | network (query), junction (query) | - | -| GET | `/api/v1/getjunctionschema` | 获取节点架构 | network (query) | - | -| GET | `/api/v1/getjunctionx/` | 获取节点 X 坐标 | network (query), junction (query) | - | -| GET | `/api/v1/getjunctiony/` | 获取节点 Y 坐标 | network (query), junction (query) | - | -| POST | `/api/v1/setjunctioncoord/` | 设置节点坐标 | network (query), junction (query), x (query), y (query) | - | -| POST | `/api/v1/setjunctiondemand/` | 设置节点需水量 | network (query), junction (query), demand (query) | - | -| POST | `/api/v1/setjunctionelevation/` | 设置节点标高 | network (query), junction (query), elevation (query) | - | -| POST | `/api/v1/setjunctionpattern/` | 设置节点需水模式 | network (query), junction (query), pattern (query) | - | -| POST | `/api/v1/setjunctionproperties/` | 批量设置节点属性 | network (query), junction (query) | - | -| POST | `/api/v1/setjunctionx/` | 设置节点 X 坐标 | network (query), junction (query), x (query) | - | -| POST | `/api/v1/setjunctiony/` | 设置节点 Y 坐标 | network (query), junction (query), y (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getjunctionschema` | 返回节点(Junction)数据模型的所有字段定义 | -| `GET /getjunctionproperties/` | 查询单个节点的所有属性(标高、需水量、坐标等) | -| `GET /getalljunctionproperties/` | 批量获取管网中所有节点的属性列表 | -| `GET /getjunctioncoord/` | 查询单个节点的坐标(x, y) | -| `GET /getjunctionelevation/` | 查询节点标高值 | -| `GET /getjunctiondemand/` | 查询节点基础需水量 | -| `GET /getjunctionpattern/` | 查询节点关联的需水时间模式名称 | -| `POST /addjunction/` | 向管网添加一个新节点,需提供ID、坐标和标高 | -| `POST /deletejunction/` | 从管网删除指定节点 | -| `POST /setjunction*/` | 设置节点某个具体属性(坐标、标高、需水量等) | diff --git a/.opencode/skills/business/network-assets/pipes/SKILL.md b/.opencode/skills/business/network-assets/pipes/SKILL.md deleted file mode 100644 index fc81d8e..0000000 --- a/.opencode/skills/business/network-assets/pipes/SKILL.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: tjwater-action-business-network-assets-pipes -description: business/network-assets 下 pipes 操作技能。 -version: 3.0.0 ---- - -# pipes Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `pipes` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/addpipe/` | 添加管道 | network (query), pipe (query), node1 (query), node2 (query) | length (query), diameter (query), roughness (query), minor_loss (query), status (query) | -| POST | `/api/v1/deletepipe/` | 删除管道 | network (query), pipe (query) | - | -| GET | `/api/v1/getallpipeproperties/` | 获取所有管道属性 | network (query) | - | -| GET | `/api/v1/getpipediameter/` | 获取管道管径 | network (query), pipe (query) | - | -| GET | `/api/v1/getpipelength/` | 获取管道长度 | network (query), pipe (query) | - | -| GET | `/api/v1/getpipeminorloss/` | 获取管道局部阻力系数 | network (query), pipe (query) | - | -| GET | `/api/v1/getpipenode1/` | 获取管道起始节点 | network (query), pipe (query) | - | -| GET | `/api/v1/getpipenode2/` | 获取管道终止节点 | network (query), pipe (query) | - | -| GET | `/api/v1/getpipeproperties/` | 获取管道属性 | network (query), pipe (query) | - | -| GET | `/api/v1/getpiperoughness/` | 获取管道粗糙度 | network (query), pipe (query) | - | -| GET | `/api/v1/getpipeschema` | 获取管道模式 | network (query) | - | -| GET | `/api/v1/getpipestatus/` | 获取管道状态 | network (query), pipe (query) | - | -| POST | `/api/v1/setpipediameter/` | 设置管道管径 | network (query), pipe (query), diameter (query) | - | -| POST | `/api/v1/setpipelength/` | 设置管道长度 | network (query), pipe (query), length (query) | - | -| POST | `/api/v1/setpipeminorloss/` | 设置管道局部阻力系数 | network (query), pipe (query), minor_loss (query) | - | -| POST | `/api/v1/setpipenode1/` | 设置管道起始节点 | network (query), pipe (query), node1 (query) | - | -| POST | `/api/v1/setpipenode2/` | 设置管道终止节点 | network (query), pipe (query), node2 (query) | - | -| POST | `/api/v1/setpipeproperties/` | 设置管道属性 | network (query), pipe (query) | - | -| POST | `/api/v1/setpiperoughness/` | 设置管道粗糙度 | network (query), pipe (query), roughness (query) | - | -| POST | `/api/v1/setpipestatus/` | 设置管道状态 | network (query), pipe (query), status (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getpipeschema` | 返回管道(Pipe)数据模型的所有字段定义 | -| `GET /getpipeproperties/` | 查询单条管道的所有属性(管径、长度、起终节点等) | -| `GET /getallpipeproperties/` | 批量获取管网中所有管道的属性列表 | -| `GET /getpipelength/` | 查询管道长度 | -| `GET /getpipediameter/` | 查询管道管径 | -| `GET /getpipestatus/` | 查询管道当前状态(开/关/CV) | -| `GET /getpiperoughness/` | 查询管道粗糙系数 | -| `POST /addpipe/` | 向管网添加一条新管道,需提供ID、起终节点、长度和管径 | -| `POST /deletepipe/` | 从管网删除指定管道 | -| `POST /setpipe*/` | 设置管道某个具体属性(管径、长度、状态等) | diff --git a/.opencode/skills/business/network-assets/pumps/SKILL.md b/.opencode/skills/business/network-assets/pumps/SKILL.md deleted file mode 100644 index 14953ae..0000000 --- a/.opencode/skills/business/network-assets/pumps/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: tjwater-action-business-network-assets-pumps -description: business/network-assets 下 pumps 操作技能。 -version: 3.0.0 ---- - -# pumps Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `pumps` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/addpump/` | 添加水泵 | network (query), pump (query), node1 (query), node2 (query) | power (query) | -| POST | `/api/v1/deletepump/` | 删除水泵 | network (query), pump (query) | - | -| GET | `/api/v1/getallpumpproperties/` | 获取所有水泵属性 | network (query) | - | -| GET | `/api/v1/getpumpnode1/` | 获取水泵起始节点 | network (query), pump (query) | - | -| GET | `/api/v1/getpumpnode2/` | 获取水泵终止节点 | network (query), pump (query) | - | -| GET | `/api/v1/getpumpproperties/` | 获取水泵属性 | network (query), pump (query) | - | -| GET | `/api/v1/getpumpschema` | 获取水泵模式 | network (query) | - | -| POST | `/api/v1/setpumpnode1/` | 设置水泵起始节点 | network (query), pump (query), node1 (query) | - | -| POST | `/api/v1/setpumpnode2/` | 设置水泵终止节点 | network (query), pump (query), node2 (query) | - | -| POST | `/api/v1/setpumpproperties/` | 设置水泵属性 | network (query), pump (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getpumpschema` | 返回水泵(Pump)数据模型的所有字段定义 | -| `GET /getpumpproperties/` | 查询单台水泵的所有属性(曲线名称、起终节点等) | -| `GET /getallpumpproperties/` | 批量获取管网中所有水泵的属性列表 | -| `POST /addpump/` | 向管网添加一台新水泵,需提供ID和起终节点 | -| `POST /deletepump/` | 从管网删除指定水泵 | -| `POST /setpumpproperties/` | 批量设置水泵属性(曲线、初始状态、效率等) | -| `POST /setpumpnode1/` | 设置水泵起始节点 | -| `POST /setpumpnode2/` | 设置水泵终止节点 | diff --git a/.opencode/skills/business/network-assets/regions/SKILL.md b/.opencode/skills/business/network-assets/regions/SKILL.md deleted file mode 100644 index 7f5072c..0000000 --- a/.opencode/skills/business/network-assets/regions/SKILL.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -name: tjwater-action-business-network-assets-regions -description: business/network-assets 下 regions 操作技能。 -version: 3.0.1 ---- - -# regions Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `regions` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/adddistrictmeteringarea/` | 添加新DMA | network (query) | - | -| POST | `/api/v1/addregion/` | 添加新区域 | network (query) | - | -| POST | `/api/v1/addservicearea/` | 添加新服务区 | network (query) | - | -| POST | `/api/v1/addvirtualdistrict/` | 添加新虚拟分区 | network (query) | - | -| GET | `/api/v1/calculatedistrictmeteringareafornetwork/` | 计算整网DMA分区 | network (query) | - | -| GET | `/api/v1/calculatedistrictmeteringareafornodes/` | 计算节点DMA分区 | network (query) | - | -| GET | `/api/v1/calculatedistrictmeteringareaforregion/` | 计算区域内DMA分区 | network (query) | - | -| GET | `/api/v1/calculateservicearea/` | 计算服务区(返回全部时间步) | network (query) | - | -| GET | `/api/v1/calculatevirtualdistrict/` | 计算虚拟分区 | network (query), centers (query) | - | -| POST | `/api/v1/deletedistrictmeteringarea/` | 删除DMA | network (query) | - | -| POST | `/api/v1/deleteregion/` | 删除区域 | network (query) | - | -| POST | `/api/v1/deleteservicearea/` | 删除服务区 | network (query) | - | -| POST | `/api/v1/deletevirtualdistrict/` | 删除虚拟分区 | network (query) | - | -| POST | `/api/v1/generatedistrictmeteringarea/` | 生成DMA分区 | network (query), part_count (query), part_type (query), inflate_delta (query) | - | -| POST | `/api/v1/generateservicearea/` | 生成服务区分区 | network (query), inflate_delta (query) | - | -| POST | `/api/v1/generatesubdistrictmeteringarea/` | 生成DMA子分区 | network (query), dma (query), part_count (query), part_type (query), inflate_delta (query) | - | -| POST | `/api/v1/generatevirtualdistrict/` | 生成虚拟分区 | network (query), inflate_delta (query) | - | -| GET | `/api/v1/getalldistrictmeteringareaids/` | 获取所有DMA ID | network (query) | - | -| GET | `/api/v1/getalldistrictmeteringareas/` | 获取所有DMA | network (query) | - | -| GET | `/api/v1/getallserviceareas/` | 获取所有服务区 | network (query) | - | -| GET | `/api/v1/getallvirtualdistrict/` | 获取所有虚拟分区 | network (query) | - | -| GET | `/api/v1/getdistrictmeteringarea/` | 获取DMA信息 | network (query), id (query) | - | -| GET | `/api/v1/getdistrictmeteringareaschema/` | 获取DMA属性架构 | network (query) | - | -| GET | `/api/v1/getregion/` | 获取区域信息 | network (query), id (query) | - | -| GET | `/api/v1/getregionschema/` | 获取区域属性架构 | network (query) | - | -| GET | `/api/v1/getservicearea/` | 获取服务区信息 | network (query), id (query) | - | -| GET | `/api/v1/getserviceareaschema/` | 获取服务区属性架构 | network (query) | - | -| GET | `/api/v1/getvirtualdistrict/` | 获取虚拟分区信息 | network (query), id (query) | - | -| GET | `/api/v1/getvirtualdistrictschema/` | 获取虚拟分区属性架构 | network (query) | - | -| POST | `/api/v1/setdistrictmeteringarea/` | 设置DMA属性 | network (query) | - | -| POST | `/api/v1/setregion/` | 设置区域属性 | network (query) | - | -| POST | `/api/v1/setservicearea/` | 设置服务区属性 | network (query) | - | -| POST | `/api/v1/setvirtualdistrict/` | 设置虚拟分区属性 | network (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getregionschema` | 返回区域(Region)数据模型的字段定义 | -| `GET /getregion/` | 查询单个区域的属性 | -| `GET /getalldistrictmeteringareas/` | 获取所有 DMA(独立计量区)列表 | -| `GET /getallserviceareas/` | 获取所有服务区列表 | -| `POST /addregion/` | 新增区域(需提供名称和节点/管道列表) | -| `POST /adddistrictmeteringarea/` | 新增 DMA 分区 | -| `POST /addvirtualdistrict/` | 新增虚拟分区 | -| `POST /addservicearea/` | 新增服务区 | -| `GET /calculatedistrictmeteringareafornodes/` | 为指定节点集合计算其所属 DMA | -| `GET /calculatedistrictmeteringareaforregion/` | 为指定区域内的所有节点计算 DMA 归属 | -| `GET /calculatedistrictmeteringareafornetwork/` | 为整个管网的所有节点计算 DMA 归属 | -| `GET /calculateservicearea/` | 计算服务区,返回全部时间步结果 | diff --git a/.opencode/skills/business/network-assets/reservoirs/SKILL.md b/.opencode/skills/business/network-assets/reservoirs/SKILL.md deleted file mode 100644 index 4b08ce2..0000000 --- a/.opencode/skills/business/network-assets/reservoirs/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: tjwater-action-business-network-assets-reservoirs -description: business/network-assets 下 reservoirs 操作技能。 -version: 3.0.0 ---- - -# reservoirs Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `reservoirs` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/addreservoir/` | 添加水库 | network (query), reservoir (query), x (query), y (query), head (query) | - | -| POST | `/api/v1/deletereservoir/` | 删除水库 | network (query), reservoir (query) | - | -| GET | `/api/v1/getallreservoirproperties/` | 获取所有水库属性 | network (query) | - | -| GET | `/api/v1/getreservoircoord/` | 获取水库坐标 | network (query), reservoir (query) | - | -| GET | `/api/v1/getreservoirhead/` | 获取水库水头 | network (query), reservoir (query) | - | -| GET | `/api/v1/getreservoirpattern/` | 获取水库模式 | network (query), reservoir (query) | - | -| GET | `/api/v1/getreservoirproperties/` | 获取水库属性 | network (query), reservoir (query) | - | -| GET | `/api/v1/getreservoirschema` | 获取水库模式 | network (query) | - | -| GET | `/api/v1/getreservoirx/` | 获取水库X坐标 | network (query), reservoir (query) | - | -| GET | `/api/v1/getreservoiry/` | 获取水库Y坐标 | network (query), reservoir (query) | - | -| POST | `/api/v1/setreservoircoord/` | 设置水库坐标 | network (query), reservoir (query), x (query), y (query) | - | -| POST | `/api/v1/setreservoirhead/` | 设置水库水头 | network (query), reservoir (query), head (query) | - | -| POST | `/api/v1/setreservoirpattern/` | 设置水库模式 | network (query), reservoir (query), pattern (query) | - | -| POST | `/api/v1/setreservoirproperties/` | 设置水库属性 | network (query), reservoir (query) | - | -| POST | `/api/v1/setreservoirx/` | 设置水库X坐标 | network (query), reservoir (query), x (query) | - | -| POST | `/api/v1/setreservoiry/` | 设置水库Y坐标 | network (query), reservoir (query), y (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getreservoirschema` | 返回水库(Reservoir)数据模型的所有字段定义 | -| `GET /getreservoirproperties/` | 查询单个水库的所有属性(水头、模式、坐标等) | -| `GET /getallreservoirproperties/` | 批量获取管网中所有水库的属性列表 | -| `GET /getreservoirhead/` | 查询水库水头(即水库水位高度) | -| `GET /getreservoirpattern/` | 查询水库关联的时间模式名称 | -| `POST /addreservoir/` | 向管网添加一个新水库,需提供ID、坐标和水头 | -| `POST /deletereservoir/` | 从管网删除指定水库 | -| `POST /setreservoir*/` | 设置水库某个具体属性(水头、模式、坐标等) | diff --git a/.opencode/skills/business/network-assets/tags/SKILL.md b/.opencode/skills/business/network-assets/tags/SKILL.md deleted file mode 100644 index aa5ef22..0000000 --- a/.opencode/skills/business/network-assets/tags/SKILL.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: tjwater-action-business-network-assets-tags -description: business/network-assets 下 tags 操作技能。 -version: 3.0.0 ---- - -# tags Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `tags` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/gettag/` | 获取标签信息 | network (query), t_type (query), id (query) | - | -| GET | `/api/v1/gettags/` | 获取所有标签 | network (query) | - | -| GET | `/api/v1/gettagschema/` | 获取标签属性架构 | network (query) | - | -| POST | `/api/v1/settag/` | 设置标签 | network (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /gettagschema/` | 返回标签(Tag)数据模型的字段定义 | -| `GET /gettag/` | 查询单个元素绑定的标签信息 | -| `GET /gettags/` | 获取管网中所有标签列表 | -| `POST /settag/` | 为管网元素设置/更新标签(支持自定义键值对属性) | diff --git a/.opencode/skills/business/network-assets/tanks/SKILL.md b/.opencode/skills/business/network-assets/tanks/SKILL.md deleted file mode 100644 index adb4e22..0000000 --- a/.opencode/skills/business/network-assets/tanks/SKILL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: tjwater-action-business-network-assets-tanks -description: business/network-assets 下 tanks 操作技能。 -version: 3.0.0 ---- - -# tanks Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `tanks` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/addtank/` | 新增水箱 | network (query), tank (query), x (query), y (query), elevation (query) | init_level (query), min_level (query), max_level (query), diameter (query), min_vol (query) | -| POST | `/api/v1/deletetank/` | 删除水箱 | network (query), tank (query) | - | -| GET | `/api/v1/getalltankproperties/` | 获取所有水箱属性 | network (query) | - | -| GET | `/api/v1/gettankcoord/` | 获取水箱坐标 | network (query), tank (query) | - | -| GET | `/api/v1/gettankdiameter/` | 获取水箱直径 | network (query), tank (query) | - | -| GET | `/api/v1/gettankelevation/` | 获取水箱标高 | network (query), tank (query) | - | -| GET | `/api/v1/gettankinitlevel/` | 获取水箱初始水位 | network (query), tank (query) | - | -| GET | `/api/v1/gettankmaxlevel/` | 获取水箱最大水位 | network (query), tank (query) | - | -| GET | `/api/v1/gettankminlevel/` | 获取水箱最小水位 | network (query), tank (query) | - | -| GET | `/api/v1/gettankminvol/` | 获取水箱最小体积 | network (query), tank (query) | - | -| GET | `/api/v1/gettankoverflow/` | 获取水箱溢流口 | network (query), tank (query) | - | -| GET | `/api/v1/gettankproperties/` | 获取水箱属性 | network (query), tank (query) | - | -| GET | `/api/v1/gettankschema` | 获取水箱模式 | network (query) | - | -| GET | `/api/v1/gettankvolcurve/` | 获取水箱容积曲线 | network (query), tank (query) | - | -| GET | `/api/v1/gettankx/` | 获取水箱X坐标 | network (query), tank (query) | - | -| GET | `/api/v1/gettanky/` | 获取水箱Y坐标 | network (query), tank (query) | - | -| POST | `/api/v1/settankcoord/` | 设置水箱坐标 | network (query), tank (query), x (query), y (query) | - | -| POST | `/api/v1/settankdiameter/` | 设置水箱直径 | network (query), tank (query), diameter (query) | - | -| POST | `/api/v1/settankelevation/` | 设置水箱标高 | network (query), tank (query), elevation (query) | - | -| POST | `/api/v1/settankinitlevel/` | 设置水箱初始水位 | network (query), tank (query), init_level (query) | - | -| POST | `/api/v1/settankmaxlevel/` | 设置水箱最大水位 | network (query), tank (query), max_level (query) | - | -| POST | `/api/v1/settankminlevel/` | 设置水箱最小水位 | network (query), tank (query), min_level (query) | - | -| POST | `/api/v1/settankminvol/` | 设置水箱最小体积 | network (query), tank (query), min_vol (query) | - | -| POST | `/api/v1/settankoverflow/` | 设置水箱溢流口 | network (query), tank (query), overflow (query) | - | -| POST | `/api/v1/settankproperties/` | 设置水箱属性 | network (query), tank (query) | - | -| POST | `/api/v1/settankvolcurve/` | 设置水箱容积曲线 | network (query), tank (query), vol_curve (query) | - | -| POST | `/api/v1/settankx/` | 设置水箱X坐标 | network (query), tank (query), x (query) | - | -| POST | `/api/v1/settanky/` | 设置水箱Y坐标 | network (query), tank (query), y (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /gettankschema` | 返回水箱(Tank)数据模型的所有字段定义 | -| `GET /gettankproperties/` | 查询单个水箱的所有属性(标高、直径、初始/最大/最小水位等) | -| `GET /getalltankproperties/` | 批量获取管网中所有水箱的属性列表 | -| `GET /gettankelevation/` | 查询水箱底部标高 | -| `GET /gettankdiameter/` | 查询水箱直径 | -| `GET /gettankinitlevel/` | 查询水箱初始水位 | -| `POST /addtank/` | 向管网添加一个新水箱,需提供ID、坐标、标高和水位参数 | -| `POST /deletetank/` | 从管网删除指定水箱 | -| `POST /settank*/` | 设置水箱某个具体属性(坐标、标高、直径、水位等) | diff --git a/.opencode/skills/business/network-assets/valves/SKILL.md b/.opencode/skills/business/network-assets/valves/SKILL.md deleted file mode 100644 index 3c78b3f..0000000 --- a/.opencode/skills/business/network-assets/valves/SKILL.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: tjwater-action-business-network-assets-valves -description: business/network-assets 下 valves 操作技能。 -version: 3.0.0 ---- - -# valves Action Skill - -## 简介 -负责 `business/network-assets` 场景下 `valves` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/addvalve/` | 添加阀门 | network (query), valve (query), node1 (query), node2 (query) | diameter (query), v_type (query), setting (query), minor_loss (query) | -| POST | `/api/v1/deletevalve/` | 删除阀门 | network (query), valve (query) | - | -| GET | `/api/v1/getallvalveproperties/` | 获取所有阀门属性 | network (query) | - | -| GET | `/api/v1/getvalvediameter/` | 获取阀门直径 | network (query), valve (query) | - | -| GET | `/api/v1/getvalveminorloss/` | 获取阀门损失系数 | network (query), valve (query) | - | -| GET | `/api/v1/getvalvenode1/` | 获取阀门起点节点 | network (query), valve (query) | - | -| GET | `/api/v1/getvalvenode2/` | 获取阀门终点节点 | network (query), valve (query) | - | -| GET | `/api/v1/getvalveproperties/` | 获取阀门所有属性 | network (query), valve (query) | - | -| GET | `/api/v1/getvalveschema` | 获取阀门架构 | network (query) | - | -| GET | `/api/v1/getvalvesetting/` | 获取阀门开度 | network (query), valve (query) | - | -| GET | `/api/v1/getvalvetype/` | 获取阀门类型 | network (query), valve (query) | - | -| POST | `/api/v1/setvalvenode1/` | 设置阀门起点节点 | network (query), valve (query), node1 (query) | - | -| POST | `/api/v1/setvalvenode2/` | 设置阀门终点节点 | network (query), valve (query), node2 (query) | - | -| POST | `/api/v1/setvalvenodediameter/` | 设置阀门直径 | network (query), valve (query), diameter (query) | - | -| POST | `/api/v1/setvalveproperties/` | 批量设置阀门属性 | network (query), valve (query) | - | -| POST | `/api/v1/setvalvesetting/` | 设置阀门开度 | network (query), valve (query), setting (query) | - | -| POST | `/api/v1/setvalvetype/` | 设置阀门类型 | network (query), valve (query), type (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getvalveschema` | 返回阀门(Valve)数据模型的所有字段定义 | -| `GET /getvalveproperties/` | 查询单个阀门的所有属性(管径、类型、设定值等) | -| `GET /getallvalveproperties/` | 批量获取管网中所有阀门的属性列表 | -| `GET /getvalvediameter/` | 查询阀门管径 | -| `GET /getvalvetype/` | 查询阀门类型(PRV/PSV/TCV/FCV/PBV/GPV) | -| `GET /getvalvesetting/` | 查询阀门设定值(压力设定或流量设定) | -| `POST /addvalve/` | 向管网添加一个新阀门,需提供ID、起终节点、管径和类型 | -| `POST /deletevalve/` | 从管网删除指定阀门 | -| `POST /setvalve*/` | 设置阀门某个具体属性(类型、设定值、管径等) | diff --git a/.opencode/skills/business/project-workspace/SKILL.md b/.opencode/skills/business/project-workspace/SKILL.md deleted file mode 100644 index dea0ee6..0000000 --- a/.opencode/skills/business/project-workspace/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: tjwater-scenario-business-project-workspace -description: 负责项目空间、快照与扩展操作。 -version: 3.0.0 ---- - -# project-workspace Scenario Skill - -## 简介 -负责项目空间、快照与扩展操作。 - -## 子模块索引 (渐进式引导) -- **extension**: 见 `./extension/SKILL.md` -- **misc**: 见 `./misc/SKILL.md` -- **project**: 见 `./project/SKILL.md` -- **project_data**: 见 `./project_data/SKILL.md` -- **schemes**: 见 `./schemes/SKILL.md` -- **snapshots**: 见 `./snapshots/SKILL.md` - diff --git a/.opencode/skills/business/project-workspace/extension/SKILL.md b/.opencode/skills/business/project-workspace/extension/SKILL.md deleted file mode 100644 index 0de69cf..0000000 --- a/.opencode/skills/business/project-workspace/extension/SKILL.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: tjwater-action-business-project-workspace-extension -description: business/project-workspace 下 extension 操作技能。 -version: 3.0.0 ---- - -# extension Action Skill - -## 简介 -负责 `business/project-workspace` 场景下 `extension` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/getallextensiondata/` | 获取所有扩展数据 | network (query) | - | -| GET | `/api/v1/getallextensiondatakeys/` | 获取所有扩展数据键 | network (query) | - | -| GET | `/api/v1/getextensiondata/` | 获取指定扩展数据 | network (query), key (query) | - | -| POST | `/api/v1/setextensiondata/` | 设置扩展数据 | network (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getallextensiondatakeys/` | 获取当前管网中所有已存储的自定义扩展数据的键名列表 | -| `GET /getallextensiondata/` | 获取当前管网所有自定义扩展数据(键值对集合) | -| `GET /getextensiondata/` | 按 key 查询指定的自定义扩展数据值 | -| `POST /setextensiondata/` | 设置或更新一个自定义扩展数据键值对(可用于存储任意业务自定义信息) | diff --git a/.opencode/skills/business/project-workspace/misc/SKILL.md b/.opencode/skills/business/project-workspace/misc/SKILL.md deleted file mode 100644 index 23118b8..0000000 --- a/.opencode/skills/business/project-workspace/misc/SKILL.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: tjwater-action-business-project-workspace-misc -description: business/project-workspace 下 misc 操作技能。 -version: 3.0.0 ---- - -# misc Action Skill - -## 简介 -负责 `business/project-workspace` 场景下 `misc` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/getallburstlocateresults/` | 获取所有爆管定位结果 | network (query) | - | -| GET | `/api/v1/getallsensorplacements/` | 获取所有传感器位置 | network (query) | - | -| GET | `/api/v1/getjson/` | 获取JSON示例 | - | - | -| GET | `/api/v1/getrealtimedata/` | 获取实时数据 | - | - | -| GET | `/api/v1/getsimulationresult/` | 获取模拟结果 | - | - | -| POST | `/api/v1/test_dict/` | 测试字典处理 | data (body) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getallburstlocateresults/` | 获取当前管网所有历史爆管定位分析结果(旧版接口) | -| `GET /getallsensorplacements/` | 获取当前管网所有传感器布置方案的结果列表 | -| `GET /getsimulationresult/` | 获取最近一次水力模拟结果(旧版接口) | -| `GET /getrealtimedata/` | 获取管网实时监测数据(旧版接口) | -| `GET /getjson/` | 返回示例 JSON 数据结构,用于开发调试 | -| `POST /test_dict/` | 测试字典类型请求体的接口,用于开发调试 | diff --git a/.opencode/skills/business/project-workspace/project-data/SKILL.md b/.opencode/skills/business/project-workspace/project-data/SKILL.md deleted file mode 100644 index e5e1ea2..0000000 --- a/.opencode/skills/business/project-workspace/project-data/SKILL.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: tjwater-action-business-project-workspace-project-data -description: business/project-workspace 下 project_data 操作技能。 -version: 3.0.0 ---- - -# project_data Action Skill - -## 简介 -负责 `business/project-workspace` 场景下 `project_data` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/burst-locate-result` | 获取爆管定位结果 | - | - | -| GET | `/api/v1/burst-locate-result/{burst_incident}` | 按事件查询爆管定位结果 | burst_incident (path) | - | -| GET | `/api/v1/scada-info` | 获取SCADA信息 | - | - | -| GET | `/api/v1/scheme-list` | 获取方案列表 | - | - | - -- 覆盖方法:`GET` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /burst-locate-result` | 获取所有爆管定位事件的历史结果列表(新版REST接口) | -| `GET /burst-locate-result/{burst_incident}` | 查询指定爆管事件(burst_incident ID)的详细定位结果 | -| `GET /scada-info` | 获取当前项目关联的 SCADA 设备和监测点信息汇总 | -| `GET /scheme-list` | 获取当前项目中所有可用的水力计算方案列表 | diff --git a/.opencode/skills/business/project-workspace/project/SKILL.md b/.opencode/skills/business/project-workspace/project/SKILL.md deleted file mode 100644 index f7ba55d..0000000 --- a/.opencode/skills/business/project-workspace/project/SKILL.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: tjwater-action-business-project-workspace-project -description: business/project-workspace 下 project 操作技能。 -version: 3.0.0 ---- - -# project Action Skill - -## 简介 -负责 `business/project-workspace` 场景下 `project` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/listprojects/` | 获取项目列表 | - | - | -| GET | `/api/v1/project_info/` | 获取项目信息 | network (query) | - | -| GET | `/api/v1/haveproject/` | 检查项目是否存在 | network (query) | - | -| POST | `/api/v1/createproject/` | 创建新项目 | network (query) | - | -| POST | `/api/v1/deleteproject/` | 删除项目 | network (query) | - | -| POST | `/api/v1/copyproject/` | 复制项目 | source (query), target (query) | - | -| GET | `/api/v1/isprojectopen/` | 检查项目是否已打开 | network (query) | - | -| POST | `/api/v1/openproject/` | 打开项目 | network (query) | - | -| POST | `/api/v1/closeproject/` | 关闭项目 | network (query) | - | -| GET | `/api/v1/isprojectlocked/` | 检查项目是否被锁定 | network (query) | - | -| GET | `/api/v1/isprojectlockedbyme/` | 检查项目是否被当前用户锁定 | network (query) | - | -| POST | `/api/v1/lockproject/` | 锁定项目 | network (query) | - | -| POST | `/api/v1/unlockproject/` | 解锁项目 | network (query) | - | -| POST | `/api/v1/importinp/` | 导入 INP 文件内容 | network (query) | - | -| GET | `/api/v1/exportinp/` | 导出项目为 ChangeSet | network (query), version (query) | - | -| POST | `/api/v1/readinp/` | 读取 INP 文件到项目 | network (query), inp (query) | - | -| GET | `/api/v1/dumpinp/` | 导出项目到 INP 文件 | network (query), inp (query) | - | -| POST | `/api/v1/uploadinp/` | 上传 INP 文件 | afile (body), name (query) | - | -| GET | `/api/v1/downloadinp/` | 下载 INP 文件 | name (query) | - | -| GET | `/api/v1/convertv3tov2/` | 转换 INP V3 为 V2 | - | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /listprojects/` | 获取服务器上所有可用的供水管网项目名称列表 | -| `GET /project_info/` | 从数据库获取项目的详细信息,包括地图范围等配置 | -| `POST /createproject/` | 创建一个新的供水管网项目;若已存在可能覆盖或报错 | -| `POST /deleteproject/` | 永久删除指定项目,此操作不可恢复 | -| `POST /openproject/` | 将指定项目加载到内存并初始化数据库连接池 | -| `POST /closeproject/` | 将指定项目从内存中卸载,释放相关资源 | -| `POST /lockproject/` | 锁定项目以防止并发修改 | -| `POST /unlockproject/` | 释放对项目的锁定 | -| `POST /importinp/` | 将 INP 格式文本内容导入到指定项目 | -| `GET /exportinp/` | 导出项目变更集(ChangeSet),含顶点、SCADA元素、DMA、SA、VD等 | -| `GET /convertv3tov2/` | 将 EPANET 3.0 格式的 INP 内容转换为 2.x 格式 | diff --git a/.opencode/skills/business/project-workspace/schemes/SKILL.md b/.opencode/skills/business/project-workspace/schemes/SKILL.md deleted file mode 100644 index 9c0a428..0000000 --- a/.opencode/skills/business/project-workspace/schemes/SKILL.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: tjwater-action-business-project-workspace-schemes -description: business/project-workspace 下 schemes 操作技能。 -version: 3.0.0 ---- - -# schemes Action Skill - -## 简介 -负责 `business/project-workspace` 场景下 `schemes` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/getallschemes/` | 获取所有方案 | network (query) | - | -| GET | `/api/v1/getscheme/` | 获取单个方案 | network (query), schema_name (query) | - | -| GET | `/api/v1/getschemeschema/` | 获取方案模式 | network (query) | - | - -- 覆盖方法:`GET` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getschemeschema/` | 返回方案(Scheme)数据模型的字段定义 | -| `GET /getallschemes/` | 获取当前管网下所有已保存方案的列表 | -| `GET /getscheme/` | 查询指定方案名称(schema_name)的详细属性和配置 | diff --git a/.opencode/skills/business/project-workspace/snapshots/SKILL.md b/.opencode/skills/business/project-workspace/snapshots/SKILL.md deleted file mode 100644 index f01f252..0000000 --- a/.opencode/skills/business/project-workspace/snapshots/SKILL.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: tjwater-action-business-project-workspace-snapshots -description: business/project-workspace 下 snapshots 操作技能。 -version: 3.0.0 ---- - -# snapshots Action Skill - -## 简介 -负责 `business/project-workspace` 场景下 `snapshots` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/batch/` | 执行批量命令 | network (query) | - | -| POST | `/api/v1/compressedbatch/` | 执行压缩批量命令 | network (query) | - | -| GET | `/api/v1/getcurrentoperationid/` | 获取当前操作ID | network (query) | - | -| GET | `/api/v1/getrestoreoperation/` | 获取恢复操作ID | network (query) | - | -| GET | `/api/v1/getsnapshots/` | 获取快照列表 | network (query) | - | -| GET | `/api/v1/havesnapshot/` | 检查快照是否存在 | network (query), tag (query) | - | -| GET | `/api/v1/havesnapshotforcurrentoperation/` | 检查当前操作快照是否存在 | network (query) | - | -| GET | `/api/v1/havesnapshotforoperation/` | 检查操作快照是否存在 | network (query), operation (query) | - | -| POST | `/api/v1/pickoperation/` | 选择操作 | network (query), operation (query) | discard (query) | -| POST | `/api/v1/picksnapshot/` | 选择快照 | network (query), tag (query) | discard (query) | -| POST | `/api/v1/redo/` | 重做操作 | network (query) | - | -| POST | `/api/v1/setrestoreoperation/` | 设置恢复操作ID | network (query), operation (query) | - | -| GET | `/api/v1/syncwithserver/` | 与服务器同步 | network (query), operation (query) | - | -| POST | `/api/v1/takenapshotforcurrentoperation` | 为当前操作创建快照(兼容模式) | network (query), tag (query) | - | -| POST | `/api/v1/takesnapshot/` | 创建快照 | network (query), tag (query) | - | -| POST | `/api/v1/takesnapshotforcurrentoperation` | 为当前操作创建快照 | network (query), tag (query) | - | -| POST | `/api/v1/takesnapshotforoperation/` | 为操作创建快照 | network (query), operation (query), tag (query) | - | -| POST | `/api/v1/undo/` | 撤销操作 | network (query) | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /getsnapshots/` | 列出当前管网所有已保存的快照标签(tag)列表 | -| `GET /havesnapshot/` | 检查指定 tag 的快照是否存在 | -| `POST /takesnapshot/` | 保存当前管网状态为一个快照,tag 为快照名称 | -| `POST /picksnapshot/` | 将管网状态回滚到指定快照,discard=true 时丢弃当前未保存修改 | -| `GET /getcurrentoperationid/` | 获取当前管网的操作ID(用于追踪操作历史) | -| `POST /undo/` | 撤销对管网的最近一次操作 | -| `POST /redo/` | 重做上一次被撤销的操作 | -| `POST /batch/` | 批量执行多个管网操作命令(原子事务) | -| `POST /compressedbatch/` | 执行压缩格式的批量命令(减少网络传输量) | -| `GET /syncwithserver/` | 将客户端的操作与服务端管网状态同步 | -| `POST /pickoperation/` | 切换到指定 operation ID 的历史操作状态 | -| `POST /takesnapshotforcurrentoperation` | 为当前 operation 创建快照(保存当前操作节点状态) | diff --git a/.opencode/skills/data/SKILL.md b/.opencode/skills/data/SKILL.md deleted file mode 100644 index d20a6a5..0000000 --- a/.opencode/skills/data/SKILL.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: tjwater-domain-data -description: 负责时序数据访问与读写能力。 -version: 3.0.0 ---- - -# Data Domain Skill - -## 简介 -负责时序数据访问与读写能力。 - -## 子模块索引 (渐进式引导) -- **timeseries-access**: 见 `./timeseries-access/SKILL.md` - diff --git a/.opencode/skills/data/timeseries-access/SKILL.md b/.opencode/skills/data/timeseries-access/SKILL.md deleted file mode 100644 index 3a08c56..0000000 --- a/.opencode/skills/data/timeseries-access/SKILL.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: tjwater-scenario-data-timeseries-access -description: 负责时序数据查询、写入与聚合。 -version: 3.0.0 ---- - -# timeseries-access Scenario Skill - -## 简介 -负责时序数据查询、写入与聚合。 - -## 子模块索引 (渐进式引导) -- **composite**: 见 `./composite/SKILL.md` -- **realtime**: 见 `./realtime/SKILL.md` -- **scheme**: 见 `./scheme/SKILL.md` - diff --git a/.opencode/skills/data/timeseries-access/composite/SKILL.md b/.opencode/skills/data/timeseries-access/composite/SKILL.md deleted file mode 100644 index 2fd3e89..0000000 --- a/.opencode/skills/data/timeseries-access/composite/SKILL.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: tjwater-action-data-timeseries-access-composite -description: data/timeseries-access 下 composite 操作技能。 -version: 3.0.0 ---- - -# composite Action Skill - -## 简介 -负责 `data/timeseries-access` 场景下 `composite` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/composite/clean-scada` | 清洗SCADA监测数据 | device_ids (query), start_time (query), end_time (query) | - | -| GET | `/api/v1/composite/element-scada` | 获取管网元素关联的SCADA监测数据 | element_id (query), start_time (query), end_time (query) | use_cleaned (query) | -| GET | `/api/v1/composite/element-simulation` | 获取管网元素的模拟数据 | start_time (query), end_time (query), feature_infos (query) | scheme_type (query), scheme_name (query) | -| GET | `/api/v1/composite/pipeline-health-prediction` | 预测管道健康状况 | query_time (query), network_name (query) | - | -| GET | `/api/v1/composite/scada-simulation` | 获取SCADA关联的模拟数据 | start_time (query), end_time (query), device_ids (query) | scheme_type (query), scheme_name (query) | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /composite/scada-simulation` | 将 SCADA 设备历史监测数据与水力模拟结果对齐返回,便于压差分析 | -| `GET /composite/element-simulation` | 按管网元素ID和属性类型查询该元素在某方案下的模拟时序数据 | -| `GET /composite/element-scada` | 查询某管网元素关联的 SCADA 设备的历史监测时序数据,支持使用清洗后数据(use_cleaned) | -| `POST /composite/clean-scada` | 对指定设备在指定时间段的 SCADA 数据进行清洗处理(去噪、异常值替换) | -| `GET /composite/pipeline-health-prediction` | 在指定时刻对整个管网的管道健康状态进行预测,返回各管道健康评分 | diff --git a/.opencode/skills/data/timeseries-access/realtime/SKILL.md b/.opencode/skills/data/timeseries-access/realtime/SKILL.md deleted file mode 100644 index a572556..0000000 --- a/.opencode/skills/data/timeseries-access/realtime/SKILL.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: tjwater-action-data-timeseries-access-realtime -description: data/timeseries-access 下 realtime 操作技能。 -version: 3.0.0 ---- - -# realtime Action Skill - -## 简介 -负责 `data/timeseries-access` 场景下 `realtime` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| DELETE | `/api/v1/realtime/links` | 删除实时管道数据 | start_time (query), end_time (query) | - | -| GET | `/api/v1/realtime/links` | 查询实时管道数据 | start_time (query), end_time (query) | - | -| POST | `/api/v1/realtime/links/batch` | 批量插入实时管道数据 | data (body) | - | -| PATCH | `/api/v1/realtime/links/{link_id}/field` | 更新实时管道字段 | link_id (path), time (query), field (query), value (query) | - | -| DELETE | `/api/v1/realtime/nodes` | 删除实时节点数据 | start_time (query), end_time (query) | - | -| GET | `/api/v1/realtime/nodes` | 查询实时节点数据 | start_time (query), end_time (query) | - | -| POST | `/api/v1/realtime/nodes/batch` | 批量插入实时节点数据 | data (body) | - | -| GET | `/api/v1/realtime/query/by-id-time` | 按ID和时间查询实时模拟数据 | id (query), type (query), query_time (query) | - | -| GET | `/api/v1/realtime/query/by-time-property` | 按时间和属性查询实时数据 | query_time (query), type (query), property (query) | - | -| POST | `/api/v1/realtime/simulation/store` | 存储实时模拟结果 | node_result_list (body), link_result_list (body), result_start_time (query) | - | - -- 覆盖方法:`DELETE, GET, PATCH, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `POST /realtime/links/batch` | 批量将管道(link)模拟结果写入实时数据表(TimescaleDB) | -| `GET /realtime/links` | 查询指定时间范围内的实时管道模拟数据 | -| `DELETE /realtime/links` | 删除指定时间范围内的实时管道数据 | -| `PATCH /realtime/links/{link_id}/field` | 修改某条管道在特定时刻的某个字段值 | -| `POST /realtime/nodes/batch` | 批量将节点模拟结果写入实时数据表 | -| `GET /realtime/nodes` | 查询指定时间范围内的实时节点模拟数据 | -| `DELETE /realtime/nodes` | 删除指定时间范围内的实时节点数据 | -| `POST /realtime/simulation/store` | 一次性存储一次完整模拟运行的节点和管道结果(含起始时间) | -| `GET /realtime/query/by-time-property` | 按查询时间点和属性名(如 pressure/flow)查询全网实时模拟值 | -| `GET /realtime/query/by-id-time` | 按单个元素ID、类型和时间点查询其实时模拟值 | diff --git a/.opencode/skills/data/timeseries-access/scheme/SKILL.md b/.opencode/skills/data/timeseries-access/scheme/SKILL.md deleted file mode 100644 index 6c87330..0000000 --- a/.opencode/skills/data/timeseries-access/scheme/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: tjwater-action-data-timeseries-access-scheme -description: data/timeseries-access 下 scheme 操作技能。 -version: 3.0.0 ---- - -# scheme Action Skill - -## 简介 -负责 `data/timeseries-access` 场景下 `scheme` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| DELETE | `/api/v1/scheme/links` | 删除方案管道数据 | scheme_type (query), scheme_name (query), start_time (query), end_time (query) | - | -| GET | `/api/v1/scheme/links` | 查询方案管道数据 | scheme_type (query), scheme_name (query), start_time (query), end_time (query) | - | -| POST | `/api/v1/scheme/links/batch` | 批量插入方案管道数据 | data (body) | - | -| GET | `/api/v1/scheme/links/{link_id}/field` | 查询方案管道字段数据 | link_id (path), scheme_type (query), scheme_name (query), start_time (query), end_time (query), field (query) | - | -| PATCH | `/api/v1/scheme/links/{link_id}/field` | 更新方案管道字段 | link_id (path), scheme_type (query), scheme_name (query), time (query), field (query), value (query) | - | -| DELETE | `/api/v1/scheme/nodes` | 删除方案节点数据 | scheme_type (query), scheme_name (query), start_time (query), end_time (query) | - | -| POST | `/api/v1/scheme/nodes/batch` | 批量插入方案节点数据 | data (body) | - | -| GET | `/api/v1/scheme/nodes/{node_id}/field` | 查询方案节点字段数据 | node_id (path), scheme_type (query), scheme_name (query), start_time (query), end_time (query), field (query) | - | -| PATCH | `/api/v1/scheme/nodes/{node_id}/field` | 更新方案节点字段 | node_id (path), scheme_type (query), scheme_name (query), time (query), field (query), value (query) | - | -| GET | `/api/v1/scheme/query/by-id-time` | 按ID和时间查询方案模拟数据 | scheme_type (query), scheme_name (query), id (query), type (query), query_time (query) | - | -| GET | `/api/v1/scheme/query/by-scheme-time-property` | 按方案、时间和属性查询数据 | scheme_type (query), scheme_name (query), query_time (query), type (query), property (query) | - | -| POST | `/api/v1/scheme/simulation/store` | 存储方案模拟结果 | scheme_type (query), scheme_name (query), node_result_list (body), link_result_list (body), result_start_time (query) | - | - -- 覆盖方法:`DELETE, GET, PATCH, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `POST /scheme/links/batch` | 批量将管道模拟结果写入指定方案(scheme_type + scheme_name)的数据表 | -| `GET /scheme/links` | 查询指定方案和时间范围内的所有管道模拟数据 | -| `DELETE /scheme/links` | 删除指定方案和时间范围内的管道模拟数据 | -| `GET /scheme/links/{link_id}/field` | 查询特定管道在指定方案和时间范围内某字段的时序数据 | -| `PATCH /scheme/links/{link_id}/field` | 修改特定管道在指定方案某时刻的某个字段值 | -| `POST /scheme/nodes/batch` | 批量将节点模拟结果写入指定方案的数据表 | -| `GET /scheme/nodes/{node_id}/field` | 查询特定节点在指定方案和时间范围内某字段的时序数据 | -| `PATCH /scheme/nodes/{node_id}/field` | 修改特定节点在指定方案某时刻的某个字段值 | -| `DELETE /scheme/nodes` | 删除指定方案和时间范围内的节点模拟数据 | -| `POST /scheme/simulation/store` | 一次性存储完整方案模拟结果(节点 + 管道),需提供 scheme_type 和 scheme_name | -| `GET /scheme/query/by-id-time` | 按元素ID、类型和时间点查询该元素在指定方案下的模拟值 | -| `GET /scheme/query/by-scheme-time-property` | 按方案、时间点和属性名查询全网在指定方案下的模拟值 | diff --git a/.opencode/skills/examples.md b/.opencode/skills/examples.md deleted file mode 100644 index 6cf022d..0000000 --- a/.opencode/skills/examples.md +++ /dev/null @@ -1,191 +0,0 @@ -# 示例(基于 opencode Agent chat/stream 工具调用链) - -## 示例 1:前端发起对话,opencode agent 触发工具调用 - -用户意图:查询设备 `170490` 在时间范围内的 `monitored_value`。 - -前端调用 `POST /api/v1/agent/chat/stream`: - -```json -{ - "message": "请查询设备170490在最近24小时的monitored_value历史数据", - "session_id": "agent-demo-001" -} -``` - -请求头至少包含(由前端传入): -- `Authorization: Bearer ` -- `x-project-id: ` - -服务端内部行为: -- 持续通过 SSE `progress` 输出处理阶段,例如“正在规划分析步骤”“正在调用后端数据查询” -- opencode agent 选择工具 `dynamic_http_call` -- 工具参数示例: -```json -{ - "path": "/api/v1/scada/by-ids-field-time-range", - "method": "GET", - "arguments": { - "device_ids": "170490", - "field": "monitored_value", - "start_time": "2026-03-29T07:57:47.338Z", - "end_time": "2026-03-30T07:57:47.338Z" - } -} -``` - -## 示例 2:opencode agent 多步规划 + 多次工具调用 - -用户消息: -- “先查这个设备历史数据,再给我异常点摘要。” - -典型链路: -- 第一步工具调用:查询历史数据接口。 -- 第二步(可选)工具调用:查询补充数据接口。 -- opencode agent 汇总工具结果,持续通过 SSE 输出 `progress` 与 `token`,最终返回 `done`。 - -`progress` 示例: - -```json -{ - "session_id": "agent-demo-001", - "id": "tool-dynamic-http", - "phase": "tool", - "status": "running", - "title": "正在调用后端数据查询" -} -``` - -## 示例 3:前端工具 — 定位要素 - -用户消息: -- "帮我找到管道 P-001 和 P-002" - -opencode agent 调用工具 `locate_features`: -```json -{ - "ids": ["P-001", "P-002"], - "feature_type": "pipe" -} -``` - -前端收到 SSE 事件后缩放地图并高亮管道。opencode agent 回复文字:"已在地图上定位到管道 P-001 和 P-002。" - -## 示例 4:前端工具 — 对话内图表 - -用户消息: -- "展示节点 J-001 最近一天的压力变化曲线" - -典型链路: -1. opencode agent 先调用 `dynamic_http_call` 查询数据 -2. 拿到数据后,调用 `show_chart` 将处理好的数据传给前端渲染 - -第一步 — opencode agent 调用 `dynamic_http_call`: -```json -{ - "path": "/api/v1/composite/element-simulation", - "method": "GET", - "arguments": { - "feature_infos": "[\"J-001\", \"node\"]", - "start_time": "2026-03-29T00:00:00Z", - "end_time": "2026-03-30T00:00:00Z" - } -} -``` - -第二步 — opencode agent 处理数据后调用 `show_chart`: -```json -{ - "title": "节点 J-001 压力变化", - "chart_type": "line", - "x_data": ["03-29 00:00", "03-29 01:00", "03-29 02:00", "..."], - "series": [ - { - "name": "J-001 压力", - "data": [32.5, 31.8, 30.2, "..."] - } - ], - "y_axis_name": "压力 (m)" -} -``` - -前端直接用 AI 提供的数据渲染 ECharts 图表,不再请求后端。 - -## 示例 5:前端工具 — 查看 SCADA 监测面板 - -用户消息: -- "我想看看 J-001 的监测数据" - -opencode agent 调用工具 `view_scada`: -```json -{ - "device_ids": ["J-001"], - "start_time": "2026-03-29T00:00:00Z", - "end_time": "2026-03-30T00:00:00Z" -} -``` - -前端打开 SCADA 监测面板,展示该节点的历史监测曲线。 - -## 示例 6:记住用户长期偏好 - -用户消息: -- "以后回答尽量简洁,先给结论再解释。" - -opencode agent 调用工具 `memory_manager`: - -```json -{ - "action": "add", - "reason": "用户明确给出了长期有效的回答风格偏好,后续会话也应遵守。", - "scope": "user", - "content": "用户偏好先给结论、再补必要解释,整体风格尽量简洁。" -} -``` - -## 示例 7:检索历史案例而不是误写入 memory - -用户消息: -- "我们之前是不是分析过类似的爆管定位问题?" - -opencode agent 调用工具 `session_search`: - -```json -{ - "reason": "用户在询问过往会话中的类似案例,应先检索历史 transcript 而不是写入新的 memory。", - "query": "爆管定位 类似案例", - "max_results": 5 -} -``` - -## 示例 8:沉淀可复用 workflow 模式 - -用户消息: -- "这套瓶颈分析流程之后可以复用。" - -opencode agent 调用工具 `skill_manager`: - -```json -{ - "action": "append_pattern", - "reason": "本轮已验证一套稳定可复用的瓶颈分析 workflow,适合沉淀到已有 skill。", - "skill_path": "workflow/bottleneck-analysis", - "pattern": "当瓶颈分析依赖大体量属性数据和模拟结果时,先用 dynamic_http_call 获取 preview,再用 fetch_result_ref 回读完整数据后再做合并与排序。" -} -``` - -## 示例 9:给单个 workflow skill 写入可复用脚本 - -当某个 workflow 的本地 Python 处理逻辑已经稳定、未来同类任务会重复使用时,可写入该 skill 自己的 `scripts/*.py`: - -```json -{ - "action": "write_script", - "reason": "本轮已验证瓶颈分析中的合并与排序脚本,后续同类 workflow 可直接复用。", - "skill_path": "workflow/bottleneck-analysis", - "file_path": "scripts/merge_and_rank.py", - "content": "import json\n\n\ndef rank_links(rows):\n return sorted(rows, key=lambda row: row['composite_score'], reverse=True)\n" -} -``` - -脚本应只归属当前 `skill_path`,不要写到 `data/` 或其他 skill 目录。 diff --git a/.opencode/skills/platform/SKILL.md b/.opencode/skills/platform/SKILL.md deleted file mode 100644 index fef60b8..0000000 --- a/.opencode/skills/platform/SKILL.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: tjwater-domain-platform -description: 负责治理、审计、缓存与元数据能力。 -version: 3.0.0 ---- - -# Platform Domain Skill - -## 简介 -负责治理、审计、缓存与元数据能力。 - -## 子模块索引 (渐进式引导) -- **governance-observability**: 见 `./governance-observability/SKILL.md` - diff --git a/.opencode/skills/platform/governance-observability/SKILL.md b/.opencode/skills/platform/governance-observability/SKILL.md deleted file mode 100644 index 2961a8d..0000000 --- a/.opencode/skills/platform/governance-observability/SKILL.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: tjwater-scenario-platform-governance-observability -description: 负责审计、缓存与平台元数据。 -version: 3.0.0 ---- - -# governance-observability Scenario Skill - -## 简介 -负责审计、缓存与平台元数据。 - -## 子模块索引 (渐进式引导) -- **audit**: 见 `./audit/SKILL.md` -- **cache**: 见 `./cache/SKILL.md` -- **meta**: 见 `./meta/SKILL.md` - diff --git a/.opencode/skills/platform/governance-observability/audit/SKILL.md b/.opencode/skills/platform/governance-observability/audit/SKILL.md deleted file mode 100644 index 5584922..0000000 --- a/.opencode/skills/platform/governance-observability/audit/SKILL.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: tjwater-action-platform-governance-observability-audit -description: platform/governance-observability 下 audit 操作技能。 -version: 3.0.0 ---- - -# audit Action Skill - -## 简介 -负责 `platform/governance-observability` 场景下 `audit` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/audit/logs` | 查询审计日志 | - | user_id (query), project_id (query), action (query), resource_type (query), start_time (query), end_time (query), skip (query), limit (query) | -| GET | `/api/v1/audit/logs/count` | 获取审计日志总数 | - | user_id (query), project_id (query), action (query), resource_type (query), start_time (query), end_time (query) | -| GET | `/api/v1/audit/logs/my` | 查询我的审计日志 | - | action (query), start_time (query), end_time (query), skip (query), limit (query) | - -- 覆盖方法:`GET` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /audit/logs` | 查询系统审计日志(仅管理员),支持按用户ID、项目ID、操作类型、资源类型、时间范围过滤;支持分页(skip/limit) | -| `GET /audit/logs/count` | 获取满足过滤条件的审计日志总条数,用于分页显示 | -| `GET /audit/logs/my` | 查询当前登录用户自己的操作日志,支持按操作类型和时间范围过滤 | diff --git a/.opencode/skills/platform/governance-observability/cache/SKILL.md b/.opencode/skills/platform/governance-observability/cache/SKILL.md deleted file mode 100644 index 1651d9a..0000000 --- a/.opencode/skills/platform/governance-observability/cache/SKILL.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: tjwater-action-platform-governance-observability-cache -description: platform/governance-observability 下 cache 操作技能。 -version: 3.0.0 ---- - -# cache Action Skill - -## 简介 -负责 `platform/governance-observability` 场景下 `cache` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| POST | `/api/v1/clearallredis/` | 清除所有缓存 | - | - | -| POST | `/api/v1/clearrediskey/` | 清除单个缓存键 | key (query) | - | -| POST | `/api/v1/clearrediskeys/` | 清除匹配的缓存键 | keys (query) | - | -| GET | `/api/v1/queryredis/` | 查询缓存键列表 | - | - | - -- 覆盖方法:`GET, POST` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /queryredis/` | 查询当前 Redis 中所有缓存键列表,用于检查缓存状态 | -| `POST /clearrediskey/` | 删除指定单个键的缓存(精确匹配) | -| `POST /clearrediskeys/` | 批量删除多个匹配的缓存键(支持模式匹配) | -| `POST /clearallredis/` | 清除 Redis 中所有缓存数据(慎用,会影响所有会话) | diff --git a/.opencode/skills/platform/governance-observability/meta/SKILL.md b/.opencode/skills/platform/governance-observability/meta/SKILL.md deleted file mode 100644 index 0f5b8e4..0000000 --- a/.opencode/skills/platform/governance-observability/meta/SKILL.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: tjwater-action-platform-governance-observability-meta -description: platform/governance-observability 下 meta 操作技能。 -version: 3.0.0 ---- - -# meta Action Skill - -## 简介 -负责 `platform/governance-observability` 场景下 `meta` 的具体接口调用。 - -## 子模块索引 (渐进式引导) -- 当前为叶子节点,直接使用下方接口目录。 - -## 接口目录 -| Method | Path | Summary | Required Params | Optional Params | -|---|---|---|---|---| -| GET | `/api/v1/meta/db/health` | 检查数据库健康状态 | - | - | -| GET | `/api/v1/meta/project` | 获取项目元数据 | - | - | -| GET | `/api/v1/meta/projects` | 列出用户项目 | - | - | - -- 覆盖方法:`GET` - -## 接口说明 - -| 接口 | 说明 | -|---|---| -| `GET /meta/db/health` | 检查数据库(PostgreSQL/TimescaleDB)连接健康状态,返回 ok/error | -| `GET /meta/project` | 获取当前用户当前项目的元数据(名称、创建时间、所有者等) | -| `GET /meta/projects` | 列出当前登录用户有权限访问的所有项目信息 | diff --git a/.opencode/skills/runbook.md b/.opencode/skills/runbook.md deleted file mode 100644 index 486b593..0000000 --- a/.opencode/skills/runbook.md +++ /dev/null @@ -1,125 +0,0 @@ -# API Skills 使用 Runbook(工具调用链) - -## 1) 总体原则 - -- Skills 负责“告诉模型可做什么”。 -- `chat/stream` 内部启动 opencode 会话,并注册工具 `dynamic_http_call`。 -- opencode agent 通过工具调用后端能力,不直接发 HTTP。 -- TJWaterAgent 执行器负责“代表当前用户调真实后端 API”(动态路径,无白名单)。 -- 会话完成后,运行时会基于 transcript 做后台 learning review;这一步用于判断是否需要更新 memory 或 skill,而不是替代主任务回答。 - -## 1.1) 自我学习闭环 - -- **memory_manager**:保存用户长期偏好 / 约束,以及稳定 workspace 事实 -- **skill_manager**:保存经过验证、可复用的 workflow / 方法 / pitfall -- **session_search**:检索当前用户 + 当前项目范围内的历史会话 transcript,用于回忆旧案例,避免把一次性案例写入 memory - -推荐分流: - -- 需要长期遵守的偏好 / 稳定事实 → `memory_manager` -- 可复用的方法、步骤、坑点 → `skill_manager` -- 某次分析过程、历史案例、临时结论 → `session_search` - -## 2) 请求入口(前端) - -- `POST /api/v1/agent/chat/stream`(唯一前端入口) - -不提供 `/execute` 对外调用路径,统一通过 `chat/stream` + 工具调用链执行。 - -请求体: - -```json -{ - "message": "帮我分析当前管网中的水力瓶颈管道,并给出改造建议", - "session_id": "agent-demo-001" -} -``` - -SSE 事件: - -| event | 用途 | 关键字段 | -| --- | --- | --- | -| `progress` | 展示 Agent 处理过程、规划和工具进度 | `session_id`, `id`, `phase`, `status`, `title`, `detail` | -| `token` | 渲染面向用户的最终回答文本 | `session_id`, `content` | -| `tool_call` | 驱动前端地图/面板/图表动作 | `session_id`, `tool`, `params` | -| `done` | 当前轮对话结束 | `session_id` | -| `error` | 当前轮失败 | `session_id`, `message`, `detail` | - -`progress.status` 取值为 `running`、`completed`、`error`;前端应按相同 `id` 覆盖更新同一条进度,而不是重复追加。 - -## 3) 工具参数约定(opencode agent 调用工具时) - -```json -{ - "path": "/api/v1/scada/by-ids-field-time-range", - "method": "GET", - "arguments": { - "device_ids": "170490", - "field": "monitored_value", - "start_time": "2026-03-29T07:57:47.338Z", - "end_time": "2026-03-30T07:57:47.338Z" - } -} -``` - -说明(工具 `dynamic_http_call`): -- `path` 必须以 `/` 开头。 -- `method` 支持:`GET/POST/PUT/PATCH/DELETE`。 -- `arguments` 会编码为 query 参数(列表会转为逗号拼接)。 - -## 3.1) 学习工具约定 - -- 所有学习类工具都必须带 `reason` -- `memory_manager` 支持:`add / list / replace / remove` -- `skill_manager` 支持:`list / append_pattern / remove_pattern / write_reference / remove_reference / write_script / remove_script` -- `session_search` 只搜索当前用户 + 当前项目作用域,不接受跨项目检索 -- `skill_manager` 的结构化写入优先落到: - 1. `## Learned Patterns` - 2. `references/*.md` - 3. `scripts/*.py` - 不应直接重写 skill frontmatter 或任意正文段落 -- `scripts/*.py` 仅表示当前 `skill_path` 私有的可复用脚本资产;不要把运行时临时脚本写进 `data/` - -## 4) 用户上下文注入(后端执行阶段) - -- `Authorization`(Bearer Token) -- `x-project-id` - -执行器会附带 `x-trace-id` 用于链路排查(可透传或自动生成)。 - -## 5) 排障要点 - -- `400`:检查工具参数 `path/method/arguments` 格式。 -- `401/403`:检查 token 与项目权限。 -- `404`:检查 `path` 是否正确。 -- `422`:检查 `arguments` 字段名与类型。 -- `5xx`:记录 `trace_id`,结合后端日志排查。 - -## 6) 前端工具调用链 - -前端工具(`locate_features`, `view_history`, `view_scada`, `show_chart`)的调用链与 `dynamic_http_call` 不同: - -``` -用户消息 → opencode agent → tool calling → 调用前端工具 (如 locate_features) - ↓ - tool handler: - 1) 推送 SSE tool_call 事件到前端 - 2) 返回简短确认给 opencode agent("已定位到管道") - ↓ -前端同时收到: - - SSE event: progress → 展示规划/工具执行/完成状态 - - SSE event: tool_call → 前端执行操作(定位地图/打开面板/渲染图表) - - SSE event: token → 渲染 opencode agent 文字回复 -``` - -关键区别: -- `dynamic_http_call`:TJWaterAgent 代理 HTTP 请求,结果返回给 opencode agent 做后续分析。 -- 前端工具:TJWaterAgent 仅推送 SSE 事件,前端直接执行,结果不返回 opencode agent。 -- `show_chart`:opencode agent 先通过 `dynamic_http_call` 查询数据,处理为 x_data + series 格式后调用 `show_chart`,前端直接渲染图表,不再请求后端。 - -## 7) 复盘与沉淀建议 - -- 复杂多工具任务完成后,优先判断是否产生了稳定 workflow,可写入 `skill_manager` -- 用户明确纠正表达风格、输出格式或步骤时,优先判断是否需要写入 `memory_manager` -- 如果你只是想确认“以前是不是处理过类似问题”,先用 `session_search` -- 如果结果仍然只是 preview,不要基于 preview 做 learned pattern,总是先 `fetch_result_ref` diff --git a/.opencode/skills/workflow/SKILL.md b/.opencode/skills/workflow/SKILL.md deleted file mode 100644 index 31fee6b..0000000 --- a/.opencode/skills/workflow/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: tjwater-workflow -description: 负责分析类工作流能力。 -version: 1.0.0 ---- - -# Workflow Domain Skill - -## 简介 -负责分析场景下的工作流组织与调用入口能力。 - -## 使用策略 - -- 当用户问题明显属于“多接口 + 本地分析 + 综合结论”的分析任务时,优先从本目录查找固定 workflow。 -- 如果找到合适 workflow,应先按 workflow 执行主路径,再补充缺少的原子 skill。 -- 如果没有匹配 workflow,或现有 workflow 缺少关键步骤、接口或输出约束,再回到其他 domain/scenario/action skills 组合能力。 - -## 子模块索引 (渐进式引导) -- **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 75fa8dd..0000000 --- a/.opencode/skills/workflow/bottleneck-analysis/SKILL.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -name: tjwater-workflow-bottleneck-analysis -description: workflow 下 bottleneck-analysis(水力瓶颈分析)工作流技能。 -version: 1.1.0 ---- - -# bottleneck-analysis Workflow Skill - -## 简介 -负责 `analytics/simulation-analysis` 场景下的水力瓶颈综合分析,通过结合管道属性与水力模拟结果,识别管网中超负荷、高流速、高水头损失的瓶颈管道,并给出分级改造建议。 - -## 前置依赖 -本工作流依赖以下两个数据源,需按顺序并行或串行获取: - -### 依赖 1:管道属性数据 -- 接口:`GET /api/v1/getallpipeproperties/` -- 参数:`network`(query,如 `tjwater`) -- 用途:获取全部管道的 id、管径(diameter)、长度(length)、粗糙度(roughness)、起端(node1)、终端(node2) 等属性 -- 注意:结果可能很大(数万条),需使用 `fetch_result_ref` 分批或全量获取 - -### 依赖 2:水力模拟结果 -- 接口:`GET /api/v1/runprojectreturndict/` -- 参数:`network`(query,如 `tjwater`) -- 用途:运行管网水力模拟,返回各管段的 flow(LPS)、velocity(m/s)、headloss(m)、status,以及各节点的 demand、head、pressure(KPA) -- 注意:结果可达 30MB+,需用 Python 脚本批量处理或使用 `fetch_result_ref` 回读 - -## 工作流步骤 - -### 第 1 步:并行获取管道属性和运行水力模拟 -同时调用 `getallpipeproperties` 和 `runprojectreturndict`,network 参数使用项目名称(如 `tjwater`)。 - -### 第 2 步:合并数据 -用 Python 脚本将管道属性的 pipe_id 与模拟结果的 link_id 进行关联,构建含以下字段的合并数据集: - -| 字段 | 来源 | 说明 | -|------|------|------| -| id | 两者关联键 | 管道/链路 ID | -| flow | 模拟 link_results | 流量 (LPS) | -| velocity | 模拟 link_results | 流速 (m/s) | -| headloss | 模拟 link_results | 水头损失 (m) | -| diameter | 管道属性 | 管径 (mm) | -| length | 管道属性 | 长度 (m) | -| roughness | 管道属性 | 粗糙度系数 | -| node1 / node2 | 管道属性 | 起端/终端节点 ID | -| unit_headloss | 计算 | headloss / length (m/m) | -| capacity_ratio | 计算 | |flow| / (π×(d/2000)²×1000),即实际流量与 1m/s 设计流量的比值 | - -同时从模拟 node_results 提取各节点 pressure,关联到管段两端。 - -### 第 3 步:多维度瓶颈识别 -按以下 5 个维度分别排序筛选,交叉印证: - -| 维度 | 筛选条件 | 指示含义 | -|------|---------|---------| -| 高流速 | velocity > 1.2 m/s | 管径不足 | -| 主干管高流量 | diameter ≥ 300mm 且 velocity > 0.5 m/s | 传输瓶颈 | -| 高水头损失 | headloss > 5m 且 0.3 < velocity < 1.5 m/s | 能耗瓶颈/粗糙度问题 | -| 高单位水头损失 | unit_headloss > 1.0 m/m | 严重局部瓶颈 | -| 超负荷 | capacity_ratio > 1.0 | 实际流量超过设计能力 | - -排除极短管道(length < 0.5m)以减少噪声。 - -### 第 4 步:综合评分 -对有效管道计算综合瓶颈分数: - -``` -composite_score = (velocity / max_velocity) × 0.4 - + (headloss / max_headloss) × 0.3 - + (capacity_ratio / max_capacity_ratio) × 0.3 -``` - -取 TOP 10~20 作为最严重瓶颈管道。 - -### 第 5 步:前端可视化 -- 使用 `show_chart` 展示流速分布柱状图 -- 使用 `locate_features` 在地图上定位 TOP 瓶颈管道(feature_type=pipe) -- 可选:使用 `view_history` 查看瓶颈管道的历史运行数据 -- 前端工具仅用于展示,分析结论必须来自 `dynamic_http_call` / `fetch_result_ref` 获得的数据 - -### 第 6 步:给出分级改造建议 -按严重程度分为三级: - -- **🚨 紧急**:综合评分 > 0.3,立即安排管径升级 -- **⚡ 重点**:综合评分 0.15~0.3,纳入近期改造计划 -- **📋 关注**:综合评分 0.05~0.15 或单维度超标,持续监测 - -每条建议含:当前管径 → 建议管径(基于目标流速 1.0~1.5 m/s 反推),并附改造理由。 - -## 改造管径计算公式 -``` -建议管径(mm) = 2 × 1000 × sqrt(|flow| / (π × target_velocity × 1000)) -``` -目标流速:DN<300 取 1.0 m/s,DN≥300 取 1.2 m/s。 - -## 证据约束 - -- 如果关键数据仍处于 preview 状态,不得直接输出最终瓶颈结论 -- 如果模拟结果不完整或接口失败,应明确说明当前仅能做初步筛查 -- 改造建议必须区分“数据直接支持的结论”和“工程经验推断” - -## 推荐输出结构 - -1. 分析范围与数据来源 -2. 主要瓶颈管段 Top N -3. 分级建议(紧急 / 重点 / 关注) -4. 假设与局限 -5. 是否建议地图定位或图表展示 - -## 参考 -- 管道属性操作:`../business/network-assets/pipes/SKILL.md` -- 模拟操作:`./simulation/SKILL.md` -- 节点属性操作:`../business/network-assets/junctions/SKILL.md` - -## Learned Patterns -- 先按“属性数据获取 → 模拟结果获取 → 本地关联 → 多指标筛选 → 分级建议”拆解工作流,再组织展示步骤,避免把一次分析过程写成会话流水账。 -- 结果集较大时,优先使用 `fetch_result_ref` 或本地脚本批处理;只要数据仍是 preview、截断或未完整回读,就不能直接输出 Top N 瓶颈结论。 -- 关联前先统一关键字段和单位:`pipe_id/link_id`、`diameter(mm)`、`length(m)`、`flow(LPS)`、`pressure(KPA)`;字段未对齐时,后续 ranking 和建议都会失真。 -- `unit_headloss`、`capacity_ratio` 等衍生指标应在过滤异常数据(如 `length < 0.5m` 的短管)后再计算,否则容易被极端值放大。 -- 阈值和评分权重应视为可调启发式,而不是唯一真理;输出时要区分“数据直接支持的结论”和“工程经验推断的建议”。 -- 地图定位、图表展示属于证据呈现层,不能替代分析层;瓶颈判定必须基于后端原始结果或完整回读数据。 -- 常见坑点:短管导致单位水头损失虚高、节点或链路映射缺失导致误判、模拟结果不完整时误把局部结果当全量结论。 diff --git a/.opencode/tools/apply_layer_style.ts b/.opencode/tools/apply_layer_style.ts new file mode 100644 index 0000000..95e183b --- /dev/null +++ b/.opencode/tools/apply_layer_style.ts @@ -0,0 +1,113 @@ +import { tool } from "@opencode-ai/plugin"; + +export default tool({ + description: + "在前端地图上对节点或管道图层应用样式,或重置为默认样式。样式参数应尽量与前端样式编辑器字段保持一致。", + args: { + reason: tool.schema + .string() + .describe( + "Why this style action is needed for the current user request.", + ), + layer_id: tool.schema + .enum(["junctions", "pipes"]) + .describe("Target layer id. Must be exactly 'junctions' or 'pipes'."), + reset_to_default: tool.schema + .boolean() + .optional() + .describe("Whether to reset the target layer to its default style."), + style_config: tool.schema + .object({ + property: tool.schema + .string() + .optional() + .describe("Data property to render."), + classification_method: tool.schema + .enum(["pretty_breaks", "custom_breaks"]) + .optional() + .describe("Classification method."), + segments: tool.schema + .number() + .int() + .min(2) + .max(10) + .optional() + .describe("Number of rendered intervals, from 2 to 10."), + min_size: tool.schema + .number() + .optional() + .describe("Minimum point radius."), + max_size: tool.schema + .number() + .optional() + .describe("Maximum point radius."), + min_stroke_width: tool.schema + .number() + .optional() + .describe("Minimum line width."), + max_stroke_width: tool.schema + .number() + .optional() + .describe("Maximum line width."), + fixed_stroke_width: tool.schema + .number() + .optional() + .describe("Fixed line width when width is not data-driven."), + color_type: tool.schema + .enum(["single", "gradient", "rainbow", "custom"]) + .optional() + .describe("Color strategy."), + single_palette_index: tool.schema.number().int().min(0).max(6).optional(), + gradient_palette_index: tool.schema.number().int().min(0).max(2).optional(), + rainbow_palette_index: tool.schema.number().int().min(0).max(1).optional(), + show_labels: tool.schema + .boolean() + .optional() + .describe("Whether to show labels."), + show_id: tool.schema + .boolean() + .optional() + .describe("Whether to show ids."), + opacity: tool.schema.number().min(0).max(1).optional().describe("Opacity in [0, 1]."), + adjust_width_by_property: tool.schema + .boolean() + .optional() + .describe("Whether line width is driven by the rendered property."), + custom_breaks: tool.schema + .array(tool.schema.number()) + .optional() + .describe("Strictly increasing boundaries. Length must equal segments + 1."), + custom_colors: tool.schema + .array(tool.schema.string()) + .optional() + .describe("Custom CSS colors. Length must equal segments."), + }) + .optional() + .describe( + "Optional style config overrides. Omit when reset_to_default is true.", + ), + }, + async execute(args) { + const layerLabel = args.layer_id === "junctions" ? "节点" : "管道"; + if (args.reset_to_default) { + return `已提交${layerLabel}图层默认样式重置请求。`; + } + const style = args.style_config; + if (style?.custom_breaks) { + if ( + style.custom_breaks.some( + (value, index, values) => index > 0 && value <= values[index - 1], + ) + ) { + throw new Error("custom_breaks must be strictly increasing"); + } + if (style.segments && style.custom_breaks.length !== style.segments + 1) { + throw new Error("custom_breaks length must equal segments + 1"); + } + } + if (style?.segments && style.custom_colors && style.custom_colors.length !== style.segments) { + throw new Error("custom_colors length must equal segments"); + } + return `已提交${layerLabel}图层样式请求。`; + }, +}); diff --git a/.opencode/tools/dynamic_http_call.ts b/.opencode/tools/dynamic_http_call.ts deleted file mode 100644 index 80a5138..0000000 --- a/.opencode/tools/dynamic_http_call.ts +++ /dev/null @@ -1,54 +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: - "通过本地 Agent 桥接调用 TJWater 后端 API。需提供 API 路径、可选的请求方法以及查询参数。", - args: { - reason: tool.schema - .string() - .describe("Why this tool call is required for the current user request."), - path: tool.schema.string().describe("Target backend API path, starting with '/'."), - method: tool.schema - .string() - .optional() - .describe("HTTP method. Defaults to GET."), - arguments: tool.schema - .record(tool.schema.string(), tool.schema.unknown()) - .optional() - .describe("Query arguments object."), - }, - 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}`); - } - // 工具本身不直接持有用户 token;通过 sessionID 回调 Agent 服务,由服务侧补齐用户上下文。 - const response = await fetch(`${internalBaseUrl}/internal/tools/dynamic-http-call`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-agent-internal-token": internalToken, - }, - body: JSON.stringify({ - sessionScopeKey: sessionContext.sessionScopeKey, - reason: args.reason, - path: args.path, - method: args.method, - arguments: args.arguments, - }), - }); - - const text = await response.text(); - if (!response.ok) { - throw new Error(text); - } - return text; - }, -}); diff --git a/.opencode/tools/fetch_result_ref.ts b/.opencode/tools/fetch_result_ref.ts deleted file mode 100644 index 32a45c0..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: - "回读由 dynamic_http_call 生成的持久化 result_ref。适用于大结果只返回 preview 时,再按需读取完整或截断后的数据。", - 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 returned by dynamic_http_call."), - 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/geocode.ts b/.opencode/tools/geocode.ts new file mode 100644 index 0000000..d113ad5 --- /dev/null +++ b/.opencode/tools/geocode.ts @@ -0,0 +1,37 @@ +import { tool } from "@opencode-ai/plugin"; + +const internalBaseUrl = + process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; +const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; + +export default tool({ + description: + "调用 TJWater 后端的天地图地理编码服务,将中国境内结构化地址或地点名称转换为经纬度。若需缩放地图,把返回的 location.lon/location.lat 传给 zoom_to_map,并设置 source_crs='EPSG:4326'。", + args: { + reason: tool.schema + .string() + .describe("Why geocoding is required for the current user request."), + keyword: tool.schema + .string() + .describe("Address or place name to geocode, such as 北京市人民政府."), + }, + async execute(args, context) { + const response = await fetch(`${internalBaseUrl}/internal/tools/geocode`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-agent-internal-token": internalToken, + }, + body: JSON.stringify({ + session_id: context.sessionID, + keyword: args.keyword, + }), + }); + + const text = await response.text(); + if (!response.ok) { + throw new Error(text); + } + return text; + }, +}); diff --git a/.opencode/tools/locate_features.ts b/.opencode/tools/locate_features.ts index 0290b9a..1ff3b49 100644 --- a/.opencode/tools/locate_features.ts +++ b/.opencode/tools/locate_features.ts @@ -5,8 +5,12 @@ export default tool({ args: { reason: tool.schema .string() - .describe("Why this map positioning action is needed for the user request."), - ids: tool.schema.array(tool.schema.string()).describe("Feature ids to locate."), + .describe( + "Why this map positioning action is needed for the user request.", + ), + ids: tool.schema + .array(tool.schema.string()) + .describe("Feature ids to locate."), feature_type: tool.schema .enum(["junction", "pipe", "valve", "reservoir", "pump", "tank"]) .describe("Type of feature to locate."), diff --git a/.opencode/tools/memory_manager.ts b/.opencode/tools/memory_manager.ts index 22f7795..bbceda5 100644 --- a/.opencode/tools/memory_manager.ts +++ b/.opencode/tools/memory_manager.ts @@ -1,17 +1,12 @@ import { tool } from "@opencode-ai/plugin"; -import { MemoryStore } from "../../src/memory/store.js"; -import { ToolSessionContextStore } from "../../src/session/toolContextStore.js"; -const memoryStore = new MemoryStore(); -const toolContextStore = new ToolSessionContextStore(); -const initializePromise = Promise.all([ - memoryStore.initialize(), - toolContextStore.initialize(), -]); +const internalBaseUrl = + process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; +const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; export default tool({ description: - "管理长期有效的用户偏好或项目事实。支持 add/list/replace/remove。禁止写入 token、password、secret、system prompt 或一次性上下文。scope 仅允许 'user' 或 'workspace'。", + "管理长期有效的用户偏好或项目事实。支持 add/list/replace/remove。add 前必须先对同 scope 执行 list 并阅读现有记忆,再决定 add、replace 或 remove;不要跳过读取直接新增。禁止写入 token、password、secret、system prompt 或一次性上下文。scope 仅允许 'user' 或 'workspace'。", args: { action: tool.schema .enum(["add", "list", "replace", "remove"]) @@ -27,104 +22,31 @@ export default tool({ content: tool.schema .string() .optional() - .describe( - "The durable fact or preference to remember, written as one concise sentence.", - ), + .describe("The durable fact or preference to remember, written as one concise sentence."), target_id: tool.schema .string() .optional() .describe("Stable memory entry id used by replace/remove."), }, 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/memory-manager`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-agent-internal-token": internalToken, + }, + body: JSON.stringify({ + ...args, + session_id: context.sessionID, + }), + }, + ); + const text = await response.text(); + if (!response.ok) { + throw new Error(text); } - const scope = - args.scope === "user" - ? "user" - : args.scope === "workspace" - ? "workspace" - : null; - if (!scope) { - return JSON.stringify({ - ok: true, - kind: "memory", - decision: "rejected", - detail: `unsupported scope: ${args.scope}; use exact keyword 'user' or 'workspace'`, - }); - } - if (sessionContext.allowLearningWrite === false && args.action !== "list") { - return JSON.stringify({ - ok: true, - kind: "memory", - decision: "rejected", - detail: "memory writes are disabled for this session", - }); - } - - const scopeKey = - scope === "user" ? sessionContext.actorKey : sessionContext.projectKey; - if (args.action === "list") { - return JSON.stringify({ - ok: true, - kind: "memory", - decision: "accepted", - detail: "memory listed", - items: await memoryStore.list(scope, scopeKey), - target: scope, - }); - } - - if (args.action === "add") { - const result = await memoryStore.upsert(scope, scopeKey, { - content: args.content ?? "", - sessionId: sessionContext.clientSessionId, - source: "tool", - traceId: sessionContext.traceId, - }); - if (!result.entry) { - return JSON.stringify({ - ok: true, - kind: "memory", - decision: "rejected", - detail: "content rejected by persistence policy", - }); - } - return JSON.stringify({ - ok: true, - kind: "memory", - decision: result.changed ? "accepted" : "deduped", - detail: result.changed ? "memory stored" : "memory already existed", - entry: result.entry, - target: scope, - }); - } - - if (args.action === "replace") { - const result = await memoryStore.replace(scope, scopeKey, args.target_id ?? "", { - content: args.content ?? "", - sessionId: sessionContext.clientSessionId, - source: "tool", - traceId: sessionContext.traceId, - }); - return JSON.stringify({ - ok: true, - kind: "memory", - decision: result.changed ? "accepted" : "rejected", - detail: result.detail, - target: scope, - }); - } - - const result = await memoryStore.remove(scope, scopeKey, args.target_id ?? ""); - return JSON.stringify({ - ok: true, - kind: "memory", - decision: result.changed ? "accepted" : "rejected", - detail: result.detail, - target: scope, - }); + return text; }, }); diff --git a/.opencode/tools/render_junctions.ts b/.opencode/tools/render_junctions.ts index 1c71afc..b5a5d9e 100644 --- a/.opencode/tools/render_junctions.ts +++ b/.opencode/tools/render_junctions.ts @@ -2,15 +2,17 @@ import { tool } from "@opencode-ai/plugin"; export default tool({ description: - "在前端地图上对 junctions 图层应用分区渲染。优先直接传入 render_ref(指向已持久化的渲染结果引用,格式应为 res-...),也不要先把 ref 内容完整读出再重组;前端会自行根据 render_ref 拉取完整 payload 并渲染,这样可以避免 LLM 读取大型 node_area_map。若当前只有本地 JSON 文件,请先调用 store_render_ref 把它迁移为受控 render_ref。供 render_ref 引用的 JSON 结构必须为 { node_area_map: Record, area_ids?: string[], area_colors?: Record },其中 node_area_map 的 key 是 junction/node id,value 是 area id。", + "在前端地图上对 junctions 图层应用分区渲染。先把包装格式 { metadata, location: { file_path }, data: { node_area_map, area_ids?, area_colors? } } 写入 RESULT_REF_IMPORT_DIR,location.file_path 必须等于文件绝对路径;再调用 store_render_ref 获得 res-... 引用,最后把引用传入本工具。不要读取并转传完整 ref 内容,也不要直接传本地文件路径。", args: { reason: tool.schema .string() - .describe("Why this junction rendering action is needed for the user request."), + .describe( + "Why this junction rendering action is needed for the user request.", + ), 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 可选。", + "上一步通过 store_render_ref 获得的渲染引用 ID(res-...)。前端会按该引用拉取完整 payload 并渲染。不可直接传入本地文件路径或完整 JSON 数据。", ), }, async execute() { diff --git a/.opencode/tools/session_search.ts b/.opencode/tools/session_search.ts index 9631b3c..50dcea9 100644 --- a/.opencode/tools/session_search.ts +++ b/.opencode/tools/session_search.ts @@ -1,11 +1,8 @@ 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: @@ -25,23 +22,21 @@ export default tool({ .describe("Optional maximum number of hits 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/session-search`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-agent-internal-token": internalToken, + const response = await fetch( + `${internalBaseUrl}/internal/tools/session-search`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-agent-internal-token": internalToken, + }, + body: JSON.stringify({ + max_results: args.max_results, + query: args.query, + session_id: context.sessionID, + }), }, - body: JSON.stringify({ - max_results: args.max_results, - query: args.query, - sessionScopeKey: sessionContext.sessionScopeKey, - }), - }); + ); const text = await response.text(); if (!response.ok) { throw new Error(text); diff --git a/.opencode/tools/show_chart.ts b/.opencode/tools/show_chart.ts index a8e3423..e133305 100644 --- a/.opencode/tools/show_chart.ts +++ b/.opencode/tools/show_chart.ts @@ -1,7 +1,8 @@ import { tool } from "@opencode-ai/plugin"; export default tool({ - description: "在前端对话界面中渲染图表。", + description: + "在前端对话界面中渲染图表。折线图/柱状图必须使用 x_data 作为横轴标签,series[].data 作为同长度的一维数值数组,不要把折线数据写成 ECharts 的 [x, y] 二维点数组。", args: { reason: tool.schema .string() @@ -11,18 +12,28 @@ export default tool({ .enum(["line", "bar", "pie"]) .optional() .describe("Chart type."), - x_data: tool.schema.array(tool.schema.string()).describe("X-axis labels."), + x_data: tool.schema + .array(tool.schema.string()) + .describe("X-axis labels. For line charts, put time/category labels here."), series: tool.schema .array( tool.schema.object({ name: tool.schema.string(), - data: tool.schema.array(tool.schema.number()), + data: tool.schema + .array(tool.schema.number()) + .describe("Y values only. Must align by index with x_data."), type: tool.schema.enum(["line", "bar"]).optional(), }), ) .describe("Series data."), - x_axis_name: tool.schema.string().optional().describe("X-axis display name."), - y_axis_name: tool.schema.string().optional().describe("Y-axis display name."), + x_axis_name: tool.schema + .string() + .optional() + .describe("X-axis display name."), + y_axis_name: tool.schema + .string() + .optional() + .describe("Y-axis display name."), }, async execute() { // 图表数据已经在工具参数里,前端收到 tool_call 后直接渲染,不再二次请求后端。 diff --git a/.opencode/tools/skill_manager.ts b/.opencode/tools/skill_manager.ts index da199b7..bb21b2b 100644 --- a/.opencode/tools/skill_manager.ts +++ b/.opencode/tools/skill_manager.ts @@ -1,19 +1,18 @@ import { tool } from "@opencode-ai/plugin"; -import { SkillStore } from "../../src/skills/store.js"; -import { ToolSessionContextStore } from "../../src/session/toolContextStore.js"; - -const toolContextStore = new ToolSessionContextStore(); -const initializePromise = toolContextStore.initialize(); -const skillStore = new SkillStore(); +const internalBaseUrl = + process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; +const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; export default tool({ description: - "维护已验证、可复用、非敏感的 workflow 或方法模式。支持 list、append_pattern、remove_pattern、write_reference、remove_reference、write_script、remove_script。", + "维护已验证、可复用、非敏感的 workflow 或方法模式。支持 list、write_skill、remove_skill、append_pattern、remove_pattern、write_reference、remove_reference、write_script、remove_script。", args: { action: tool.schema .enum([ "list", + "write_skill", + "remove_skill", "append_pattern", "remove_pattern", "write_reference", @@ -24,18 +23,13 @@ export default tool({ .describe("Skill maintenance operation."), reason: tool.schema .string() - .describe( - "Why this skill maintenance action is justified for future reuse.", - ), + .describe("Why this skill maintenance action is justified for future reuse."), 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. Use 'workflow' for the workflow index, or '__root__' for the root skills index.", ), - pattern: tool.schema - .string() - .optional() - .describe("Pattern text used by append_pattern."), + pattern: tool.schema.string().optional().describe("Pattern text used by append_pattern."), target_id: tool.schema .string() .optional() @@ -47,69 +41,27 @@ export default tool({ content: tool.schema .string() .optional() - .describe("Asset content used by write_reference or write_script."), + .describe("Content used by write_skill, write_reference, or write_script."), }, 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/skill-manager`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-agent-internal-token": internalToken, + }, + body: JSON.stringify({ + ...args, + session_id: context.sessionID, + }), + }, + ); + const text = await response.text(); + if (!response.ok) { + throw new Error(text); } - if (sessionContext.allowLearningWrite === false && args.action !== "list") { - return JSON.stringify({ - ok: true, - kind: "skill", - decision: "rejected", - detail: "skill writes are disabled for this session", - }); - } - if (args.action === "list") { - const result = await skillStore.list(args.skill_path); - if (!result) { - return JSON.stringify({ - ok: true, - kind: "skill", - decision: "rejected", - detail: - "invalid skill_path; expected a relative path under .opencode/skills", - }); - } - return JSON.stringify({ - ok: true, - kind: "skill", - decision: "accepted", - detail: "skill listed", - ...result, - }); - } - - const result = - args.action === "append_pattern" - ? await skillStore.appendPattern(args.skill_path, args.pattern ?? "") - : args.action === "remove_pattern" - ? await skillStore.removePattern(args.skill_path, args.target_id ?? "") - : args.action === "write_reference" - ? await skillStore.writeReference( - args.skill_path, - args.file_path ?? "", - args.content ?? "", - ) - : args.action === "remove_reference" - ? await skillStore.removeReference(args.skill_path, args.file_path ?? "") - : args.action === "write_script" - ? await skillStore.writeScript( - args.skill_path, - args.file_path ?? "", - args.content ?? "", - ) - : await skillStore.removeScript(args.skill_path, args.file_path ?? ""); - - return JSON.stringify({ - ok: true, - kind: "skill", - decision: result.changed ? "accepted" : "rejected", - detail: result.detail, - target: result.target, - }); + return text; }, }); diff --git a/.opencode/tools/store_render_ref.ts b/.opencode/tools/store_render_ref.ts index 20725f1..5e395b4 100644 --- a/.opencode/tools/store_render_ref.ts +++ b/.opencode/tools/store_render_ref.ts @@ -1,41 +1,41 @@ 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 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(); +const importDirectory = + process.env.RESULT_REF_IMPORT_DIR ?? "./data/result-imports"; export default tool({ description: - "把本地 JSON 渲染文件迁移成受控的 render_ref。仅适用于需要通过链接引用传递的大型 junction render payload。", + `导入 ${importDirectory} 下的受控 JSON 包装文件并返回 render_ref。文件必须是 { metadata: object, location: { file_path: string }, data: { node_area_map, area_ids?, area_colors? } },location.file_path 必须与传入的绝对路径完全一致。只接受该目录内的真实文件,不接受目录外路径或指向目录外的符号链接。`, args: { reason: tool.schema .string() - .describe("Why this local render payload should be persisted as a render_ref."), + .describe( + "为何需要将此本地渲染数据持久化为 render_ref,以便后续通过 render_junctions 渲染到前端。", + ), file_path: tool.schema .string() .describe( - "Absolute path to a local JSON file containing the raw render payload, or a wrapper object with data, metadata, and location. If wrapper metadata/location is missing or stale, the resolver will normalize and write it back before storing the render_ref.", + `位于 ${importDirectory} 内的包装 JSON 文件绝对路径。必须包含 metadata、location.file_path 和 data;data 才是 render_junctions 使用的 { node_area_map, area_ids?, area_colors? }。`, ), }, 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/store-render-ref`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-agent-internal-token": internalToken, + const response = await fetch( + `${internalBaseUrl}/internal/tools/store-render-ref`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-agent-internal-token": internalToken, + }, + body: JSON.stringify({ + session_id: context.sessionID, + file_path: args.file_path, + }), }, - body: JSON.stringify({ - sessionScopeKey: sessionContext.sessionScopeKey, - file_path: args.file_path, - }), - }); + ); const text = await response.text(); if (!response.ok) { diff --git a/.opencode/tools/tjwater_cli.ts b/.opencode/tools/tjwater_cli.ts new file mode 100644 index 0000000..462d0eb --- /dev/null +++ b/.opencode/tools/tjwater_cli.ts @@ -0,0 +1,48 @@ +import { tool } from "@opencode-ai/plugin"; + +const internalBaseUrl = + process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; +const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; + +export default tool({ + description: + "通过本地 Agent 桥接调用 tjwater-cli 命令访问 TJWater 后端服务。提供 CLI 子命令和参数。", + args: { + reason: tool.schema + .string() + .describe("Why this tool call is required for the current user request."), + command: tool.schema + .string() + .describe( + "tjwater-cli 子命令,不含二进制路径。示例:'data scheme list'、'data timeseries realtime links --start-time 2025-01-01T00:00:00+08:00 --end-time 2025-01-01T01:00:00+08:00'", + ), + timeout: tool.schema + .number() + .optional() + .describe("超时秒数,默认 120。大结果集建议设 300+。"), + }, + async execute(args, context) { + const response = await fetch( + `${internalBaseUrl}/internal/tools/tjwater-cli-call`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-agent-internal-token": internalToken, + }, + body: JSON.stringify({ + session_id: context.sessionID, + reason: args.reason, + command: args.command, + timeout: args.timeout, + }), + }, + ); + + const text = await response.text(); + if (!response.ok) { + throw new Error(text); + } + return text; + }, +}); diff --git a/.opencode/tools/view_history.ts b/.opencode/tools/view_history.ts index 9d4f07f..29c68a9 100644 --- a/.opencode/tools/view_history.ts +++ b/.opencode/tools/view_history.ts @@ -5,15 +5,23 @@ export default tool({ args: { reason: tool.schema .string() - .describe("Why this history panel should be opened for the current task."), + .describe( + "Why this history panel should be opened for the current task.", + ), feature_infos: tool.schema .array(tool.schema.tuple([tool.schema.string(), tool.schema.string()])) .describe("List of [id, type] pairs."), data_type: tool.schema .enum(["realtime", "scheme", "none"]) .describe("History data source type."), - start_time: tool.schema.string().optional().describe("Optional ISO8601 start time."), - end_time: tool.schema.string().optional().describe("Optional ISO8601 end time."), + start_time: tool.schema + .string() + .optional() + .describe("Optional ISO8601 start time."), + end_time: tool.schema + .string() + .optional() + .describe("Optional ISO8601 end time."), }, async execute() { // 返回短确认即可;面板打开动作由前端根据 tool_call 参数完成。 diff --git a/.opencode/tools/view_scada.ts b/.opencode/tools/view_scada.ts index 3c6cb21..ad05355 100644 --- a/.opencode/tools/view_scada.ts +++ b/.opencode/tools/view_scada.ts @@ -10,13 +10,18 @@ export default tool({ .array(tool.schema.string()) .optional() .describe("Preferred SCADA device ids."), - device_id: tool.schema.string().optional().describe("Single SCADA device id."), - feature_infos: tool.schema - .array(tool.schema.tuple([tool.schema.string(), tool.schema.string()])) + device_id: tool.schema + .string() .optional() - .describe("Legacy [id, type] pairs."), - start_time: tool.schema.string().optional().describe("Optional ISO8601 start time."), - end_time: tool.schema.string().optional().describe("Optional ISO8601 end time."), + .describe("Single SCADA device id."), + start_time: tool.schema + .string() + .optional() + .describe("Optional ISO8601 start time."), + end_time: tool.schema + .string() + .optional() + .describe("Optional ISO8601 end time."), }, async execute() { // SCADA 面板仍在浏览器侧执行,工具结果不承载实际监测数据。 diff --git a/.opencode/tools/web_search.ts b/.opencode/tools/web_search.ts new file mode 100644 index 0000000..fd64c2a --- /dev/null +++ b/.opencode/tools/web_search.ts @@ -0,0 +1,62 @@ +import { tool } from "@opencode-ai/plugin"; + +const internalBaseUrl = + process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; +const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; + +export default tool({ + description: + "调用 TJWater 后端的实时网页搜索服务。适合查询新闻、政策、规范、产品资料、公开网页事实等可能变化的信息。", + args: { + reason: tool.schema + .string() + .describe("Why web search is required for the current user request."), + query: tool.schema.string().describe("Search query text."), + freshness: tool.schema + .enum(["no_limit", "one_day", "one_week", "one_month", "one_year"]) + .optional() + .describe("Optional freshness filter. Defaults to no_limit."), + summary: tool.schema + .boolean() + .optional() + .describe("Whether the backend should include page summaries."), + count: tool.schema + .number() + .int() + .positive() + .optional() + .describe("Optional result count, backend accepts 1 to 50."), + include: tool.schema + .array(tool.schema.string()) + .optional() + .describe("Optional domains to include."), + exclude: tool.schema + .array(tool.schema.string()) + .optional() + .describe("Optional domains to exclude."), + }, + async execute(args, context) { + const response = await fetch(`${internalBaseUrl}/internal/tools/web-search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-agent-internal-token": internalToken, + }, + body: JSON.stringify({ + session_id: context.sessionID, + query: args.query, + freshness: args.freshness, + summary: args.summary, + count: args.count, + include: args.include, + exclude: args.exclude, + }), + }); + + const text = await response.text(); + if (!response.ok) { + throw new Error(text); + } + return text; + }, +}); diff --git a/.opencode/tools/zoom_to_map.ts b/.opencode/tools/zoom_to_map.ts new file mode 100644 index 0000000..986533b --- /dev/null +++ b/.opencode/tools/zoom_to_map.ts @@ -0,0 +1,32 @@ +import { tool } from "@opencode-ai/plugin"; + +export default tool({ + description: + "在前端地图上缩放定位到坐标。默认坐标为 EPSG:3857;如果来自天地图 geocode 的 lon/lat,传 source_crs='EPSG:4326',前端会转换为 EPSG:3857 后缩放。", + args: { + reason: tool.schema + .string() + .describe("Why this map zoom action is needed for the current request."), + x: tool.schema + .number() + .describe("X coordinate. For EPSG:4326 this is longitude; for EPSG:3857 this is meters."), + y: tool.schema + .number() + .describe("Y coordinate. For EPSG:4326 this is latitude; for EPSG:3857 this is meters."), + source_crs: tool.schema + .enum(["EPSG:3857", "EPSG:4326"]) + .optional() + .describe("Input coordinate CRS. Defaults to EPSG:3857."), + zoom: tool.schema + .number() + .optional() + .describe("Optional OpenLayers zoom level. Defaults to 18."), + duration_ms: tool.schema + .number() + .optional() + .describe("Optional animation duration in milliseconds. Defaults to 1000."), + }, + async execute() { + return "已缩放到指定地图坐标。"; + }, +}); diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..58018e4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +This repository is the internal TJWater agent service. Runtime TypeScript code is organized under `src/`; CLI-specific code is under `cli/src`; Node test files live in `node-tests/`. Agent extensions and OpenCode integration files are under `.opencode/`, including `.opencode/agents`, `.opencode/skills`, and `.opencode/tools`. Runtime data, session metadata, logs, and result references are stored under `data/` and `logs/` and should be treated as local/generated state. + +Deployment files are `Dockerfile`, `docker-compose.yml`, and `.gitea/workflows/package.yml`. + +## Build, Test, and Development Commands + +Use Bun for this project: + +```bash +bun install +bun run dev +bun run check +bun run test:cli +bun run start +``` + +`bun run dev` starts `src/server.ts` in watch mode. `bun run check` runs TypeScript checks for the main project and `.opencode`. `bun run test:cli` runs Node CLI tests. `bun run start` starts the service without watch mode. + +## Coding Style & Naming Conventions + +Use TypeScript ESM and keep types explicit at module boundaries. Use two-space indentation, `camelCase` for functions and variables, `PascalCase` for classes/types, and kebab-case or descriptive lowercase names for scripts and data files. Prefer existing `src/` service, route, and runtime patterns before introducing new structure. + +## Testing Guidelines + +Tests use Node's built-in test runner for CLI coverage. Name test files with `.node.mjs` when using `node --test`, matching `node-tests/cli/*.node.mjs`. Add focused tests for CLI parsing, tool behavior, and session/runtime changes. Do not depend on mutable local files under `data/`. + +## Commit & Pull Request Guidelines + +History uses Conventional Commit messages such as `feat(tools): add search and map tools`, `fix(agent): warm up opencode on startup`, and `refactor(chat): ...`. Prefer `feat(scope):`, `fix(scope):`, or `refactor(scope):`. + +PRs should describe runtime behavior changes, list `bun run check` and any test commands run, and mention changes to `.opencode`, secrets, ports, or deploy workflow behavior. + +## Security & Configuration Tips + +Do not commit `.env`, logs, session transcripts, generated result references, or `node_modules/`. Keep registry and deploy credentials in Gitea secrets. diff --git a/Dockerfile b/Dockerfile index 0ac0f9b..ee1c207 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,6 @@ -FROM oven/bun:canary-slim AS bun-bin - -FROM smanx/opencode:latest AS base +FROM smanx/opencode:1.18.13@sha256:b976acda21efffacd44abd7847dac7d646910dbaa477d1877e2881b39cf22a91 AS base USER root -ARG UBUNTU_APT_MIRROR=mirrors.aliyun.com +ARG UBUNTU_APT_MIRROR= ARG PYPI_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple ARG PYPI_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn ENV VIRTUAL_ENV=/opt/venv @@ -11,13 +9,22 @@ ENV PIP_INDEX_URL=${PYPI_INDEX_URL} ENV PIP_TRUSTED_HOST=${PYPI_TRUSTED_HOST} ENV UV_INDEX_URL=${PYPI_INDEX_URL} -RUN sed -i "s|http://archive.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g; s|http://security.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g" /etc/apt/sources.list 2>/dev/null || true && \ - sed -i "s|http://archive.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g; s|http://security.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g" /etc/apt/sources.list.d/*.sources 2>/dev/null || true && \ +COPY vendor/bun-linux-x64.zip /tmp/bun.zip + +RUN if [ -n "${UBUNTU_APT_MIRROR}" ]; then \ + sed -i "s|http://archive.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g; s|http://security.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g" /etc/apt/sources.list 2>/dev/null || true; \ + sed -i "s|http://archive.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g; s|http://security.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g" /etc/apt/sources.list.d/*.sources 2>/dev/null || true; \ + fi && \ apt-get update && apt-get install -y --no-install-recommends \ curl \ + jq \ unzip \ python3 \ python3-venv && \ + unzip -q /tmp/bun.zip -d /usr/local && \ + mv /usr/local/bun-linux-x64 /usr/local/bun && \ + ln -sf /usr/local/bun/bun /usr/local/bin/bun && \ + rm -f /tmp/bun.zip && \ curl -LsSf https://astral.sh/uv/install.sh | sh && \ ln -s /root/.local/bin/uv /usr/local/bin/uv && \ ln -sf /usr/bin/python3 /usr/local/bin/python && \ @@ -38,8 +45,6 @@ RUN sed -i "s|http://archive.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g; s|http:/ pytest && \ rm -rf /var/lib/apt/lists/* -COPY --from=bun-bin /usr/local/bin/bun /usr/local/bin/bun - FROM base AS deps WORKDIR /app @@ -53,30 +58,38 @@ WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/.opencode/node_modules ./.opencode/node_modules -COPY package.json bun.lock tsconfig.json opencode.json README.md ./ +COPY package.json bun.lock ./ +COPY tsconfig.json opencode.json README.md .gitignore ./ COPY src ./src +COPY cli ./cli COPY .opencode ./.opencode RUN bun run check -FROM base AS runner +FROM build AS test +RUN apt-get update && apt-get install -y --no-install-recommends nodejs && \ + rm -rf /var/lib/apt/lists/* +COPY contracts ./contracts +COPY node-tests ./node-tests +COPY scripts ./scripts +COPY tests ./tests +RUN bun run test:ci + +FROM build AS runner WORKDIR /app ENV NODE_ENV=production ENV HOST=0.0.0.0 ENV PORT=8787 - -COPY --from=deps /app/node_modules ./node_modules -COPY --from=deps /app/.opencode/node_modules ./.opencode/node_modules -COPY package.json bun.lock ./ -COPY tsconfig.json opencode.json ./ -COPY src ./src -COPY .opencode ./.opencode +ENV OPENCODE_HOST=127.0.0.1 +ENV OPENCODE_HOSTNAME=127.0.0.1 +ENV TJWATER_CLI_PATH=./cli/tjwater-cli COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +RUN chmod +x /entrypoint.sh ./cli/tjwater-cli ENTRYPOINT ["/entrypoint.sh"] -COPY .opencode ./.opencode EXPOSE 8787 +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ + CMD curl --fail --silent --show-error "http://127.0.0.1:${PORT}/health" >/dev/null || exit 1 CMD ["bun", "src/server.ts"] diff --git a/README.md b/README.md index c77e63e..2377220 100644 --- a/README.md +++ b/README.md @@ -1,312 +1,111 @@ -# TJWaterAgent 目录结构说明 +# TJWaterAgent 内部智能体服务 -`TJWaterAgent/` 是新的 opencode Agent 服务工程目录,负责对外提供 TJWater 智能助手接口,并通过 opencode SDK 启动或连接 opencode 运行时。 +`TJWaterAgent` 是 TJWater 内部版智能体服务,负责连接前端聊天界面、OpenCode 运行时、MCP 工具和 TJWater 后端 API。它面向内部研发与部署,保留完整的 Agent 编排、会话上下文、工具调用和运行时调试能力。 -## 总体边界 +## 主要能力 + +- 提供 `POST /api/v1/agent/sessions/{session_id}/runs` SSE 聊天接口。 +- 以内嵌模式启动并预热 OpenCode 运行时。 +- 管理前端 `session_id` 与 OpenCode session 的映射。 +- 在服务端保存当前会话的用户 token、项目、network 和 trace 上下文。 +- 通过 `.opencode/tools` 和 MCP 工具驱动地图定位、图表、SCADA、历史数据和业务 API 调用。 +- 通过 `data/` 保存运行时会话元数据、结果引用和本地状态。 + +## 目录结构 ```text -TJWaterAgent/ - package.json - tsconfig.json - src/ - opencode.json - .opencode/ - agents/ - tools/ - skills/ - package.json - tsconfig.json +src/ 服务端 TypeScript 源码 +src/routes/ HTTP 路由 +src/chat/ 聊天流和 SSE 事件适配 +src/runtime/ OpenCode 运行时管理 +src/session/ 会话映射和运行上下文 +src/mcp/ MCP 服务与工具桥接 +cli/ Agent 使用的 TypeScript 后端 API CLI +.opencode/agents/ Agent prompt 和模型行为配置 +.opencode/tools/ OpenCode 自定义工具 +.opencode/skills/ 可复用分析工作流 +node-tests/ Node CLI 测试 +data/ 本地运行时数据,禁止提交 +logs/ 本地日志,禁止提交 ``` -| 位置 | 主要作用 | 典型内容 | -| --- | --- | --- | -| `TJWaterAgent/` | 服务宿主、API 层和编排层 | Express 服务、SSE 接口、会话管理、鉴权上下文、后端 API 代理、opencode SDK 启动逻辑 | -| `TJWaterAgent/.opencode/` | opencode 项目资产目录 | agent prompt、自定义 tools、skills 树、plugins 相关依赖 | +## 本地开发 -## `TJWaterAgent/` 根目录的职责 - -根目录是 Node/TypeScript 服务本体,主要负责: - -1. 启动 HTTP 服务。 -2. 通过 `@opencode-ai/sdk` 启动内嵌 opencode server,或连接外部 opencode server。 -3. 管理前端 `session_id -> opencode sessionId` 的映射。 -4. 保存并传递用户 `Authorization`、`x-user-id`、`x-project-id`、`x-trace-id`。 -5. 把 opencode 输出适配成前端需要的 SSE 事件。 -6. 为 `.opencode/tools/dynamic_http_call.ts` 提供内部回调接口。 -7. 代理调用真实 TJWater 后端 API。 - -当前 Agent API 的主入口: - -```text -POST /api/v1/agent/chat/stream -``` - -该接口返回 SSE,事件包括: - -| event | 用途 | -| --- | --- | -| `progress` | 前端过程可视化,展示规划、工具调用和完成状态 | -| `token` | 最终回答文本流 | -| `tool_call` | 前端地图/面板/图表动作 | -| `done` | 当前轮完成 | -| `error` | 当前轮失败 | - -主要目录和文件: - -```text -src/ - server.ts - config.ts - runtime/ - session/ - chat/ - routes/ - tools/ -``` - -其中 `src/` 是业务服务层,不直接放 opencode skill 或 agent prompt。 - -## `.opencode/` 的职责 - -`.opencode/` 是给 opencode 运行时读取的项目资产目录,不是对外 HTTP 服务的主代码目录。 - -### agents - -```text -.opencode/agents/agent.md -``` - -这里定义默认 agent 的角色、行为规则、模型配置和工具使用策略。 - -当前项目已将 always-loaded instructions 收敛到 `agent.md`,`opencode.json` 不再额外配置 `instructions` 数组。 - -### tools - -```text -.opencode/tools/ - dynamic_http_call.ts - locate_features.ts - view_history.ts - view_scada.ts - show_chart.ts -``` - -这些是 opencode 可以调用的自定义工具。 - -`dynamic_http_call.ts` 不直接保存用户 token,也不直接访问后端。它会回调 `TJWaterAgent` 的内部接口,由上级服务层根据当前 session 补上用户 token、项目 ID 和 trace ID,再调用 TJWater 后端。 - -前端类工具如 `locate_features`、`view_history`、`view_scada`、`show_chart` 主要用于触发 UI 动作或可视化,不应被当作数据查询工具。 - -### skills - -```text -.opencode/skills/tjwater-skills-root-index/ - SKILL.md - ai/ - analytics/ - business/ - data/ - platform/ -``` - -这里保存 TJWater 技能树,并保持树结构,符合渐进式披露设计。 - -agent 需要某个领域知识时再按需加载对应 skill,不把整棵技能树作为 always-loaded prompt 一次性注入。 - -## 依赖边界 - -根目录和 `.opencode/` 使用两组 npm 依赖,职责不同。 - -### 根目录依赖 - -```text -TJWaterAgent/package.json -``` - -用于服务本体,例如: - -```text -@opencode-ai/sdk -express -zod -pino -``` - -### `.opencode` 依赖 - -```text -TJWaterAgent/.opencode/package.json -``` - -用于 opencode 自定义 tools/plugins,例如: - -```text -@opencode-ai/plugin -typescript -@types/node -``` - -这两组依赖不要混在一起:根目录负责服务运行,`.opencode` 负责 opencode 扩展资产的类型检查和运行依赖。 - -## 启动与部署 - -支持两种 opencode 接入方式: - -1. Embedded 模式:服务通过 `@opencode-ai/sdk` 调用 `createOpencode`,启动本地 `opencode` CLI 子进程并自动创建 client。 -2. Client 模式:服务通过 `createOpencodeClient` 直接连接一个已经存在的 opencode server。 - -因此,只有 Embedded 模式要求运行环境已安装 `opencode` CLI;Client 模式不依赖本地 CLI。 - -根目录的 Bun scripts 已经封装 `.opencode` 依赖安装和类型检查,日常只需要在 `TJWaterAgent/` 根目录操作。 - -### 本地开发 +项目使用 Bun: ```bash -cd TJWaterAgent bun install bun run dev ``` -`bun install` 会通过 `postinstall` 自动执行 `.opencode` 依赖安装;`bun run dev` 启动前会检查 `.opencode/tools` 的类型。 +`bun install` 会通过 `postinstall` 安装 `.opencode` 子目录依赖。`bun run dev` 以 watch 模式启动 `src/server.ts`,修改 `src/**`、`.opencode/**`、`opencode.json` 或 `.local.env` 后会自动重启。 -开发模式支持热重载,以下文件变化会触发服务重启并重新拉起 embedded opencode: +`cli/tjwater-cli` 是当前唯一的 TJWater 业务 CLI 入口,由 Bun 直接执行 +`cli/tjwater-cli.ts` 及 `cli/src/` 源码,并随 Agent 镜像一起交付,不需要 +Python 或 PyInstaller 构建步骤。 -```text -src/** -.opencode/** -opencode.json -.local.env +## 常用命令 + +```bash +bun run check +bun run contract:generate +bun run test:api +bun run test:cli +bun run start +bun run start:prod +docker build -t tjwater-agent:local . ``` -因此修改 agent prompt、tools、skills、模型配置或本地环境变量后,不需要手动重启 `bun run dev`。 +- `bun run check`:检查主项目和 `.opencode` 的 TypeScript 类型。 +- `bun run contract:generate`:生成 `contracts/agent-v1.openapi.json`。 +- `bun run test:api`:验证公开 REST 契约和聊天路由。 +- `bun run test:cli`:运行 `node-tests/cli/*.node.mjs`。 +- `bun run start`:直接启动服务。 +- `bun run start:prod`:先类型检查,再启动服务。 -本地开发可以在项目根目录的 `.local.env` 中配置环境变量。 +## 运行模式 -Embedded 模式示例: +当前运行时使用 OpenCode 稳定版 1.x CLI,并通过稳定版 SDK 的 `@opencode-ai/sdk/v2` HTTP 客户端访问运行时;这与 `opencode2` 及 `@opencode-ai/client` 的 2.0 beta 运行时不同。Embedded 模式由服务进程拉起本机 OpenCode: ```bash OPENCODE_MODE=embedded -DEEPSEEK_API_KEY=sk-xxx TJWATER_API_BASE_URL=http://127.0.0.1:8000 ``` -Client 模式示例: +当前仅支持 Embedded 模式,不支持连接外部 OpenCode server。 + +## 认证续期与学习工具 + +后端工具调用遇到即将过期的 access token 或首次 `401` 时,Agent 会通过当前 SSE 流发送 `credential_refresh_required`。前端使用服务端保存的 Keycloak refresh token 强制换取新 access token,再调用 `POST /api/v1/agent/sessions/{session_id}/credential-refreshes` 唤醒原工具调用。等待上限为 30 秒,同一会话的并发请求合并为一次续期,原调用最多重试一次;`403` 不触发续期。 + +`memory_manager` 和 `skill_manager` 在 OpenCode 侧只保留内部 HTTP 桥,读取会话上下文和持久化数据的逻辑统一在 Agent 主进程中执行。长期记忆、自动学习和显式工具写入因此共享同一组 `MemoryStore`、`SkillStore` 和运行时会话上下文。 + +本地可使用 `.local.env` 保存开发配置;系统环境变量优先级更高。 + +服务会在 HTTP 端口开始监听前完成 OpenCode 健康检查、临时会话创建和工具目录加载。`GET /health` 返回 `ready: true` 与 `warmed_up: true` 时,表示冷启动预热已经完成。开发环境会输出各预热阶段的耗时。 + +`opencode.json` 已启用 `experimental.continue_loop_on_deny`。用户拒绝权限请求后,OpenCode V1 会把拒绝结果交还给 Agent,让其尝试无需该权限的替代方案,而不是直接结束本轮执行。 + +前端提供三种整体权限模式:“请求批准”只执行 OpenCode 明确允许的白名单,其他权限请求逐次交给用户确认;“自动批准”额外自动放行低风险业务工具,其他请求仍需确认;“始终允许”自动放行当前对话中所有未被 OpenCode 明确禁止的权限请求。自动放行统一使用单次批准,切换整体模式后立即恢复对应策略,不会写入持久授权。 + +单次权限请求支持“允许一次”“保存授权”和“拒绝”。“保存授权”使用 OpenCode 的 `always` 回复,仅保存 OpenCode 为本次请求建议的权限范围,并只在当前 OpenCode 会话内生效。外部目录以及 `.env`、`data/`、`logs/` 路径仍由静态配置明确禁止,三种整体模式都不能绕过这些拒绝规则。 + +`store_render_ref` 只会从 `RESULT_REF_IMPORT_DIR`(默认 `./data/result-imports`)导入包装格式 JSON。文件必须包含 `metadata`、`location.file_path` 和 `data`,且真实路径不能越出导入目录;单文件默认上限为 64 MiB,成功导入后源包装文件会被删除。 + +## 配置与安全 + +不要提交 `.env`、`.local.env`、`data/`、`logs/`、会话记录、模型输出、访问令牌或 `node_modules/`。部署凭据、镜像仓库账号和 webhook 地址应放在 Gitea secrets 或部署环境变量中。 + +## 发布 + +Gitea 包工作流位于 `.gitea/workflows/package.yml`。发布前至少运行: ```bash -OPENCODE_MODE=client -OPENCODE_CLIENT_BASE_URL=http://127.0.0.1:4096 -DEEPSEEK_API_KEY=sk-xxx -TJWATER_API_BASE_URL=http://127.0.0.1:8000 -``` - -服务启动时会自动读取 `.local.env`,但系统环境变量优先级更高,适合在本机保存开发用 key。 - -### 生产启动 - -```bash -cd TJWaterAgent -bun install bun run check -bun run start ``` -也可以使用一条命令完成构建并启动: +如修改 CLI 或工具调用逻辑,同时运行: ```bash -cd TJWaterAgent -bun install -bun run start:prod +bun run test:cli ``` - -### Docker Compose 启动 - -项目根目录已提供 `Dockerfile` 和 `docker-compose.yml`,可直接使用: - -```bash -cd TJWaterAgent -docker compose up -d --build -``` - -查看日志: - -```bash -docker compose logs -f tjwater-agent -``` - -停止并清理容器: - -```bash -docker compose down -``` - -### 常用脚本 - -| 命令 | 作用 | -| --- | --- | -| `bun run dev` | 类型检查 `.opencode` tools 后,以 watch 模式直接运行 `src/server.ts` | -| `bun run check` | 执行完整类型检查(服务与 `.opencode` tools) | -| `bun run start` | 直接运行 `src/server.ts` | -| `bun run start:prod` | 先类型检查再启动 | -| `bun run install:opencode` | 手动安装 `.opencode` 依赖 | -| `bun run pipeline:trigger` | 通过重建并强推 annotated `latest` tag 触发 Gitea CI/CD,只发布/覆盖 `latest` 镜像 | - -### 模型与 API 配置 - -默认 Agent 模型为: - -```text -deepseek/deepseek-v4-pro -``` - -涉及位置: - -```text -opencode.json -.opencode/agents/tjwater-assistant.md -src/config.ts 的 OPENCODE_MODEL 默认值 -``` - -如果需要临时覆盖模型,可以在启动时设置: - -```bash -OPENCODE_MODEL=deepseek/deepseek-v4-pro bun run start -``` - -DeepSeek API key 不写入代码,部署时通过环境变量设置: - -```bash -DEEPSEEK_API_KEY=sk-xxx bun run start -``` - -`opencode.json` 已配置从环境变量读取: - -```json -{ - "provider": { - "deepseek": { - "options": { - "apiKey": "{env:DEEPSEEK_API_KEY}" - } - } - } -} -``` - -如果需要自定义 DeepSeek 兼容 API 地址,可以通过 opencode 的 provider 配置增加 `baseURL`,例如在部署环境使用 `OPENCODE_CONFIG_CONTENT` 覆盖: - -```bash -OPENCODE_CONFIG_CONTENT='{"provider":{"deepseek":{"options":{"baseURL":"https://your-api.example.com/v1"}}}}' \ -DEEPSEEK_API_KEY=sk-xxx \ -bun run start -``` - -也可以使用 opencode 的 `/connect` 命令写入用户级凭据,但服务部署更推荐使用环境变量。 - -如果需要连接外部独立运行的 opencode server,可以配置: - -```bash -OPENCODE_MODE=client -OPENCODE_CLIENT_BASE_URL=http://127.0.0.1:4096 -``` - -配置后,`TJWaterAgent` 会连接该外部 opencode server,而不是自行启动 embedded opencode server。 diff --git a/bun.lock b/bun.lock index 90544b1..ab167bf 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "tjwater-agent", "dependencies": { - "@opencode-ai/sdk": "^1.14.29", + "@opencode-ai/sdk": "1.18.13", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^4.21.2", @@ -14,6 +14,7 @@ "zod": "^3.25.76", }, "devDependencies": { + "@asteasolutions/zod-to-openapi": "7.3.4", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/node": "^24.7.2", @@ -23,7 +24,9 @@ }, }, "packages": { - "@opencode-ai/sdk": ["@opencode-ai/sdk@1.14.30", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-OgPEDvALekHZIjByo/okJ699aLPn+XtsVxgZxUqE8TlzAG7TtskMGFl0fro8O0T2p+nkOT/LstnKGbECvc0+YA=="], + "@asteasolutions/zod-to-openapi": ["@asteasolutions/zod-to-openapi@7.3.4", "", { "dependencies": { "openapi3-ts": "^4.1.2" }, "peerDependencies": { "zod": "^3.20.2" } }, "sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA=="], + + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.13", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-JY9etiVcu1G/pZjaH2vjK/b8z54ujxaWCD1GziO4ADUhRM6m6zm2332bPGcxEfA6TwweiJfNlK6wVZQ0f/X4KQ=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], @@ -175,6 +178,8 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "openapi3-ts": ["openapi3-ts@4.6.1", "", { "dependencies": { "yaml": "^2.9.0" } }, "sha512-XW9MOldkhoICNeXVzzmXzmOW5G73ppOEGmh7fLCqHjgfdEYCGGN+00MlVCeUZgovjjfC56j9tvtDt1zGabNjjA=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -259,6 +264,8 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "body-parser/qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], diff --git a/cli/src/commands/analysis.ts b/cli/src/commands/analysis.ts new file mode 100644 index 0000000..6463301 --- /dev/null +++ b/cli/src/commands/analysis.ts @@ -0,0 +1,189 @@ +import { CliError } from "../core/errors.js"; +import { emitApi, requestJson } from "../core/http.js"; +import { assignDatasetKeys, parseBurstFile, parseValveSettingFile } from "../core/files.js"; +import { optionalNumber, optionalString, optionalStringArray, parseOptions, requiredNumber, requiredString, validateChoice } from "../core/options.js"; +import { resolveScheme } from "../core/runtime.js"; +import { parseTime } from "../core/time.js"; +import { success } from "../core/output.js"; +import type { HandlerMap, RuntimeContext } from "../core/types.js"; + +function analysisBurst(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv, { duration: "integer" }); + const [ids, sizes] = parseBurstFile(requiredString(values, "burst-file")); + const schemeName = resolveScheme(ctx, optionalString(values, "scheme"), true)!; + return emitApi(ctx, "爆管分析执行成功", { + method: "POST", + path: "/burst-analyses", + params: { + modify_pattern_start_time: parseTime(requiredString(values, "start-time"), "--start-time"), + burst_id: ids, + burst_size: sizes, + modify_total_duration: requiredNumber(values, "duration"), + scheme_name: schemeName, + }, + requireProject: true, + }, [`tjwater-cli data scheme get --name ${schemeName}`, "tjwater-cli data scheme list"]); +} + +function analysisValve(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv, { valve: "repeat", element: "repeat", "disabled-valve": "repeat", duration: "integer" }); + const mode = validateChoice(requiredString(values, "mode"), ["close", "isolation"] as const, "--mode"); + if (mode === "close") { + const valves = optionalStringArray(values, "valve"); + const startTime = optionalString(values, "start-time"); + if (!startTime || !valves) throw new CliError("CLI 参数错误", "INVALID_VALVE_CLOSE_ARGS", "close mode requires --start-time and at least one --valve", 2); + return emitApi(ctx, "阀门关闭分析执行成功", { + method: "POST", + path: "/valve-isolation-analyses", + params: { + start_time: parseTime(startTime, "--start-time"), + valves, + duration: optionalNumber(values, "duration") || 900, + scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true), + }, + requireProject: true, + }); + } + const elements = optionalStringArray(values, "element"); + if (!elements) throw new CliError("CLI 参数错误", "INVALID_VALVE_ISOLATION_ARGS", "isolation mode requires at least one --element", 2); + return emitApi(ctx, "阀门隔离分析执行成功", { + method: "POST", + path: "/valve-isolation-analyses", + params: { accident_element: elements, disabled_valves: optionalStringArray(values, "disabled-valve") }, + requireProject: true, + }); +} + +function analysisFlushing(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv, { flow: "number", duration: "integer" }); + const [valves, openings] = parseValveSettingFile(requiredString(values, "valve-setting-file")); + return emitApi(ctx, "冲洗分析执行成功", { + method: "POST", + path: "/flushing-analyses", + params: { + start_time: parseTime(requiredString(values, "start-time"), "--start-time"), + valves, + valves_k: openings, + drainage_node_id: requiredString(values, "drainage-node"), + flush_flow: requiredNumber(values, "flow"), + duration: optionalNumber(values, "duration") || 900, + scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true), + }, + requireProject: true, + }); +} + +function analysisAge(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv, { duration: "integer" }); + return emitApi(ctx, "水龄分析执行成功", { + method: "POST", + path: "/water-age-analyses", + params: { start_time: parseTime(requiredString(values, "start-time"), "--start-time"), duration: requiredNumber(values, "duration") }, + requireProject: true, + }); +} + +function analysisContaminant(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv, { duration: "integer", concentration: "number" }); + const params: Record = { + start_time: parseTime(requiredString(values, "start-time"), "--start-time"), + source: requiredString(values, "source-node"), + concentration: requiredNumber(values, "concentration"), + duration: requiredNumber(values, "duration"), + scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true), + }; + const pattern = optionalString(values, "pattern"); + if (pattern) params.pattern = pattern; + return emitApi(ctx, "污染物模拟执行成功", { method: "POST", path: "/contaminant-simulations", params, requireProject: true }); +} + +function sensorKmeans(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv, { count: "integer", "min-diameter": "integer" }); + return emitApi(ctx, "传感器选址执行成功", { + method: "POST", + path: "/pressure-sensor-placement-kmeans", + body: { + scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true), + sensor_number: requiredNumber(values, "count"), + min_diameter: optionalNumber(values, "min-diameter") || 0, + }, + requireProject: true, + }); +} + +function schemeAnalysis(ctx: RuntimeContext, argv: string[], summary: string, path: string, startKey: string, endKey: string): Promise { + const { values } = parseOptions(argv); + return emitApi(ctx, summary, { + method: "POST", + path, + body: { + [startKey]: parseTime(requiredString(values, "start-time"), "--start-time"), + [endKey]: parseTime(requiredString(values, "end-time"), "--end-time"), + scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true), + }, + requireProject: true, + }); +} + +function schemeList(ctx: RuntimeContext, summary: string, schemeType: string): Promise { + return emitApi(ctx, summary, { method: "GET", path: "/schemes", params: { scheme_type: schemeType }, requireProject: true }); +} + +function schemeGet(ctx: RuntimeContext, argv: string[], summary: string, schemeType: string): Promise { + const { positionals } = parseOptions(argv); + if (!positionals[0]) throw new CliError("CLI 参数错误", "MISSING_ARGUMENT", "Missing argument 'SCHEME_NAME'", 2); + return emitApi(ctx, summary, { method: "GET", path: `/schemes/${positionals[0]}`, params: { scheme_type: schemeType }, requireProject: true }); +} + +function burstLocation(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv, { "burst-leakage": "number", "pressure-scada-id": "repeat", "flow-scada-id": "repeat", "use-scada-flow": "boolean" }); + const body: Record = { + scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true), + data_source: optionalString(values, "data-source") || "monitoring", + scada_burst_start: parseTime(requiredString(values, "start-time"), "--start-time"), + scada_burst_end: parseTime(requiredString(values, "end-time"), "--end-time"), + burst_leakage: requiredNumber(values, "burst-leakage"), + use_scada_flow: Boolean(values["use-scada-flow"]), + }; + const pressureIds = optionalStringArray(values, "pressure-scada-id"); + const flowIds = optionalStringArray(values, "flow-scada-id"); + if (pressureIds) body.pressure_scada_ids = pressureIds; + if (flowIds) body.flow_scada_ids = flowIds; + const pressureFile = optionalString(values, "pressure-file"); + const flowFile = optionalString(values, "flow-file"); + if (pressureFile) assignDatasetKeys(body, pressureFile, ["burst_pressure", "normal_pressure"], "pressure"); + if (flowFile) assignDatasetKeys(body, flowFile, ["burst_flow", "normal_flow"], "flow"); + return emitApi(ctx, "爆管定位执行成功", { method: "POST", path: "/burst-locations", body, requireProject: true }); +} + +function riskPipe(ctx: RuntimeContext, argv: string[], summary: string, path: string): Promise { + const { values } = parseOptions(argv); + return emitApi(ctx, summary, { method: "GET", path, params: { pipe_id: requiredString(values, "pipe") }, requireProject: true }); +} + +async function riskNetwork(ctx: RuntimeContext): Promise { + const [probabilities, a] = await requestJson(ctx, { method: "GET", path: "/network-pipe-risk-probability-nows", requireProject: true }); + const [geometries, b] = await requestJson(ctx, { method: "GET", path: "/pipes/risk-probability-geometries", requireProject: true }); + success("读取全网风险成功", { probabilities, geometries }, ctx, a + b); +} + +export const analysisHandlers: HandlerMap = { + "analysis burst": analysisBurst, + "analysis valve": analysisValve, + "analysis flushing": analysisFlushing, + "analysis age": analysisAge, + "analysis contaminant": analysisContaminant, + "analysis sensor-placement kmeans": sensorKmeans, + "analysis leakage identify": (ctx, argv) => schemeAnalysis(ctx, argv, "漏损识别执行成功", "/leakage-identifications", "scada_start", "scada_end"), + "analysis leakage schemes list": (ctx) => schemeList(ctx, "读取漏损方案列表成功", "dma_leak_identification"), + "analysis leakage schemes get": (ctx, argv) => schemeGet(ctx, argv, "读取漏损方案详情成功", "dma_leak_identification"), + "analysis burst-detection detect": (ctx, argv) => schemeAnalysis(ctx, argv, "爆管检测执行成功", "/burst-detections", "scada_start", "scada_end"), + "analysis burst-detection schemes list": (ctx) => schemeList(ctx, "读取爆管检测方案列表成功", "burst_detection"), + "analysis burst-detection schemes get": (ctx, argv) => schemeGet(ctx, argv, "读取爆管检测方案详情成功", "burst_detection"), + "analysis burst-location locate": burstLocation, + "analysis burst-location schemes list": (ctx) => schemeList(ctx, "读取爆管定位方案列表成功", "burst_location"), + "analysis burst-location schemes get": (ctx, argv) => schemeGet(ctx, argv, "读取爆管定位方案详情成功", "burst_location"), + "analysis risk pipe-now": (ctx, argv) => riskPipe(ctx, argv, "读取当前管道风险成功", "/pipes/risk-probability-now"), + "analysis risk pipe-history": (ctx, argv) => riskPipe(ctx, argv, "读取历史管道风险成功", "/pipes/risk-probability"), + "analysis risk network": riskNetwork, +}; diff --git a/cli/src/commands/component.ts b/cli/src/commands/component.ts new file mode 100644 index 0000000..106b034 --- /dev/null +++ b/cli/src/commands/component.ts @@ -0,0 +1,33 @@ +import { CliError } from "../core/errors.js"; +import { emitApi } from "../core/http.js"; +import { optionalString, parseOptions, requiredString, validateChoice } from "../core/options.js"; +import type { HandlerMap, RuntimeContext } from "../core/types.js"; + +type ComponentKind = "time" | "energy" | "pump-energy" | "network"; + +function componentOption(ctx: RuntimeContext, argv: string[], schema: boolean): Promise { + const { values } = parseOptions(argv); + const kind = validateChoice(requiredString(values, "kind"), ["time", "energy", "pump-energy", "network"] as const, "--kind"); + const routes: Record<`${ComponentKind}:${boolean}`, string> = { + "time:true": "/network-schemas/time", + "time:false": "/network-options/time", + "energy:true": "/network-schemas/energy", + "energy:false": "/network-options/energy", + "pump-energy:true": "/network-schemas/pump-energy", + "pump-energy:false": "/network-options/pump-energy", + "network:true": "/network-schemas/option", + "network:false": "/network-options", + }; + const params: Record = {}; + const pump = optionalString(values, "pump"); + if (kind === "pump-energy") { + if (!schema && !pump) throw new CliError("CLI 参数错误", "PUMP_REQUIRED", "--pump is required when --kind pump-energy", 2); + if (pump) params.pump = pump; + } + return emitApi(ctx, schema ? "读取选项 schema 成功" : "读取选项属性成功", { method: "GET", path: routes[`${kind}:${schema}`], params, requireProject: true }); +} + +export const componentHandlers: HandlerMap = { + "component option schema": (ctx, argv) => componentOption(ctx, argv, true), + "component option get": (ctx, argv) => componentOption(ctx, argv, false), +}; diff --git a/cli/src/commands/data.ts b/cli/src/commands/data.ts new file mode 100644 index 0000000..bca471e --- /dev/null +++ b/cli/src/commands/data.ts @@ -0,0 +1,176 @@ +import { SCADA_FIELDS, type ElementType } from "../core/constants.js"; +import { CliError } from "../core/errors.js"; +import { emitApi } from "../core/http.js"; +import { fieldsFor, optionalString, parseOptions, requiredString, requiredStringArray, validateChoice } from "../core/options.js"; +import { resolveScheme } from "../core/runtime.js"; +import { parseTime } from "../core/time.js"; +import type { HandlerMap, RuntimeContext } from "../core/types.js"; + +function backendElementType(type: ElementType): "link" | "node" { + return type === "pipe" ? "link" : "node"; +} + +function rangeGet(ctx: RuntimeContext, argv: string[], summary: string, path: string): Promise { + const { values } = parseOptions(argv); + return emitApi(ctx, summary, { + method: "GET", + path, + params: { start_time: parseTime(requiredString(values, "start-time"), "--start-time"), end_time: parseTime(requiredString(values, "end-time"), "--end-time") }, + requireProject: true, + }); +} + +function realtimeByIdTime(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv); + const type = validateChoice(requiredString(values, "type"), ["pipe", "junction"] as const, "--type"); + return emitApi(ctx, "读取实时模拟数据成功", { + method: "GET", + path: "/timeseries/realtime/simulation-results", + params: { id: requiredString(values, "id"), type: backendElementType(type), query_time: parseTime(requiredString(values, "time"), "--time") }, + requireProject: true, + }); +} + +function realtimeByTimeProperty(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv); + const type = validateChoice(requiredString(values, "type"), ["pipe", "junction"] as const, "--type"); + return emitApi(ctx, "读取实时属性聚合数据成功", { + method: "GET", + path: "/timeseries/realtime/records", + params: { type: backendElementType(type), query_time: parseTime(requiredString(values, "time"), "--time"), property: validateChoice(requiredString(values, "property"), fieldsFor(type), "--property") }, + requireProject: true, + }); +} + +function schemeLinks(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv); + return emitApi(ctx, "读取方案管道数据成功", { + method: "GET", + path: "/timeseries/schemes/links", + params: { + scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true), + scheme_type: optionalString(values, "scheme-type") || "simulation", + start_time: parseTime(requiredString(values, "start-time"), "--start-time"), + end_time: parseTime(requiredString(values, "end-time"), "--end-time"), + }, + requireProject: true, + }); +} + +function schemeNodeField(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv); + return emitApi(ctx, "读取方案节点字段成功", { + method: "GET", + path: `/timeseries/schemes/nodes/${requiredString(values, "node")}/field`, + params: { + field: validateChoice(requiredString(values, "field"), fieldsFor("junction"), "--field"), + scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true), + scheme_type: optionalString(values, "scheme-type") || "simulation", + start_time: parseTime(requiredString(values, "start-time"), "--start-time"), + end_time: parseTime(requiredString(values, "end-time"), "--end-time"), + }, + requireProject: true, + }); +} + +function schemeSimulation(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv); + const query = validateChoice(requiredString(values, "query"), ["by-id-time", "by-scheme-time-property"] as const, "--query"); + const type = validateChoice(optionalString(values, "type") || "pipe", ["pipe", "junction"] as const, "--type") as ElementType; + const params: Record = { + scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true), + scheme_type: optionalString(values, "scheme-type") || "simulation", + query_time: parseTime(requiredString(values, "time"), "--time"), + type: backendElementType(type), + }; + if (query === "by-id-time") { + params.id = requiredString(values, "id"); + return emitApi(ctx, "读取方案单点模拟数据成功", { method: "GET", path: "/timeseries/schemes/simulation-results", params, requireProject: true }); + } + params.property = validateChoice(requiredString(values, "property"), fieldsFor(type), "--property"); + return emitApi(ctx, "读取方案属性聚合数据成功", { method: "GET", path: "/timeseries/schemes/records", params, requireProject: true }); +} + +function scadaQuery(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv, { "device-id": "repeat" }); + const params: Record = { + device_ids: requiredStringArray(values, "device-id").join(","), + start_time: parseTime(requiredString(values, "start-time"), "--start-time"), + end_time: parseTime(requiredString(values, "end-time"), "--end-time"), + }; + const field = optionalString(values, "field"); + if (field) params.field = validateChoice(field, SCADA_FIELDS, "--field"); + return emitApi(ctx, "读取 SCADA 时序成功", { method: "GET", path: field ? "/timeseries/scada-readings/fields" : "/timeseries/scada-readings", params, requireProject: true }); +} + +function composite(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv, { feature: "repeat", "use-cleaned": "boolean" }); + const kind = validateChoice(requiredString(values, "kind"), ["scada-simulation", "element-simulation", "element-scada"] as const, "--kind"); + const params: Record = { + start_time: parseTime(requiredString(values, "start-time"), "--start-time"), + end_time: parseTime(requiredString(values, "end-time"), "--end-time"), + }; + const schemeName = resolveScheme(ctx, optionalString(values, "scheme")); + if (schemeName) Object.assign(params, { scheme_name: schemeName, scheme_type: optionalString(values, "scheme-type") || "simulation" }); + if (kind === "scada-simulation") params.device_ids = requiredStringArray(values, "feature").join(","); + else if (kind === "element-simulation") params.feature_infos = requiredStringArray(values, "feature").join(","); + else { + const feature = requiredStringArray(values, "feature"); + if (feature.length !== 1) throw new CliError("CLI 参数错误", "FEATURE_REQUIRED", "element-scada requires exactly one --feature as element_id", 2); + params.element_id = feature[0]; + params.use_cleaned = Boolean(values["use-cleaned"]); + } + const paths = { + "scada-simulation": "/timeseries/views/scada-simulations", + "element-simulation": "/timeseries/views/element-simulations", + "element-scada": "/timeseries/views/element-scada-readings", + } as const; + return emitApi(ctx, kind === "scada-simulation" ? "读取复合 SCADA-模拟数据成功" : kind === "element-simulation" ? "读取复合元素模拟数据成功" : "读取元素关联 SCADA 数据成功", { method: "GET", path: paths[kind], params, requireProject: true }); +} + +function pipelineHealth(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv); + requiredString(values, "pipe"); + requiredString(values, "start-time"); + return emitApi(ctx, "读取管道健康预测成功", { + method: "GET", + path: "/pipeline-health-predictions", + params: { query_time: parseTime(requiredString(values, "end-time"), "--end-time") }, + requireProject: true, + }); +} + +function dataScadaGet(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv); + validateChoice(requiredString(values, "kind"), ["info"] as const, "--kind"); + return emitApi(ctx, "读取 SCADA 数据成功", { method: "GET", path: "/scada-info/detail", params: { id: requiredString(values, "id") }, requireProject: true }); +} + +function dataScadaList(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv); + validateChoice(requiredString(values, "kind"), ["info"] as const, "--kind"); + return emitApi(ctx, "读取 SCADA 列表成功", { method: "GET", path: "/scada-info", requireProject: true }); +} + +function dataSchemeGet(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv); + return emitApi(ctx, "读取方案成功", { method: "GET", path: "/schemes/detail", params: { schema_name: requiredString(values, "name") }, requireProject: true }); +} + +export const dataHandlers: HandlerMap = { + "data timeseries realtime links": (ctx, argv) => rangeGet(ctx, argv, "读取实时管道数据成功", "/timeseries/realtime/links"), + "data timeseries realtime nodes": (ctx, argv) => rangeGet(ctx, argv, "读取实时节点数据成功", "/timeseries/realtime/nodes"), + "data timeseries realtime simulation-by-id-time": realtimeByIdTime, + "data timeseries realtime simulation-by-time-property": realtimeByTimeProperty, + "data timeseries scheme links": schemeLinks, + "data timeseries scheme node-field": schemeNodeField, + "data timeseries scheme simulation": schemeSimulation, + "data timeseries scada query": scadaQuery, + "data timeseries composite": composite, + "data timeseries composite pipeline-health": pipelineHealth, + "data scada get": dataScadaGet, + "data scada list": dataScadaList, + "data scheme schema": (ctx) => emitApi(ctx, "读取方案 schema 成功", { method: "GET", path: "/network-schemas/scheme", requireProject: true }), + "data scheme get": dataSchemeGet, + "data scheme list": (ctx) => emitApi(ctx, "读取方案列表成功", { method: "GET", path: "/schemes", requireProject: true }), +}; diff --git a/cli/src/commands/network.ts b/cli/src/commands/network.ts new file mode 100644 index 0000000..1321d5e --- /dev/null +++ b/cli/src/commands/network.ts @@ -0,0 +1,26 @@ +import { emitApi } from "../core/http.js"; +import { parseOptions, requiredString } from "../core/options.js"; +import type { HandlerMap, RuntimeContext } from "../core/types.js"; + +function apiGet(ctx: RuntimeContext, argv: string[], summary: string, path: string, key: string): Promise { + const { values } = parseOptions(argv); + return emitApi(ctx, summary, { method: "GET", path, params: { [key]: requiredString(values, key) }, requireProject: true }); +} + +function apiGetAll(ctx: RuntimeContext, summary: string, path: string): Promise { + return emitApi(ctx, summary, { method: "GET", path, requireProject: true }); +} + +export const networkHandlers: HandlerMap = { + "network get-junction-properties": (ctx, argv) => apiGet(ctx, argv, "读取节点属性成功", "/junctions/properties", "junction"), + "network get-pipe-properties": (ctx, argv) => apiGet(ctx, argv, "读取管道属性成功", "/pipes/properties", "pipe"), + "network get-all-pipes-properties": (ctx) => apiGetAll(ctx, "读取全部管道属性成功", "/pipes"), + "network get-reservoir-properties": (ctx, argv) => apiGet(ctx, argv, "读取水库属性成功", "/reservoirs/properties", "reservoir"), + "network get-all-reservoirs-properties": (ctx) => apiGetAll(ctx, "读取全部水库属性成功", "/reservoirs"), + "network get-tank-properties": (ctx, argv) => apiGet(ctx, argv, "读取水箱属性成功", "/tanks/properties", "tank"), + "network get-all-tanks-properties": (ctx) => apiGetAll(ctx, "读取全部水箱属性成功", "/tanks"), + "network get-pump-properties": (ctx, argv) => apiGet(ctx, argv, "读取水泵属性成功", "/pumps/properties", "pump"), + "network get-all-pumps-properties": (ctx) => apiGetAll(ctx, "读取全部水泵属性成功", "/pumps"), + "network get-valve-properties": (ctx, argv) => apiGet(ctx, argv, "读取阀门属性成功", "/valves/properties", "valve"), + "network get-all-valves-properties": (ctx) => apiGetAll(ctx, "读取全部阀门属性成功", "/valves"), +}; diff --git a/cli/src/commands/simulation.ts b/cli/src/commands/simulation.ts new file mode 100644 index 0000000..503b652 --- /dev/null +++ b/cli/src/commands/simulation.ts @@ -0,0 +1,24 @@ +import { emitApi } from "../core/http.js"; +import { parseOptions, requiredNumber, requiredString } from "../core/options.js"; +import { addMinutesPreservingOffset, parseTime } from "../core/time.js"; +import type { HandlerMap, RuntimeContext } from "../core/types.js"; + +function simulationRun(ctx: RuntimeContext, argv: string[]): Promise { + const { values } = parseOptions(argv, { duration: "integer" }); + const start = parseTime(requiredString(values, "start-time"), "--start-time"); + const duration = requiredNumber(values, "duration"); + const end = addMinutesPreservingOffset(start, duration); + return emitApi( + ctx, + "触发模拟成功", + { method: "POST", path: "/simulation-runs", body: { start_time: start.replace(/\.\d+/, ""), duration }, requireProject: true }, + [ + `tjwater-cli data timeseries realtime links --start-time ${start} --end-time ${end}`, + `tjwater-cli data timeseries realtime nodes --start-time ${start} --end-time ${end}`, + ], + ); +} + +export const simulationHandlers: HandlerMap = { + "simulation run": simulationRun, +}; diff --git a/cli/src/core/constants.ts b/cli/src/core/constants.ts new file mode 100644 index 0000000..11f6181 --- /dev/null +++ b/cli/src/core/constants.ts @@ -0,0 +1,13 @@ +export const SCHEMA_VERSION = "tjwater-cli/v1"; +export const DEFAULT_TIMEOUT = 180; +export const DEFAULT_SERVER = "http://192.168.1.114:8000"; + +export const PIPE_FIELDS = ["flow", "friction", "headloss", "quality", "reaction", "setting", "status", "velocity"] as const; +export const JUNCTION_FIELDS = ["actual_demand", "total_head", "pressure", "quality"] as const; +export const SCADA_FIELDS = ["monitored_value", "cleaned_value"] as const; + +export type ElementType = "pipe" | "junction"; +export type PipeField = (typeof PIPE_FIELDS)[number]; +export type JunctionField = (typeof JUNCTION_FIELDS)[number]; +export type ScadaField = (typeof SCADA_FIELDS)[number]; + diff --git a/cli/src/core/errors.ts b/cli/src/core/errors.ts new file mode 100644 index 0000000..a0a6c5f --- /dev/null +++ b/cli/src/core/errors.ts @@ -0,0 +1,31 @@ +export class CliError extends Error { + summary: string; + code: string; + exitCode: number; + retryable: boolean; + data: unknown; + nextCommands: string[]; + + constructor( + summary: string, + code: string, + message: string, + exitCode = 2, + retryable = false, + data: unknown = null, + nextCommands: string[] = [], + ) { + super(message); + this.summary = summary; + this.code = code; + this.exitCode = exitCode; + this.retryable = retryable; + this.data = data; + this.nextCommands = nextCommands; + } +} + +export function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + diff --git a/cli/src/core/files.ts b/cli/src/core/files.ts new file mode 100644 index 0000000..ed79e74 --- /dev/null +++ b/cli/src/core/files.ts @@ -0,0 +1,68 @@ +import { readFileSync } from "node:fs"; +import { CliError } from "./errors.js"; + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +export function readJsonFile(path: string, label: string): unknown { + try { + return JSON.parse(readFileSync(path, "utf8")); + } catch (error) { + if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { + throw new CliError("CLI 参数错误", "INPUT_NOT_FOUND", `${label} file not found: ${path}`, 2); + } + if (error instanceof SyntaxError) throw new CliError("CLI 参数错误", "INPUT_INVALID_JSON", `${label} file must be valid JSON: ${path}`, 2); + throw error; + } +} + +export function parseBurstFile(path: string): [string[], number[]] { + let raw = readJsonFile(path, "burst"); + if (isRecord(raw) && "bursts" in raw) raw = raw.bursts; + if (isRecord(raw) && "burst_id" in raw && "burst_size" in raw && Array.isArray(raw.burst_id) && Array.isArray(raw.burst_size)) { + const ids = raw.burst_id.map(String); + const sizes = raw.burst_size.map(Number); + if (ids.length !== sizes.length) throw new CliError("CLI 参数错误", "BURST_FILE_INVALID", "burst file burst_id and burst_size must have the same length", 2); + return [ids, sizes]; + } + if (Array.isArray(raw)) { + return [ + raw.map((item) => { + if (!isRecord(item) || !("id" in item) || !("size" in item)) throw new CliError("CLI 参数错误", "BURST_FILE_INVALID", "burst file items must contain id and size", 2); + return String(item.id); + }), + raw.map((item) => Number((item as Record).size)), + ]; + } + throw new CliError("CLI 参数错误", "BURST_FILE_INVALID", "burst file must be a JSON array or object with burst_id/burst_size", 2); +} + +export function parseValveSettingFile(path: string): [string[], number[]] { + const raw = readJsonFile(path, "valve-setting"); + if (isRecord(raw) && "valves" in raw && "valves_k" in raw && Array.isArray(raw.valves) && Array.isArray(raw.valves_k)) { + const valves = raw.valves.map(String); + const openings = raw.valves_k.map(Number); + if (valves.length !== openings.length) throw new CliError("CLI 参数错误", "VALVE_SETTING_INVALID", "valves and valves_k must have the same length", 2); + return [valves, openings]; + } + if (Array.isArray(raw)) { + return [ + raw.map((item) => { + if (!isRecord(item) || !("valve" in item) || !("opening" in item)) throw new CliError("CLI 参数错误", "VALVE_SETTING_INVALID", "valve-setting items must contain valve and opening", 2); + return String(item.valve); + }), + raw.map((item) => Number((item as Record).opening)), + ]; + } + throw new CliError("CLI 参数错误", "VALVE_SETTING_INVALID", "valve-setting file must be a JSON array or object with valves/valves_k", 2); +} + +export function assignDatasetKeys(target: Record, path: string, keys: string[], label: string): void { + const payload = readJsonFile(path, label); + if (isRecord(payload)) { + for (const key of keys) { + if (key in payload) target[key] = payload[key]; + } + } +} diff --git a/cli/src/core/http.ts b/cli/src/core/http.ts new file mode 100644 index 0000000..27b2af0 --- /dev/null +++ b/cli/src/core/http.ts @@ -0,0 +1,91 @@ +import { CliError, errorMessage } from "./errors.js"; +import { success } from "./output.js"; +import type { RequestOptions, RuntimeContext } from "./types.js"; + +function headers(ctx: RuntimeContext, requireAuth: boolean, requireProject: boolean): Record { + const out: Record = { + Accept: "application/json, text/plain, */*", + "X-Request-Id": ctx.requestId, + ...ctx.auth.headers, + }; + if (requireAuth) { + if (!ctx.auth.accessToken) { + throw new CliError("认证失败", "UNAUTHENTICATED", "missing access token for agent context", 3, false, null, ["provide access_token via --auth-stdin or TJWATER_ACCESS_TOKEN env var"]); + } + out.Authorization = `Bearer ${ctx.auth.accessToken}`; + } else if (ctx.auth.accessToken) out.Authorization = `Bearer ${ctx.auth.accessToken}`; + if (requireProject) { + if (!ctx.auth.projectId) throw new CliError("认证失败", "PROJECT_CONTEXT_REQUIRED", "missing project_id for agent context", 3, false, null, ["add project_id to auth context"]); + out["X-Project-Id"] = ctx.auth.projectId; + } else if (ctx.auth.projectId) out["X-Project-Id"] = ctx.auth.projectId; + return out; +} + +function stringifyParam(value: unknown): string { + if (typeof value === "boolean") return value ? "True" : "False"; + return String(value); +} + +function appendParams(url: URL, params: Record = {}): void { + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null) continue; + if (Array.isArray(value)) value.forEach((item) => url.searchParams.append(key, stringifyParam(item))); + else url.searchParams.set(key, stringifyParam(value)); + } +} + +export async function requestJson(ctx: RuntimeContext, request: RequestOptions): Promise<[unknown, number]> { + const { method, path, params, body, requireAuth = true, requireProject = false } = request; + const url = new URL(`/api/v1${path}`, ctx.server.replace(/\/+$/, "")); + appendParams(url, params); + const started = performance.now(); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), ctx.timeout * 1000); + let response: Response; + try { + response = await fetch(url, { + method: method.toUpperCase(), + headers: { + ...headers(ctx, requireAuth, requireProject), + ...(body === undefined ? {} : { "Content-Type": "application/json" }), + }, + body: body === undefined ? undefined : JSON.stringify(body), + signal: controller.signal, + }); + } catch (error) { + if (error instanceof Error && error.name === "AbortError") throw new CliError("请求超时", "REQUEST_TIMEOUT", `request timed out after ${ctx.timeout} seconds`, 7, true); + throw new CliError("连接失败", "REQUEST_FAILED", errorMessage(error), 7, true); + } finally { + clearTimeout(timer); + } + const durationMs = Math.trunc(performance.now() - started); + const contentType = response.headers.get("content-type")?.toLowerCase() ?? ""; + const text = response.status === 204 ? "" : await response.text(); + let payload: unknown = {}; + if (contentType.includes("application/json") && text) payload = JSON.parse(text); + else if (text) payload = { report: text }; + if (!response.ok) { + const record = payload && typeof payload === "object" && !Array.isArray(payload) ? (payload as Record) : {}; + const message = typeof record.detail === "string" ? record.detail : typeof record.message === "string" ? record.message : text || `http ${response.status}`; + throw new CliError("请求失败", `HTTP_${response.status}`, message, mapStatus(response.status), response.status >= 500); + } + if (payload && typeof payload === "object" && !Array.isArray(payload)) { + const record = payload as Record; + if (record.status === "error") throw new CliError("服务端错误", "SERVER_ERROR", String(record.message || "server returned error status"), 7, false, payload); + } + return [payload, durationMs]; +} + +function mapStatus(status: number): number { + if (status === 400 || status === 422) return 2; + if (status === 401) return 3; + if (status === 403) return 4; + if (status === 404) return 5; + if (status === 409 || status === 412) return 6; + return 7; +} + +export async function emitApi(ctx: RuntimeContext, summary: string, request: RequestOptions, nextCommands: string[] = []): Promise { + const [data, durationMs] = await requestJson(ctx, request); + success(summary, data, ctx, durationMs, nextCommands); +} diff --git a/cli/src/core/options.ts b/cli/src/core/options.ts new file mode 100644 index 0000000..47bbec5 --- /dev/null +++ b/cli/src/core/options.ts @@ -0,0 +1,112 @@ +import { JUNCTION_FIELDS, PIPE_FIELDS, type ElementType } from "./constants.js"; +import { CliError } from "./errors.js"; +import type { OptionSchema, ParsedOptionValue, ParsedOptions } from "./types.js"; + +export function requireValue(argv: string[], index: number, option: string): string { + const value = argv[index]; + if (!value || value.startsWith("--")) throw new CliError("CLI 参数错误", "MISSING_OPTION_VALUE", `${option} requires a value`, 2); + return value; +} + +export function parseIntStrict(value: string, option: string): number { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) throw new CliError("CLI 参数错误", "INVALID_INTEGER", `${option} must be an integer`, 2); + return parsed; +} + +export function parseFloatStrict(value: string, option: string): number { + const parsed = Number.parseFloat(value); + if (!Number.isFinite(parsed)) throw new CliError("CLI 参数错误", "INVALID_NUMBER", `${option} must be a number`, 2); + return parsed; +} + +export function parseOptions(argv: string[], schema: OptionSchema = {}): ParsedOptions { + const values: Record = {}; + const positionals: string[] = []; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]!; + if (!arg.startsWith("--")) { + positionals.push(arg); + continue; + } + const name = arg.slice(2); + const kind = schema[name] ?? "string"; + if (kind === "boolean") { + values[name] = true; + } else { + const raw = requireValue(argv, ++i, arg); + if (kind === "repeat") values[name] = [...asStringArray(values[name]), raw]; + else if (kind === "number") values[name] = parseFloatStrict(raw, arg); + else if (kind === "integer") values[name] = parseIntStrict(raw, arg); + else values[name] = raw; + } + } + return { values, positionals }; +} + +export function required(values: Record, name: string): string | number | boolean | string[] { + const value = values[name]; + if (value === undefined || value === null || value === "") { + throw new CliError("CLI 参数错误", "MISSING_PARAMETER", `Missing option '--${name}'`, 2); + } + return value; +} + +export function requiredString(values: Record, name: string): string { + const value = required(values, name); + if (typeof value !== "string") throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be a string`, 2); + return value; +} + +export function requiredNumber(values: Record, name: string): number { + const value = required(values, name); + if (typeof value !== "number") throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be a number`, 2); + return value; +} + +export function requiredStringArray(values: Record, name: string): string[] { + const value = required(values, name); + if (!Array.isArray(value)) throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be repeatable`, 2); + return value; +} + +export function optionalString(values: Record, name: string): string | undefined { + const value = values[name]; + if (value === undefined) return undefined; + if (typeof value !== "string") throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be a string`, 2); + return value; +} + +export function optionalNumber(values: Record, name: string): number | undefined { + const value = values[name]; + if (value === undefined) return undefined; + if (typeof value !== "number") throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be a number`, 2); + return value; +} + +export function optionalStringArray(values: Record, name: string): string[] | undefined { + const value = values[name]; + if (value === undefined) return undefined; + if (!Array.isArray(value)) throw new CliError("CLI 参数错误", "INVALID_PARAMETER", `--${name} must be repeatable`, 2); + return value; +} + +export function asStringArray(value: ParsedOptionValue | undefined): string[] { + if (value === undefined) return []; + if (Array.isArray(value)) return value; + throw new CliError("CLI 参数错误", "INVALID_PARAMETER", "repeat option received a non-array value", 2); +} + +export function validateChoice(value: string, valid: T, option: string): T[number] { + if (!valid.includes(value)) { + throw new CliError("CLI 参数错误", `INVALID_${option.replace(/^--/, "").replaceAll("-", "_").toUpperCase()}`, `${option} must be one of: ${valid.join(", ")}`, 2); + } + return value as T[number]; +} + +export function fieldsFor(type: ElementType): typeof PIPE_FIELDS | typeof JUNCTION_FIELDS { + if (type === "pipe") return PIPE_FIELDS; + if (type === "junction") return JUNCTION_FIELDS; + throw new CliError("CLI 参数错误", "INVALID_TYPE", "--type must be one of: pipe, junction", 2); +} + diff --git a/cli/src/core/output.ts b/cli/src/core/output.ts new file mode 100644 index 0000000..c643880 --- /dev/null +++ b/cli/src/core/output.ts @@ -0,0 +1,61 @@ +import { SCHEMA_VERSION } from "./constants.js"; +import type { RuntimeContext } from "./types.js"; + +export function json(value: unknown): void { + process.stdout.write(`${JSON.stringify(value)}\n`); +} + +export function generatedAt(): string { + return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); +} + +export function success(summary: string, data: unknown, ctx: RuntimeContext, durationMs: number, nextCommands: string[] = []): void { + json({ + ok: true, + schema_version: SCHEMA_VERSION, + summary, + data, + metadata: { + request_id: ctx.requestId, + server: ctx.server, + duration_ms: durationMs, + generated_at: generatedAt(), + }, + next_commands: nextCommands, + }); +} + +export function failure({ + summary, + code, + message, + retryable = false, + server = null, + requestId = null, + data = null, + nextCommands = [], +}: { + summary: string; + code: string; + message: string; + retryable?: boolean; + server?: string | null; + requestId?: string | null; + data?: unknown; + nextCommands?: string[]; +}): void { + json({ + ok: false, + schema_version: SCHEMA_VERSION, + summary, + error: { code, message, retryable }, + data, + metadata: { + request_id: requestId, + server, + generated_at: generatedAt(), + }, + next_commands: nextCommands, + }); +} + diff --git a/cli/src/core/runtime.ts b/cli/src/core/runtime.ts new file mode 100644 index 0000000..cf9fed9 --- /dev/null +++ b/cli/src/core/runtime.ts @@ -0,0 +1,84 @@ +import { randomUUID } from "node:crypto"; +import { DEFAULT_SERVER, DEFAULT_TIMEOUT } from "./constants.js"; +import { CliError } from "./errors.js"; +import { requireValue, parseIntStrict } from "./options.js"; +import type { AuthContext, GlobalArgs, ParsedGlobalArgs, RuntimeContext } from "./types.js"; + +function pick(source: Record, ...keys: string[]): string | null { + for (const key of keys) { + const value = source[key]; + if (value !== undefined && value !== null && value !== "") return String(value); + } + return null; +} + +function readStdin(): Promise { + return new Promise((resolve, reject) => { + let body = ""; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk: string) => { + body += chunk; + }); + process.stdin.on("end", () => resolve(body)); + process.stdin.on("error", reject); + }); +} + +export async function loadAuthContext(authStdin: boolean): Promise { + const raw = authStdin + ? (JSON.parse(await readStdin()) as Record) + : { + server: process.env.TJWATER_SERVER, + access_token: process.env.TJWATER_ACCESS_TOKEN, + project_id: process.env.TJWATER_PROJECT_ID, + headers: process.env.TJWATER_EXTRA_HEADERS ? JSON.parse(process.env.TJWATER_EXTRA_HEADERS) : {}, + }; + const headers = (raw.headers ?? {}) as unknown; + if (!headers || Array.isArray(headers) || typeof headers !== "object") { + throw new CliError("认证失败", "AUTH_CONTEXT_INVALID", "auth context headers must be a JSON object", 3); + } + return { + server: pick(raw, "server", "base_url"), + accessToken: pick(raw, "access_token", "token", "accessToken"), + projectId: pick(raw, "project_id", "projectId", "x_project_id"), + headers: Object.fromEntries(Object.entries(headers as Record).map(([key, value]) => [String(key), String(value)])), + }; +} + +export function parseGlobalArgs(argv: string[]): ParsedGlobalArgs { + const globals: GlobalArgs = { + server: null, + authStdin: false, + scheme: null, + timeout: DEFAULT_TIMEOUT, + requestId: null, + }; + const rest: string[] = []; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]!; + if (arg === "--auth-stdin") globals.authStdin = true; + else if (arg === "--server") globals.server = requireValue(argv, ++i, "--server"); + else if (arg === "--scheme") globals.scheme = requireValue(argv, ++i, "--scheme"); + else if (arg === "--timeout") globals.timeout = parseIntStrict(requireValue(argv, ++i, "--timeout"), "--timeout"); + else if (arg === "--request-id") globals.requestId = requireValue(argv, ++i, "--request-id"); + else rest.push(arg); + } + return { globals, rest }; +} + +export async function buildRuntime(globals: GlobalArgs): Promise { + const auth = await loadAuthContext(globals.authStdin); + return { + server: globals.server || auth.server || DEFAULT_SERVER, + auth, + scheme: globals.scheme, + timeout: globals.timeout, + requestId: globals.requestId || randomUUID(), + }; +} + +export function resolveScheme(ctx: RuntimeContext, explicit: string | undefined, must = false): string | null { + const scheme = explicit || ctx.scheme; + if (must && !scheme) throw new CliError("CLI 参数错误", "SCHEME_REQUIRED", "missing scheme; use --scheme", 2); + return scheme ?? null; +} diff --git a/cli/src/core/time.ts b/cli/src/core/time.ts new file mode 100644 index 0000000..47ec21e --- /dev/null +++ b/cli/src/core/time.ts @@ -0,0 +1,23 @@ +import { CliError } from "./errors.js"; + +export function parseTime(value: string, option: string): string { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + throw new CliError("CLI 参数错误", "INVALID_TIME", `${option} must be a valid ISO 8601 / RFC 3339 timestamp`, 2); + } + if (!/[zZ]|[+-]\d\d:\d\d$/.test(value)) { + throw new CliError("CLI 参数错误", "TIMEZONE_REQUIRED", `${option} must include an explicit timezone offset`, 2); + } + return value; +} + +export function addMinutesPreservingOffset(value: string, minutes: number): string { + const match = value.match(/([+-])(\d\d):(\d\d)$/); + const end = new Date(new Date(value).getTime() + minutes * 60_000); + if (!match) return end.toISOString().replace(".000Z", "Z"); + const sign = match[1] === "+" ? 1 : -1; + const offsetMinutes = sign * (Number(match[2]) * 60 + Number(match[3])); + const local = new Date(end.getTime() + offsetMinutes * 60_000); + return `${local.toISOString().replace(".000Z", "")}${match[1]}${match[2]}:${match[3]}`; +} + diff --git a/cli/src/core/types.ts b/cli/src/core/types.ts new file mode 100644 index 0000000..9f6adb7 --- /dev/null +++ b/cli/src/core/types.ts @@ -0,0 +1,83 @@ +export type JsonPrimitive = string | number | boolean | null; +export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }; +export type Dict = Record; + +export interface AuthContext { + server: string | null; + accessToken: string | null; + projectId: string | null; + headers: Record; +} + +export interface RuntimeContext { + server: string; + auth: AuthContext; + scheme: string | null; + timeout: number; + requestId: string; +} + +export interface GlobalArgs { + server: string | null; + authStdin: boolean; + scheme: string | null; + timeout: number; + requestId: string | null; +} + +export interface ParsedGlobalArgs { + globals: GlobalArgs; + rest: string[]; +} + +export type OptionKind = "string" | "boolean" | "repeat" | "number" | "integer"; +export type OptionSchema = Record; +export type ParsedOptionValue = string | number | boolean | string[]; + +export interface ParsedOptions { + values: Record; + positionals: string[]; +} + +export interface RequestOptions { + method: string; + path: string; + params?: Record; + body?: unknown; + requireAuth?: boolean; + requireProject?: boolean; +} + +export type Handler = (ctx: RuntimeContext, argv: string[]) => Promise | void; +export type HandlerMap = Record; + +export interface CommandDoc { + ok: true; + schema_version: string; + command: string; + summary: string; + description: string; + usage: string; + options: CommandOptionDoc[]; + examples: string[]; + output: string; + next_commands: string[]; +} + +export interface CommandOptionDoc { + name: string; + description: string; + required: boolean; + repeated: boolean; + default: null; +} + +export type HelpPayload = + | CommandDoc + | { + ok: true; + schema_version: string; + summary: string; + menu_level?: number; + commands: Array<{ command: string; summary: string; usage?: string; example?: string }>; + }; diff --git a/cli/src/dispatch.ts b/cli/src/dispatch.ts new file mode 100644 index 0000000..31d580b --- /dev/null +++ b/cli/src/dispatch.ts @@ -0,0 +1,50 @@ +import { CliError } from "./core/errors.js"; +import { handlers } from "./handlers.js"; +import { helpPayload } from "./help/index.js"; +import { failure, json } from "./core/output.js"; +import type { RuntimeContext } from "./core/types.js"; + +export async function dispatch(ctx: RuntimeContext | null, argv: string[]): Promise { + if (argv.length === 0 || argv.includes("--help")) { + process.stdout.write("Usage: tjwater-cli [OPTIONS] COMMAND [ARGS]...\n\nStructured JSON:\n tjwater-cli help\n"); + return; + } + if (argv[0] === "help") { + const payload = helpPayload(argv.slice(1)); + if (!payload) { + failure({ + summary: "未找到命令", + code: "COMMAND_NOT_FOUND", + message: `unknown command path: ${argv.slice(1).join(" ")}`, + retryable: false, + data: { usage: "tjwater-cli help ", examples: ["tjwater-cli help simulation run", "tjwater-cli simulation help"] }, + nextCommands: ["tjwater-cli help", "tjwater-cli help simulation"], + }); + return; + } + json(payload); + return; + } + const matched = matchCommand(argv); + if (!matched) throw new CliError("未找到命令", "COMMAND_NOT_FOUND", `No such command: ${argv.join(" ")}`, 2, false, { usage: "tjwater-cli help" }, ["tjwater-cli help"]); + const handler = handlers[matched.path]; + if (!handler) throw new CliError("未找到命令", "COMMAND_NOT_FOUND", `No such command: ${argv.join(" ")}`, 2, false, { usage: "tjwater-cli help" }, ["tjwater-cli help"]); + if (!ctx && matched.path !== "__noop") throw new CliError("CLI 参数错误", "RUNTIME_REQUIRED", "runtime context is required for command execution", 2); + await handler(ctx!, matched.args); +} + +function matchCommand(argv: string[]): { path: string; args: string[] } | null { + const paths = Object.keys(handlers).sort((a, b) => b.split(" ").length - a.split(" ").length); + for (const path of paths) { + const parts = path.split(" "); + if (parts.every((part, index) => argv[index] === part)) return { path, args: argv.slice(parts.length) }; + } + if (argv.at(-1) === "help") { + const payload = helpPayload(argv.slice(0, -1)); + if (payload) { + json(payload); + return { path: "__noop", args: [] }; + } + } + return null; +} diff --git a/cli/src/handlers.ts b/cli/src/handlers.ts new file mode 100644 index 0000000..7af85f2 --- /dev/null +++ b/cli/src/handlers.ts @@ -0,0 +1,15 @@ +import { analysisHandlers } from "./commands/analysis.js"; +import { componentHandlers } from "./commands/component.js"; +import { dataHandlers } from "./commands/data.js"; +import { networkHandlers } from "./commands/network.js"; +import { simulationHandlers } from "./commands/simulation.js"; +import type { HandlerMap } from "./core/types.js"; + +export const handlers: HandlerMap = { + "__noop": async () => {}, + ...networkHandlers, + ...componentHandlers, + ...simulationHandlers, + ...analysisHandlers, + ...dataHandlers, +}; diff --git a/cli/src/help/docs.ts b/cli/src/help/docs.ts new file mode 100644 index 0000000..15b4b12 --- /dev/null +++ b/cli/src/help/docs.ts @@ -0,0 +1,145 @@ +import { SCHEMA_VERSION } from "../core/constants.js"; +import type { CommandDoc, CommandOptionDoc } from "../core/types.js"; + +export const GROUP_SUMMARIES: Record = { + network: "管网节点、管线等基础属性查询命令。", + component: "组件选项与配置读取命令。", + "component option": "组件选项查询命令。", + simulation: "模拟运行与调度相关命令。", + analysis: "分析计算与诊断相关命令。", + "analysis leakage": "漏损分析相关命令。", + "analysis leakage schemes": "漏损方案查询命令。", + "analysis burst-detection": "爆管检测相关命令。", + "analysis burst-detection schemes": "爆管检测方案查询命令。", + "analysis burst-location": "爆管定位相关命令。", + "analysis burst-location schemes": "爆管定位方案查询命令。", + "analysis risk": "风险分析相关命令。", + "analysis sensor-placement": "传感器选址相关命令。", + data: "时序、SCADA 和方案数据查询命令。", + "data timeseries": "时序数据查询命令。", + "data timeseries realtime": "实时模拟时序查询命令。", + "data timeseries scheme": "方案时序查询命令。", + "data timeseries scada": "SCADA 时序查询命令。", + "data timeseries composite": "复合时序查询命令。", + "data scada": "SCADA 元数据查询命令。", + "data scheme": "方案数据查询命令。", +}; + +export const HIDDEN_PATH_PREFIXES = ["analysis burst-location", "analysis risk"]; + +type CommandSpec = readonly [path: string, summary: string, options: readonly string[], examples: readonly string[], nextCommands?: readonly string[]]; + +const commandSpecs: readonly CommandSpec[] = [ + ["network get-junction-properties", "读取节点属性", ["--junction "], ["tjwater-cli network get-junction-properties --junction J1"]], + ["network get-pipe-properties", "读取管道属性", ["--pipe "], ["tjwater-cli network get-pipe-properties --pipe P1"]], + ["network get-all-pipes-properties", "读取全部管道属性", [], ["tjwater-cli network get-all-pipes-properties"]], + ["network get-reservoir-properties", "读取水库属性", ["--reservoir "], ["tjwater-cli network get-reservoir-properties --reservoir R1"]], + ["network get-all-reservoirs-properties", "读取全部水库属性", [], ["tjwater-cli network get-all-reservoirs-properties"]], + ["network get-tank-properties", "读取水箱属性", ["--tank "], ["tjwater-cli network get-tank-properties --tank T1"]], + ["network get-all-tanks-properties", "读取全部水箱属性", [], ["tjwater-cli network get-all-tanks-properties"]], + ["network get-pump-properties", "读取水泵属性", ["--pump "], ["tjwater-cli network get-pump-properties --pump PU1"]], + ["network get-all-pumps-properties", "读取全部水泵属性", [], ["tjwater-cli network get-all-pumps-properties"]], + ["network get-valve-properties", "读取阀门属性", ["--valve "], ["tjwater-cli network get-valve-properties --valve V1"]], + ["network get-all-valves-properties", "读取全部阀门属性", [], ["tjwater-cli network get-all-valves-properties"]], + ["component option schema", "读取选项 schema", ["--kind ", "[--pump ]"], ["tjwater-cli component option schema --kind time", "tjwater-cli component option schema --kind energy", "tjwater-cli component option schema --kind pump-energy --pump PUMP1", "tjwater-cli component option schema --kind network"]], + ["component option get", "读取选项属性", ["--kind ", "[--pump ]"], ["tjwater-cli component option get --kind time", "tjwater-cli component option get --kind energy", "tjwater-cli component option get --kind pump-energy --pump PUMP1", "tjwater-cli component option get --kind network"]], + ["simulation run", "触发指定绝对时间的模拟运行", ["--start-time ", "--duration "], ["tjwater-cli simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30"], ["tjwater-cli data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00", "tjwater-cli data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00"]], + ["analysis burst", "执行爆管分析", ["--start-time ", "--duration ", "--burst-file ", "[--scheme ]"], ["tjwater-cli analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 900 --burst-file ./burst.json --scheme burst_case_01", "tjwater-cli data scheme get --name burst_case_01", "tjwater-cli data scheme list"]], + ["analysis valve", "阀门工况分析。", ["--mode ", "[--start-time ]", "[--valve ]", "[--element ]", "[--disabled-valve ]", "[--duration ]", "[--scheme ]"], ["tjwater-cli analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --valve V2 --duration 900 --scheme valve_case_01", "tjwater-cli analysis valve --mode isolation --element E1 --element E2", "tjwater-cli analysis valve --mode isolation --element E1 --disabled-valve V3"]], + ["analysis flushing", "执行冲洗分析", ["--start-time ", "--valve-setting-file ", "--drainage-node ", "--flow ", "[--duration ]", "[--scheme ]"], ["tjwater-cli analysis flushing --start-time 2025-01-02T03:04:05+08:00 --valve-setting-file ./valve.json --drainage-node N1 --flow 100.0 --duration 900 --scheme flush_case_01"]], + ["analysis age", "执行水龄分析", ["--start-time ", "--duration "], ["tjwater-cli analysis age --start-time 2025-01-02T03:04:05+08:00 --duration 900"]], + ["analysis contaminant", "执行污染物模拟", ["--start-time ", "--duration ", "--source-node ", "--concentration ", "[--pattern ]", "[--scheme ]"], ["tjwater-cli analysis contaminant --start-time 2025-01-02T03:04:05+08:00 --duration 900 --source-node N1 --concentration 10.0 --scheme contam_case_01"]], + ["analysis sensor-placement kmeans", "执行 KMeans 传感器选址", ["--count ", "[--min-diameter ]", "[--scheme ]"], ["tjwater-cli analysis sensor-placement kmeans --count 5 --min-diameter 100 --scheme placement_case_01"]], + ["analysis leakage identify", "执行漏损识别", ["--start-time ", "--end-time ", "[--scheme ]"], ["tjwater-cli analysis leakage identify --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme leak_case_01"]], + ["analysis leakage schemes list", "列出漏损方案", [], ["tjwater-cli analysis leakage schemes list"]], + ["analysis leakage schemes get", "读取漏损方案详情", [""], ["tjwater-cli analysis leakage schemes get my_scheme"]], + ["analysis burst-detection detect", "执行爆管检测", ["--start-time ", "--end-time ", "[--scheme ]"], ["tjwater-cli analysis burst-detection detect --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme detect_case_01"]], + ["analysis burst-detection schemes list", "列出爆管检测方案", [], ["tjwater-cli analysis burst-detection schemes list"]], + ["analysis burst-detection schemes get", "读取爆管检测方案详情", [""], ["tjwater-cli analysis burst-detection schemes get my_scheme"]], + ["analysis burst-location locate", "执行爆管定位", ["--start-time ", "--end-time ", "--burst-leakage ", "[--scheme ]"], ["tjwater-cli analysis burst-location locate --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --burst-leakage 100.0 --scheme locate_case_01"]], + ["analysis burst-location schemes list", "列出爆管定位方案", [], ["tjwater-cli analysis burst-location schemes list"]], + ["analysis burst-location schemes get", "读取爆管定位方案详情", [""], ["tjwater-cli analysis burst-location schemes get my_scheme"]], + ["analysis risk pipe-now", "读取单条管道当前风险", ["--pipe "], ["tjwater-cli analysis risk pipe-now --pipe P1"]], + ["analysis risk pipe-history", "读取单条管道历史风险", ["--pipe "], ["tjwater-cli analysis risk pipe-history --pipe P1"]], + ["analysis risk network", "读取全网风险", [], ["tjwater-cli analysis risk network"]], + ["data timeseries realtime links", "查询实时管道时序", ["--start-time ", "--end-time "], ["tjwater-cli data timeseries realtime links --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00"]], + ["data timeseries realtime nodes", "查询实时节点时序", ["--start-time ", "--end-time "], ["tjwater-cli data timeseries realtime nodes --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00"]], + ["data timeseries realtime simulation-by-id-time", "按元素和时间查询实时模拟结果", ["--id ", "--type ", "--time