Compare commits
154
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c09eb39cd | ||
|
|
72ebf4d6c1 | ||
|
|
a438433068 | ||
|
|
4ec010455b | ||
|
|
80cfc1f2ab | ||
|
|
ce04704af2 | ||
|
|
004c9bb72d | ||
|
|
774f39cbbe | ||
|
|
c6efccb88a | ||
|
|
11ebf428bb | ||
|
|
18e8b25f48 | ||
|
|
9aa5a96e60 | ||
|
|
a5f6474be5 | ||
|
|
b49290bd91 | ||
|
|
31fdb36e48 | ||
|
|
0a64de89bb | ||
|
|
2f267af7a3 | ||
|
|
649af949c5 | ||
|
|
c565e89d60 | ||
|
|
9c9e31c570 | ||
|
|
c0c54e238d | ||
|
|
99f5a0b823 | ||
|
|
4a9681c148 | ||
|
|
8530793882 | ||
|
|
cb3aa3a150 | ||
|
|
4cbeca4e09 | ||
|
|
f66b9c3e9d | ||
|
|
b19af8846a | ||
|
|
a5e91ac2b8 | ||
|
|
258f4996eb | ||
|
|
2dc37e3fd8 | ||
|
|
a53839e157 | ||
|
|
1407dd3bbe | ||
|
|
764a1f4e82 | ||
|
|
07016451d6 | ||
|
|
94529cb141 | ||
|
|
2415f75841 | ||
|
|
d7faaa2ecb | ||
|
|
78f9f6fb42 | ||
|
|
9aad38acb1 | ||
|
|
d782b7fa11 | ||
|
|
40c0395fb1 | ||
|
|
d75b98a61b | ||
|
|
ead76185b7 | ||
|
|
8857b18dc9 | ||
|
|
4b572d9264 | ||
|
|
fc7393fa2b | ||
|
|
c823e3935e | ||
|
|
366c05b752 | ||
|
|
fa2c28c1c0 | ||
|
|
cf6cada538 | ||
|
|
d1b91a4b1e | ||
|
|
20b93c688f | ||
|
|
873c169c2c | ||
|
|
8ed73b1da6 | ||
|
|
60b9080c47 | ||
|
|
801f611ce5 | ||
|
|
15c3263369 | ||
|
|
f20847399a | ||
|
|
f3b62ed108 | ||
|
|
0e1ca2418f | ||
|
|
f61389ab07 | ||
|
|
05d36aa8ca | ||
|
|
4e31b141e7 | ||
|
|
7dbbeb4aa9 | ||
|
|
6f3b72628f | ||
|
|
2295bdcb97 | ||
|
|
359b1e6e55 | ||
|
|
93d70da8be | ||
|
|
ff87817fb5 | ||
|
|
4b03aa3a91 | ||
|
|
8a7964dc57 | ||
|
|
741e39b444 | ||
|
|
5020e58b7e | ||
|
|
ba46258845 | ||
|
|
9d4e5486e9 | ||
|
|
1ed7e56f35 | ||
|
|
5e0c16f8b2 | ||
|
|
67d027e60c | ||
|
|
8f0e93ceec | ||
|
|
ad31956f53 | ||
|
|
fc0e76439d | ||
|
|
10c11a5254 | ||
|
|
f4749d6e2e | ||
|
|
8a1785c244 | ||
|
|
0188240d62 | ||
|
|
0ecb2babf3 | ||
|
|
04ded0ceb0 | ||
|
|
76d4b510f4 | ||
|
|
96e5d25518 | ||
|
|
a825c3c31d | ||
|
|
5b285ad7a5 | ||
|
|
20329bb771 | ||
|
|
5ac50bfeaa | ||
|
|
4c47841483 | ||
|
|
ab12d79d91 | ||
|
|
7427d08d6c | ||
|
|
f7122d1260 | ||
|
|
5d80961930 | ||
|
|
7e63d38cf5 | ||
|
|
e0b81c2114 | ||
|
|
cb298f2099 | ||
|
|
4870e8a577 | ||
|
|
f24e8109a0 | ||
|
|
725935e270 | ||
|
|
6c53e12962 | ||
|
|
0b5004fc2c | ||
|
|
872570ac3a | ||
|
|
23d8249286 | ||
|
|
bd04444d9d | ||
|
|
96d894d1e0 | ||
|
|
105dfea18e | ||
|
|
53a423cafe | ||
|
|
9dffa59603 | ||
|
|
97fea698f0 | ||
|
|
dbeb2084cf | ||
|
|
3eb5829053 | ||
|
|
fe09b02393 | ||
|
|
a6f6e633f0 | ||
|
|
61702d095a | ||
|
|
1234d28536 | ||
|
|
5e5f2494ac | ||
|
|
4690a0980b | ||
|
|
0ad3bd4d89 | ||
|
|
7b4f479aad | ||
|
|
6584239e75 | ||
|
|
d56f516161 | ||
|
|
d0cb19c521 | ||
|
|
8b74e98291 | ||
|
|
1ac46814ad | ||
|
|
ef3253d895 | ||
|
|
8439d56b42 | ||
|
|
8b02cae2af | ||
|
|
69a90de9a1 | ||
|
|
3e3deaa724 | ||
|
|
eebf802e31 | ||
|
|
f150c602e5 | ||
|
|
3ebcd98ec5 | ||
|
|
61b1018900 | ||
|
|
f58abe8003 | ||
|
|
3d85f13f26 | ||
|
|
0d5435022a | ||
|
|
59de5c672f | ||
|
|
93cba2f391 | ||
|
|
61e9fa94ac | ||
|
|
cbaa1099de | ||
|
|
5fbe8ae40c | ||
|
|
a27c45910c | ||
|
|
37f5bd8a80 | ||
|
|
65fb368f40 | ||
|
|
9fa24b39f3 | ||
|
|
a9bab86d64 | ||
|
|
2473117198 | ||
|
|
e5d780efce |
+4
-1
@@ -1,8 +1,11 @@
|
||||
.git
|
||||
.gitignore
|
||||
node_modules
|
||||
.opencode/node_modules
|
||||
.env
|
||||
.env.*
|
||||
.local.env
|
||||
data/
|
||||
logs/
|
||||
dist
|
||||
.vscode
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Agent CI/CD v2
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build-test-publish-and-deploy:
|
||||
uses: OrgTJWater/ci-templates/.gitea/workflows/container-cd.yml@main
|
||||
with:
|
||||
image_name: gitea.waternetwork.cn/orgtjwater/tjwateragent
|
||||
dockerfile: Dockerfile
|
||||
build_context: .
|
||||
cache_image: gitea.waternetwork.cn/orgtjwater/tjwateragent:ci-cache
|
||||
test_target: test
|
||||
deploy_service: agent
|
||||
deploy_host: 192.168.1.114
|
||||
secrets:
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
DEV_DEPLOY_SSH_KEY: ${{ secrets.DEV_DEPLOY_SSH_KEY }}
|
||||
@@ -1,4 +1,10 @@
|
||||
node_modules/
|
||||
__pycache__/
|
||||
.opencode/node_modules/
|
||||
.local.env
|
||||
.vscode
|
||||
docker-compose.yml
|
||||
data/
|
||||
logs/
|
||||
AGENT_HARNESS_REPORT.md
|
||||
HARNESS_INTRODUCTION.md
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
package-lock.json
|
||||
.gitignore
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
description: TJWater Agent,用于供水网络分析和操作员工作流
|
||||
mode: primary
|
||||
model: deepseek/deepseek-v4-pro
|
||||
temperature: 0.2
|
||||
---
|
||||
您是运行在 opencode 上的默认 TJWater Agent。
|
||||
|
||||
按照以下规则操作:
|
||||
|
||||
1. 使用 `.opencode/skills/tjwater-skills-root-index` 作为 TJWater 技能树,仅在任务需要该领域知识时加载特定技能。
|
||||
2. 当您需要后端数据用于推理、总结、诊断或分析时,优先使用 `dynamic_http_call`。
|
||||
3. 当用户主要需要 UI 操作或可视化时,优先使用前端工具(`locate_features`、`view_history`、`view_scada`、`show_chart`)。
|
||||
4. 仅将前端工具视为显示/交互工具,不要假设它们返回数据。
|
||||
5. 保持回复准确、简洁,对供水网络用户在操作上有用。
|
||||
6. 尊重用户授权和项目隔离,工具调用失败或无可用数据时,切勿编造后端结果。
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
description: TJWater Agent,用于供水网络分析和操作员工作流
|
||||
mode: primary
|
||||
model: deepseek/deepseek-flash
|
||||
---
|
||||
你是 TJWater 供水管网分析 Agent,运用水力专业知识,回复用户时使用简体中文,内容要求简洁准确。
|
||||
|
||||
## 回复要求
|
||||
|
||||
- 工具执行期间不输出过程说明,全部完成后只回复最终结果
|
||||
- 最终回答必须通过 `final_answer` 提交;调用前必须完成全部业务动作和其他工具,调用后禁止继续调用工具或输出额外文本
|
||||
- 直接给出结论、关键数据和可执行建议,默认仅展示最重要的 Top 5;数据不足或任务失败时简要说明影响和下一步
|
||||
- 多步骤或预计超过 30 秒的任务,开始时使用 `todowrite` 给用户展示计划;简单问答不创建计划
|
||||
- `todowrite` 是面向用户的业务任务摘要:每项只描述目标或可验证结果,不出现函数名、脚本/文件名、命令、工具名、参数、内部目录或具体修复实现;这些技术细节仅保留在工具过程信息中
|
||||
- 任务标题使用简洁的业务语言,例如“准备供水分区所需数据”“计算供水服务范围”“生成并展示分析结果”“整理可复用分析经验”
|
||||
- 开始工作及每次进入新的业务阶段时调用一次 `activity_update`,用 `title` 概括当前阶段、用 `reason` 说明该阶段为何必要;已有计划时必须通过 `todos` 提交完整计划状态快照,使阶段与任务状态同时更新,不再单独调用 `todowrite` 更新里程碑
|
||||
- `activity_update` 是过程分组,不是任务清单:活动描述当前正在做的一组动作,`todowrite` 描述整个任务的业务目标与完成状态
|
||||
|
||||
## 工作流生命周期
|
||||
|
||||
Skills 树是**动态生长的**——工作流不是预置的,而是从实际任务中沉淀出来的:
|
||||
```
|
||||
初次遇到问题 → tjwater_cli + Python 脚本拼装 → 验证有效 →
|
||||
→ 立即调用 skill_manager 保存到 skills/workflow/<name>/
|
||||
→ 下次遇到同类问题直接加载该 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` 渲染到前端 |
|
||||
|
||||
**前端工具仅做显示,不返回数据**,不要假设其返回内容。
|
||||
|
||||
`tjwater_cli.command` 虽然是字符串,但命令空间不是可类推的层级语法。当前会话尚未验证某个完整命令路径和参数时,先调用 `help <命令族或前缀>`;已加载工作流中明确记录且已验证的固定命令可直接使用。禁止根据 `analysis runs` 等已有路径创造其他命令族的同名子路径。收到 `COMMAND_NOT_FOUND` 后只执行返回的 `next_commands` 做命令发现,不得继续猜测近似命令。
|
||||
|
||||
## 执行约束
|
||||
|
||||
1. 普通工具不填写重复的调用理由,具体动作自动归入当前 `activity_update` 活动;切换业务阶段前先更新活动
|
||||
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` 子代理;当前前端无法观测和干预子代理的具体工作过程
|
||||
7. Bash 始终运行在当前对话专属沙箱中:只能读写当前对话目录、读取 skills 和 Python 环境,不能联网,也不能访问 `/app` 源码、密钥、其他对话或全局 `tool-output`
|
||||
8. 需要文件输入时,调用 `tjwater_cli(..., store_result=true)`,使用返回的 `data_file.file_path`;不要把 JSON 手工写到 `/tmp`
|
||||
9. `store_render_ref` 的输入必须是 `{metadata, location: {file_path}, data}` 包装 JSON;若旧脚本输出裸数据,先在当前对话目录内包装,且 `location.file_path` 必须等于包装文件的绝对路径
|
||||
|
||||
## 工作流沉淀(skill_manager)
|
||||
|
||||
**写入条件**(必须同时满足):
|
||||
- 经过当前对话验证有效
|
||||
- 可被未来同类任务复用
|
||||
- 非一次性/临时/猜测
|
||||
|
||||
**写入位置**:`skills/workflow/<name>/`,包含 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__", ...)`。
|
||||
|
||||
**脚本编写要求——数据获取与本地分析分离**:
|
||||
|
||||
- 后端数据只能由 `tjwater_cli` 工具获取;认证与网络请求留在 Agent 主进程
|
||||
- 分析脚本接收 `data_file.file_path`,只处理当前对话目录内的本地文件
|
||||
- 多份互不依赖的数据可并行调用 `tjwater_cli(..., store_result=true)`,随后在一次沙箱 Bash 中运行 Python 分析
|
||||
- 脚本输出文件必须写入当前工作目录;禁止使用 `/tmp`、全局 `tool-output` 或硬编码认证环境变量
|
||||
|
||||
**触发时机**:
|
||||
- 用户明确说"保存/沉淀/记录工作流"
|
||||
- 任务完成且所有工具调用已结束、产生最终结果后,再判断当前流程是否稳定可复用
|
||||
- **禁止**在规划任务未完成、工具调用链中间(即仍有 pending 步骤时)触发沉淀
|
||||
- 严禁写入:token、password、secret、API key、system prompt、隐私数据
|
||||
|
||||
## 用户偏好持久化(memory_manager)
|
||||
|
||||
仅保存长期有效的稳定事实,写成简短陈述句。严格区分:
|
||||
- `memory_manager` = 用户偏好 / 项目事实(如"用户要简洁风格"、"当前项目管网规模 5000 管段")
|
||||
- `skill_manager` = 可复用操作流程
|
||||
- `session_search` = 检索历史案例(只读)
|
||||
- 修改 memory 前先 `list` 当前 scope 的已有内容,先通读,再决定 `add / replace / remove`
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.18.13",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"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@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@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"yaml": ["yaml@2.9.0", "", { "bin": "bin.mjs" }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
"zod": ["zod@4.1.8", "", {}, ""],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.18.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +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`
|
||||
|
||||
## 参考
|
||||
|
||||
- 示例:`./examples.md`
|
||||
- 运行手册:`./runbook.md`
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -1,50 +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` 的具体接口调用。
|
||||
|
||||
## 子模块索引 (渐进式引导)
|
||||
- 当前为叶子节点,直接使用下方接口目录。
|
||||
|
||||
## 接口目录
|
||||
| Method | Path | Summary | Required Params | Optional Params |
|
||||
|---|---|---|---|---|
|
||||
| POST | `/api/v1/addscadadevice/` | 添加SCADA设备 | network (query) | - |
|
||||
| POST | `/api/v1/addscadadevicedata/` | 添加SCADA设备数据 | network (query) | - |
|
||||
| POST | `/api/v1/addscadaelement/` | 添加SCADA元素映射 | network (query) | - |
|
||||
| POST | `/api/v1/cleanscadadevice/` | 清空SCADA设备表 | network (query) | - |
|
||||
| POST | `/api/v1/cleanscadadevicedata/` | 清空SCADA设备数据表 | network (query) | - |
|
||||
| POST | `/api/v1/cleanscadaelement/` | 清空SCADA元素映射表 | network (query) | - |
|
||||
| POST | `/api/v1/deletescadadevice/` | 删除SCADA设备 | network (query) | - |
|
||||
| POST | `/api/v1/deletescadadevicedata/` | 删除SCADA设备数据 | network (query) | - |
|
||||
| POST | `/api/v1/deletescadaelement/` | 删除SCADA元素映射 | network (query) | - |
|
||||
| GET | `/api/v1/getallscadadeviceids/` | 获取所有SCADA设备ID | network (query) | - |
|
||||
| GET | `/api/v1/getallscadadevices/` | 获取所有SCADA设备 | network (query) | - |
|
||||
| GET | `/api/v1/getallscadainfo/` | 获取所有SCADA信息 | network (query) | - |
|
||||
| GET | `/api/v1/getallscadaproperties/` | 获取所有SCADA属性 | network (query) | - |
|
||||
| GET | `/api/v1/getscadadevice/` | 获取SCADA设备 | network (query), id (query) | - |
|
||||
| GET | `/api/v1/getscadadevicedata/` | 获取SCADA设备数据 | network (query), device_id (query) | - |
|
||||
| GET | `/api/v1/getscadadevicedataschema/` | 获取SCADA设备数据架构 | network (query) | - |
|
||||
| GET | `/api/v1/getscadadeviceschema/` | 获取SCADA设备架构 | network (query) | - |
|
||||
| GET | `/api/v1/getscadaelement/` | 获取单个SCADA元素映射 | network (query), id (query) | - |
|
||||
| GET | `/api/v1/getscadaelements/` | 获取所有SCADA元素映射 | network (query) | - |
|
||||
| GET | `/api/v1/getscadaelementschema/` | 获取SCADA元素架构 | network (query) | - |
|
||||
| GET | `/api/v1/getscadainfo/` | 获取SCADA信息 | network (query), id (query) | - |
|
||||
| GET | `/api/v1/getscadainfoschema/` | 获取SCADA信息架构 | network (query) | - |
|
||||
| GET | `/api/v1/getscadaproperties/` | 获取SCADA属性 | network (query), scada (query) | - |
|
||||
| POST | `/api/v1/scada/batch` | 批量插入SCADA监测数据 | data (body) | - |
|
||||
| DELETE | `/api/v1/scada/by-id-time-range` | 按设备ID和时间范围删除SCADA数据 | device_id (query), start_time (query), end_time (query) | - |
|
||||
| GET | `/api/v1/scada/by-ids-field-time-range` | 按设备ID、字段和时间范围查询SCADA数据 | start_time (query), end_time (query), field (query), device_ids (query) | - |
|
||||
| GET | `/api/v1/scada/by-ids-time-range` | 按设备ID和时间范围查询SCADA数据 | start_time (query), end_time (query), device_ids (query) | - |
|
||||
| PATCH | `/api/v1/scada/{device_id}/field` | 更新SCADA设备字段 | device_id (path), time (query), field (query), value (query) | - |
|
||||
| POST | `/api/v1/setscadadevice/` | 更新SCADA设备 | network (query) | - |
|
||||
| POST | `/api/v1/setscadadevicedata/` | 更新SCADA设备数据 | network (query) | - |
|
||||
| POST | `/api/v1/setscadaelement/` | 更新SCADA元素映射 | network (query) | - |
|
||||
|
||||
- 覆盖方法:`DELETE, GET, PATCH, POST`
|
||||
@@ -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`
|
||||
|
||||
@@ -1,22 +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`
|
||||
@@ -1,22 +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`
|
||||
@@ -1,22 +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`
|
||||
@@ -1,24 +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/getnetworkpiperiskprobabilitynow/` | 获取整个网络的管道风险概率 | network (query) | - |
|
||||
| GET | `/api/v1/getpiperiskprobability/` | 获取管道风险概率历史 | network (query), pipe_id (query) | - |
|
||||
| GET | `/api/v1/getpiperiskprobabilitygeometries/` | 获取管道风险几何信息 | network (query) | - |
|
||||
| GET | `/api/v1/getpiperiskprobabilitynow/` | 获取管道当前风险概率 | network (query), pipe_id (query) | - |
|
||||
| GET | `/api/v1/getpipesriskprobability/` | 批量获取多条管道风险概率 | network (query), pipe_ids (query) | - |
|
||||
|
||||
- 覆盖方法:`GET`
|
||||
@@ -1,48 +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/age_analysis/` | 水龄分析(高级) | network (query), start_time (query), end_time (query), duration (query) | - |
|
||||
| GET | `/api/v1/ageanalysis/` | 水龄分析(基础) | network (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/burstanalysis/` | 爆管分析(基础) | network (query), pipe_id (query), start_time (query), end_time (query), burst_flow (query) | - |
|
||||
| GET | `/api/v1/contaminant_simulation/` | 污染物模拟 | network (query), start_time (query), source (query), concentration (query), duration (query) | scheme_name (query), pattern (query) |
|
||||
| POST | `/api/v1/daily_scheduling_analysis/` | 日排程分析 | data (body) | - |
|
||||
| GET | `/api/v1/dumpoutput/` | 导出模拟输出 | output (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/flushinganalysis/` | 冲洗分析(基础) | network (query), pipe_id (query), start_time (query), duration (query), flow (query) | - |
|
||||
| POST | `/api/v1/network_project/` | 导入网络项目 | file (file) | - |
|
||||
| POST | `/api/v1/network_update/` | 管网更新(高级) | file (file) | - |
|
||||
| GET | `/api/v1/networkupdate/` | 管网更新(基础) | network (query) | - |
|
||||
| POST | `/api/v1/pressure_regulation/` | 压力调节(高级) | data (body) | - |
|
||||
| POST | `/api/v1/pressure_sensor_placement_kmeans/` | 压力传感器放置-KMeans聚类分析(高级) | data (body) | - |
|
||||
| POST | `/api/v1/pressure_sensor_placement_sensitivity/` | 压力传感器放置-灵敏度分析(高级) | data (body) | - |
|
||||
| GET | `/api/v1/pressureregulation/` | 压力调节(基础) | network (query), target_node (query), target_pressure (query) | - |
|
||||
| GET | `/api/v1/pressuresensorplacementkmeans/` | 压力传感器放置-KMeans聚类分析(基础) | name (query), scheme_name (query), sensor_number (query), min_diameter (query), username (query) | - |
|
||||
| GET | `/api/v1/pressuresensorplacementsensitivity/` | 压力传感器放置-灵敏度分析(基础) | name (query), scheme_name (query), sensor_number (query), min_diameter (query), username (query) | - |
|
||||
| POST | `/api/v1/project_management/` | 项目管理(高级) | data (body) | - |
|
||||
| GET | `/api/v1/projectmanagement/` | 项目管理(基础) | network (query) | - |
|
||||
| POST | `/api/v1/pump_failure/` | 泵故障管理 | data (body) | - |
|
||||
| GET | `/api/v1/runinp/` | 运行INP文件 | network (query) | - |
|
||||
| GET | `/api/v1/runproject/` | 运行项目模拟 | network (query) | - |
|
||||
| GET | `/api/v1/runprojectreturndict/` | 运行项目模拟(返回字典) | network (query) | - |
|
||||
| POST | `/api/v1/runsimulationmanuallybydate/` | 手动运行日期指定模拟 | data (body) | - |
|
||||
| POST | `/api/v1/scheduling_analysis/` | 排程分析 | 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) |
|
||||
| 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/valvecloseanalysis/` | 阀门关闭分析(基础) | network (query), valve_id (query), start_time (query), end_time (query) | - |
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -1,25 +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`
|
||||
@@ -1,26 +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`
|
||||
@@ -1,31 +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`
|
||||
@@ -1,26 +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`
|
||||
@@ -1,44 +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`
|
||||
@@ -1,34 +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`
|
||||
@@ -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`
|
||||
|
||||
@@ -1,24 +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` | login | form_data (body) | - |
|
||||
| POST | `/api/v1/auth/login/simple` | login_simple | username (query), password (query) | - |
|
||||
| GET | `/api/v1/auth/me` | get_current_user_info | - | - |
|
||||
| POST | `/api/v1/auth/refresh` | refresh_token | refresh_token (query) | - |
|
||||
| POST | `/api/v1/auth/register` | register | user_data (body) | - |
|
||||
|
||||
- 覆盖方法:`GET, POST`
|
||||
@@ -1,25 +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`
|
||||
@@ -1,22 +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`
|
||||
@@ -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`
|
||||
|
||||
@@ -1,25 +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`
|
||||
@@ -1,48 +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`
|
||||
@@ -1,25 +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`
|
||||
@@ -1,37 +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`
|
||||
@@ -1,39 +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`
|
||||
@@ -1,29 +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`
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
name: tjwater-action-business-network-assets-regions
|
||||
description: business/network-assets 下 regions 操作技能。
|
||||
version: 3.0.0
|
||||
---
|
||||
|
||||
# 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/calculatedistrictmeteringarea/` | 计算DMA分区 | 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/calculateregion/` | 计算区域 | network (query), time_index (query) | - |
|
||||
| GET | `/api/v1/calculateservicearea/` | 计算服务区 | network (query), time_index (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/generateregion/` | 生成区域分区 | network (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/getallregions/` | 获取所有区域 | 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`
|
||||
@@ -1,35 +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`
|
||||
@@ -1,23 +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`
|
||||
@@ -1,47 +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`
|
||||
@@ -1,36 +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`
|
||||
@@ -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`
|
||||
|
||||
@@ -1,23 +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`
|
||||
@@ -1,25 +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`
|
||||
@@ -1,23 +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`
|
||||
@@ -1,48 +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 |
|
||||
|---|---|---|---|---|
|
||||
| POST | `/api/v1/closeproject/` | 关闭项目 | network (query) | - |
|
||||
| GET | `/api/v1/convertv3tov2/` | 转换 INP V3 为 V2 | - | - |
|
||||
| GET | `/api/v1/convertv3tov2/` | 转换 INP V3 为 V2 | - | - |
|
||||
| POST | `/api/v1/copyproject/` | 复制项目 | source (query), target (query) | - |
|
||||
| POST | `/api/v1/createproject/` | 创建新项目 | network (query) | - |
|
||||
| POST | `/api/v1/deleteproject/` | 删除项目 | network (query) | - |
|
||||
| GET | `/api/v1/downloadinp/` | 下载 INP 文件 | name (query) | - |
|
||||
| GET | `/api/v1/downloadinp/` | 下载 INP 文件 | name (query) | - |
|
||||
| GET | `/api/v1/dumpinp/` | 导出项目到 INP 文件 | network (query), inp (query) | - |
|
||||
| GET | `/api/v1/dumpinp/` | 导出项目到 INP 文件 | network (query), inp (query) | - |
|
||||
| GET | `/api/v1/exportinp/` | 导出项目为 ChangeSet | network (query), version (query) | - |
|
||||
| GET | `/api/v1/haveproject/` | 检查项目是否存在 | network (query) | - |
|
||||
| POST | `/api/v1/importinp/` | 导入 INP 文件内容 | network (query) | - |
|
||||
| GET | `/api/v1/isprojectlocked/` | 检查项目是否被锁定 | network (query) | - |
|
||||
| GET | `/api/v1/isprojectlocked/` | 检查项目是否被锁定 | network (query) | - |
|
||||
| GET | `/api/v1/isprojectlockedbyme/` | 检查项目是否被当前用户锁定 | network (query) | - |
|
||||
| GET | `/api/v1/isprojectlockedbyme/` | 检查项目是否被当前用户锁定 | network (query) | - |
|
||||
| GET | `/api/v1/isprojectopen/` | 检查项目是否已打开 | network (query) | - |
|
||||
| GET | `/api/v1/listprojects/` | 获取项目列表 | - | - |
|
||||
| POST | `/api/v1/lockproject/` | 锁定项目 | network (query) | - |
|
||||
| POST | `/api/v1/lockproject/` | 锁定项目 | network (query) | - |
|
||||
| POST | `/api/v1/openproject/` | 打开项目 | network (query) | - |
|
||||
| GET | `/api/v1/project_info/` | 获取项目信息 | network (query) | - |
|
||||
| POST | `/api/v1/readinp/` | 读取 INP 文件到项目 | network (query), inp (query) | - |
|
||||
| POST | `/api/v1/readinp/` | 读取 INP 文件到项目 | network (query), inp (query) | - |
|
||||
| POST | `/api/v1/unlockproject/` | 解锁项目 | network (query) | - |
|
||||
| POST | `/api/v1/unlockproject/` | 解锁项目 | network (query) | - |
|
||||
| POST | `/api/v1/uploadinp/` | 上传 INP 文件 | afile (body), name (query) | - |
|
||||
| POST | `/api/v1/uploadinp/` | 上传 INP 文件 | afile (body), name (query) | - |
|
||||
|
||||
- 覆盖方法:`GET, POST`
|
||||
@@ -1,22 +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`
|
||||
@@ -1,37 +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`
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: tjwater-domain-data
|
||||
description: 负责时序数据访问与读写能力。
|
||||
version: 3.0.0
|
||||
---
|
||||
|
||||
# Data Domain Skill
|
||||
|
||||
## 简介
|
||||
负责时序数据访问与读写能力。
|
||||
|
||||
## 子模块索引 (渐进式引导)
|
||||
- **timeseries-access**: 见 `./timeseries-access/SKILL.md`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -1,24 +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`
|
||||
@@ -1,29 +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`
|
||||
@@ -1,31 +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`
|
||||
@@ -1,128 +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 <token>`
|
||||
- `x-project-id: <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 监测面板,展示该节点的历史监测曲线。
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: tjwater-domain-platform
|
||||
description: 负责治理、审计、缓存与元数据能力。
|
||||
version: 3.0.0
|
||||
---
|
||||
|
||||
# Platform Domain Skill
|
||||
|
||||
## 简介
|
||||
负责治理、审计、缓存与元数据能力。
|
||||
|
||||
## 子模块索引 (渐进式引导)
|
||||
- **governance-observability**: 见 `./governance-observability/SKILL.md`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -1,22 +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`
|
||||
@@ -1,23 +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`
|
||||
@@ -1,22 +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`
|
||||
@@ -1,92 +0,0 @@
|
||||
# API Skills 使用 Runbook(工具调用链)
|
||||
|
||||
## 1) 总体原则
|
||||
|
||||
- Skills 负责“告诉模型可做什么”。
|
||||
- `chat/stream` 内部启动 opencode 会话,并注册工具 `dynamic_http_call`。
|
||||
- opencode agent 通过工具调用后端能力,不直接发 HTTP。
|
||||
- TJWaterAgent 执行器负责“代表当前用户调真实后端 API”(动态路径,无白名单)。
|
||||
|
||||
## 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 参数(列表会转为逗号拼接)。
|
||||
|
||||
## 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`,前端直接渲染图表,不再请求后端。
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
name: tjwater-cli
|
||||
description: tjwater-cli 命令行工具使用说明,涵盖命令发现、输出格式、命令族、错误处理及最佳实践。
|
||||
---
|
||||
|
||||
# tjwater-cli 使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
`tjwater-cli` 是 TJWater 供水管网系统的命令行工具,用于与后端服务交互,支持数据查询、分析和工程操作。所有输出统一为 JSON 格式。
|
||||
|
||||
## 工具调用
|
||||
|
||||
通过 `tjwater_cli` 工具执行 CLI 命令:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "project list",
|
||||
"timeout": 120,
|
||||
"store_result": false
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `command` | string | 是 | CLI 子命令(不含二进制路径和 `--auth-context`) |
|
||||
| `timeout` | number | 否 | 超时秒数,默认 120,大结果集建议 300+ |
|
||||
| `store_result` | boolean | 否 | 强制保存到当前对话目录并返回 `data_file.file_path`;分析脚本需要文件输入时设为 true |
|
||||
|
||||
认证上下文(token、server、project、network)由内部桥接自动注入,无需手动传参。
|
||||
|
||||
## 命令发现
|
||||
|
||||
Agent 通过 `help` 动态发现可用命令,而非依赖硬编码清单。
|
||||
|
||||
**重要:命令分为三类——触发动作、运行/结果查询与时序数据获取。**
|
||||
|
||||
- **触发动作**(`simulation`、各类 `analysis`):向服务端发起计算请求。
|
||||
- **运行/结果查询**(`analysis runs`):按 `run_id` 查询运行元数据和非时序结果。
|
||||
- **时序数据获取**(`data timeseries`):实时结果或按 `run_id` 查询节点、管道时序。
|
||||
|
||||
```
|
||||
analysis → 触发计算 → analysis runs list/get/results
|
||||
↓ run_id
|
||||
data timeseries analysis → 获取元素时序
|
||||
|
||||
simulation → 触发实时模拟 → data timeseries realtime → 获取实时结果
|
||||
```
|
||||
|
||||
通过 `help` 发现命令:
|
||||
|
||||
```
|
||||
tjwater-cli help → 一级命令清单(含 commands 数组和 summary)
|
||||
tjwater-cli help data timeseries → data timeseries 的子命令与参数详情
|
||||
tjwater-cli help simulation → simulation 的子命令与参数详情
|
||||
tjwater-cli help COMMAND → 子命令与参数详情
|
||||
```
|
||||
|
||||
`help` 返回 JSON 格式,Agent 可直接解析 `commands` 数组识别可用能力。
|
||||
|
||||
**严禁猜测命令或参数!** 所有命令路径、子命令和参数(名称、类型、必填/可选)均以 `help` 输出为准。执行任何命令前,必须先通过 `help` 确认其存在及参数签名,禁止凭经验拼写。
|
||||
|
||||
### 已知命令族
|
||||
|
||||
| 命令族 | 典型子命令 | 用途 |
|
||||
|------|-----------|------|
|
||||
| `network` | `get-pipe-properties`, `get-all-pipes-properties` | 管网元素查询 |
|
||||
| `component` | `option get`, `option schema` | 模型选项和结构查询 |
|
||||
| `data` | `timeseries realtime`, `timeseries analysis`, `timeseries scada`, `scada` | 实时、分析时序和 SCADA 查询 |
|
||||
| `simulation` | 通过 `help simulation` 发现 | **触发水力仿真计算**(执行成功返回状态,实际结果需走 `data timeseries` 获取) |
|
||||
| `analysis` | `runs`, `sensor-placement` 及各类分析命令 | 触发分析,并按运行 ID 查询元数据与结果 |
|
||||
| `help` | (无子命令) | 命令发现入口 |
|
||||
|
||||
> 完整命令清单始终以 `tjwater-cli help` 实时输出为准。
|
||||
|
||||
## 输出格式
|
||||
|
||||
所有命令返回统一 JSON 结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "tjwater-cli/v1",
|
||||
"ok": true,
|
||||
"data": { ... },
|
||||
"error": {
|
||||
"code": "COMMAND_NOT_FOUND",
|
||||
"message": "详细错误描述"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `ok: true` — 成功,数据在 `data` 字段
|
||||
- `ok: false` — 失败,检查 `error.code` 和 `error.message`
|
||||
|
||||
### 大结果集处理
|
||||
|
||||
超过内联阈值的结果不会终止 CLI,而是保存到当前对话的 `tool-data/` 目录并返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data_file": {
|
||||
"file_path": "/app/data/conversation-workspaces/conversation-.../tool-data/cli-....json",
|
||||
"bytes": 38700000,
|
||||
"content_type": "application/json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
禁止完整读取超大结果集。优先使用:
|
||||
- 采样/截断参数(如 `--limit`、`--offset`)
|
||||
- `--field` 按字段过滤
|
||||
- `store_result=true` 后用沙箱 Python 脚本按字段读取
|
||||
|
||||
## 错误码速查
|
||||
|
||||
| error.code | 含义 | 来源 | 处理建议 |
|
||||
|------|------|------|------|
|
||||
| `UNAUTHENTICATED` | 缺少 access token | CLI `core.py:162` | 检查认证上下文注入 |
|
||||
| `SERVER_ERROR` | 后端返回 error 状态 | CLI `core.py:400` | 记录 `request_id`,结合后端日志排查 |
|
||||
| `REQUEST_TIMEOUT` | CLI 请求后端超时 | CLI `core.py:445` | 增大 `timeout` 参数或检查后端负载 |
|
||||
| `TIMEOUT` | bridge 层进程超时 | Agent `server.ts:199` | 增大 `tjwater_cli` 的 `timeout` 参数 |
|
||||
| `COMMAND_NOT_FOUND` | 命令/子命令不存在 | CLI `helping.py:300` | 执行 `help` 确认命令拼写 |
|
||||
| `INPUT_NOT_FOUND` | `--input` 文件不存在 | CLI `core.py:243` | 检查文件路径 |
|
||||
| `REQUEST_FAILED` | 网络连接失败 | CLI `core.py:453` | 检查服务端可达性 |
|
||||
| `AUTH_CONTEXT_INVALID` | 认证上下文格式错误 | CLI `core.py:111` | 检查 auth headers 格式 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **禁止猜测命令** — 执行任何命令前必须先 `tjwater_cli(command="help ...")` 确认命令存在及参数签名,参数均已写在 help 中,禁止凭经验拼写
|
||||
2. **阶段分组** — 调用 CLI 前确认当前业务阶段已通过 `activity_update` 建立,同一阶段的多个查询无需重复说明理由
|
||||
3. **按运行 ID 取结果** — 分析完成后先用 `analysis runs list/get/results` 获取 `run_id` 和非时序结果;元素时序再用 `data timeseries analysis` 查询
|
||||
4. **文件分析** — workflow 脚本需要文件时使用 `store_result=true`,不得从 Bash 直接联网调用 CLI
|
||||
5. **结果验证** — 始终检查 `ok` 字段,失败时先处理错误码再重试
|
||||
6. **大结果集** — 优先过滤/采样,不要一次性拉取全部数据
|
||||
7. **模拟时长控制** — 实时模拟或分析运行的 `--duration` 不宜过长,建议每次仿真时间跨度控制在一小时以内,避免计算耗时过长或结果数据量过大
|
||||
|
||||
## 示例
|
||||
|
||||
### 查询所有实时节点数据
|
||||
```json
|
||||
{
|
||||
"command": "data timeseries realtime nodes --start-time 2026-06-03T08:00:00+08:00 --end-time 2026-06-03T09:00:00+08:00"
|
||||
}
|
||||
```
|
||||
> `data timeseries realtime nodes` 仅接受 `--start-time` / `--end-time`,返回全量节点数据。
|
||||
|
||||
### 按节点查询分析运行时序字段
|
||||
```json
|
||||
{
|
||||
"command": "data timeseries analysis node-field --run-id 00000000-0000-0000-0000-000000000001 --node J-001 --field pressure --start-time 2026-06-03T08:00:00+08:00 --end-time 2026-06-03T09:00:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 查询 SCADA 时序数据
|
||||
```json
|
||||
{
|
||||
"command": "data timeseries scada query --device-id 170490 --field monitored_value --start-time 2026-06-02T00:00:00+08:00 --end-time 2026-06-03T00:00:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 触发仿真并获取结果
|
||||
|
||||
通常系统会自动跑仿真,建议**先尝试获取结果**,若无数据再触发仿真:
|
||||
|
||||
```json
|
||||
// step 1: 先尝试获取仿真结果
|
||||
{
|
||||
"command": "data timeseries realtime simulation-by-id-time --id J-001 --type junction --time 2026-06-03T09:00:00+08:00"
|
||||
}
|
||||
// step 2: 若 step 1 无数据(ok: false 或 data 为空),触发仿真
|
||||
{
|
||||
"command": "simulation run --start-time 2026-06-03T08:00:00+08:00 --duration 60"
|
||||
}
|
||||
// step 3: 仿真完成后,再次获取结果(同 step 1)
|
||||
{
|
||||
"command": "data timeseries realtime simulation-by-id-time --id J-001 --type junction --time 2026-06-03T09:00:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
`simulation run` 仅接受 `--start-time`(RFC3339,必填)和 `--duration`(整数分钟,必填)。
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
name: hydraulic-bottleneck-analysis
|
||||
description: 基于实时水力数据的管网水力瓶颈识别与改造建议。复合评分法(流速×水头损失)定位瓶颈管段,输出分级改造方案。
|
||||
---
|
||||
|
||||
# 水力瓶颈分析工作流
|
||||
|
||||
## 概述
|
||||
|
||||
本工作流通过复合评分法(流速分级 × 水头损失百分位)从全管网管道中识别水力瓶颈管段,并结合节点压力、管径、粗糙系数给出分级改造建议。
|
||||
|
||||
适用场景:管网运行评估、管网改造优先级排序、泵站阀站运行诊断。
|
||||
|
||||
## 评分方法论
|
||||
|
||||
### 双维度复合评分
|
||||
|
||||
| 维度 | 判定标准 | 分值 |
|
||||
|------|----------|------|
|
||||
| **流速** | >3.0 m/s = 极危 | 3 |
|
||||
| | 2.0–3.0 m/s = 严重 | 2 |
|
||||
| | 1.5–2.0 m/s = 偏高 | 1 |
|
||||
| | <1.5 m/s = 正常 | 0 |
|
||||
| **水头损失** | >P90 = 严重 | 2 |
|
||||
| | P80–P90 = 中度 | 1 |
|
||||
| | <P80 = 正常 | 0 |
|
||||
|
||||
**瓶颈判定**:`(流速≥1 且 水损≥1)` 或 `(流速≥2)` —— 即双侧超标或单侧流速严重。
|
||||
|
||||
**复合评分** = 流速分值 + 水损分值(最高 5 分),按降序排列。
|
||||
|
||||
### 辅助指标
|
||||
|
||||
| 指标 | 阈值 | 含义 |
|
||||
|------|------|------|
|
||||
| 节点压力 < 20m | — | 低压区域,需增压 |
|
||||
| 节点压力 20–25m | — | 压力偏低 |
|
||||
| roughness > 130 | — | 管壁粗糙,建议内衬修复 |
|
||||
|
||||
> ⚠️ **setting 字段无效**:`data timeseries realtime links` 返回的 `setting` 字段为无效值,不可用于阀门节流或水泵出口判定。若需确定阀门/泵状态,应通过 `network get-link-properties` 逐条查询。
|
||||
|
||||
## 数据依赖
|
||||
|
||||
| 步骤 | 命令 | 数据量 | 超时 | 关键字段 |
|
||||
|------|------|--------|------|----------|
|
||||
| ① 管道属性 | `network get-all-pipes-properties` | ~11.7MB / 91K条 | 120s | id, node1, node2, length, diameter, roughness |
|
||||
| ② 管道水力 | `data timeseries realtime links --start-time T --end-time T+15min` | ~39MB / 182K条 | 300s | id, flow, velocity, headloss, time |
|
||||
| ③ 节点压力 | `data timeseries realtime nodes --start-time T --end-time T+15min` | ~28MB / 176K条 | 300s | id, pressure, time |
|
||||
|
||||
> **时间窗口说明**:模拟步长 15 分钟,查询窗口取 `T~T+15min` 可覆盖 1–2 个时间步。脚本内部按 `--target-time` 精确筛选目标时刻的记录。
|
||||
|
||||
> **大结果集处理**:三份调用都使用 `store_result=true`,结果保存到当前对话的 `tool-data/` 目录。脚本直接读取每次返回的 `data_file.file_path`,不得访问全局 `tool-output/`。
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### 第 1 步:拉取三份数据
|
||||
|
||||
并行发起 3 个 `tjwater_cli` 调用(互不依赖):
|
||||
|
||||
```bash
|
||||
# ① 管道静态属性
|
||||
tjwater_cli(command="network get-all-pipes-properties", timeout=120, store_result=true)
|
||||
|
||||
# ② 目标时刻管道水力数据
|
||||
tjwater_cli(command="data timeseries realtime links --start-time 2026-04-01T08:00:00+08:00 --end-time 2026-04-01T08:15:00+08:00", timeout=300, store_result=true)
|
||||
|
||||
# ③ 目标时刻节点压力数据
|
||||
tjwater_cli(command="data timeseries realtime nodes --start-time 2026-04-01T08:00:00+08:00 --end-time 2026-04-01T08:15:00+08:00", timeout=300, store_result=true)
|
||||
```
|
||||
|
||||
### 第 2 步:运行分析脚本
|
||||
|
||||
```bash
|
||||
python3 <skill_dir>/scripts/bottleneck_analysis.py \
|
||||
--pipe-props <data_file.file_path-①> \
|
||||
--realtime <data_file.file_path-②> \
|
||||
--node-pressures <data_file.file_path-③> \
|
||||
--target-time '2026-04-01T08:00:00+08:00' \
|
||||
--top 50 \
|
||||
2>./bottleneck_report.txt
|
||||
```
|
||||
|
||||
**脚本参数**:
|
||||
- `--pipe-props`:管道属性 JSON 文件路径(必填)
|
||||
- `--realtime`:实时管道水力 JSON 文件路径(必填)
|
||||
- `--node-pressures`:实时节点压力 JSON 文件路径(必填)
|
||||
- `--target-time`:目标时刻 ISO8601(必填,如 `2026-04-01T08:00:00+08:00`)
|
||||
- `--top`:输出 Top N 瓶颈管段(默认 100)
|
||||
|
||||
**输出**:
|
||||
- **stderr**:文本摘要 + Top 30 表格(可重定向到文件查看)
|
||||
- **stdout**:完整 JSON 结果,包含 `summary` 和 `top_bottlenecks`(含每条的建议 `suggestions`)
|
||||
|
||||
### 第 3 步:结果解读与分类
|
||||
|
||||
按瓶颈严重程度和根因分类:
|
||||
|
||||
| 类别 | 判定条件 | 优先处理方案 |
|
||||
|------|----------|--------------|
|
||||
| 🔴 极危 | 流速>3.0m/s 且 水损>P90 | 检查模型/扩容/分流 |
|
||||
| 🔵 串联瓶颈 | 连续多根瓶颈管段共享节点 | 统一规划扩径,一次性解除 |
|
||||
| 🟣 低压区 | 端节点压力<20m | 扩径 + 评估中途加压 |
|
||||
| 🟡 高粗糙度 | roughness>130 的瓶颈管段 | 内衬修复降阻 |
|
||||
| 🟢 短管异常 | length<10m 且 headloss>P95 | 检查模型是否存在局部阻塞 |
|
||||
|
||||
### 第 4 步:可视化(可选)
|
||||
|
||||
1. **地图定位**:将 Top N 瓶颈管段用 `locate_features` 高亮到地图
|
||||
2. **统计图表**:用 `show_chart` 展示管径分布柱状图 + 流速等级柱状图
|
||||
3. **样式渲染**:可对 pipes 图层按 velocity 或 headloss 属性做分层设色
|
||||
|
||||
## 改造建议生成逻辑
|
||||
|
||||
脚本自动为每条瓶颈管段生成建议,规则如下:
|
||||
|
||||
```
|
||||
if velocity > 2.0 → "流速X.XXm/s过高,需扩容或分流"
|
||||
elif velocity > 1.5 → "流速X.XXm/s偏高"
|
||||
|
||||
if headloss > P95 → "水头损失X.XXm(>P95)严重超标"
|
||||
elif headloss > P90 → "水头损失X.XXm(>P90)"
|
||||
|
||||
if diameter < 100 → "管径Xmm偏小,建议扩径至≥150mm"
|
||||
elif diameter < 200 → "管径Xmm,评估扩容至250-300mm"
|
||||
|
||||
if roughness > 130 → "粗糙系数X偏高,建议内衬修复"
|
||||
|
||||
if min_pressure < 20 → "端节点压力X.Xm(<20m),低压区域需增压"
|
||||
elif min_pressure < 25 → "端节点压力X.Xm偏低"
|
||||
|
||||
if length < 0.01 and headloss > 0.5 → "短管高水损,检查是否存在模型异常或局部阻塞"
|
||||
```
|
||||
|
||||
## 参考数据规模(实测)
|
||||
|
||||
基于 91,000 管段 / 88,000 节点规模的管网模型:
|
||||
|
||||
| 指标 | 实测值 |
|
||||
|------|--------|
|
||||
| 管道属性数据量 | 91,052 条 / ~11.7MB |
|
||||
| 实时管道数据量 | 182,108 条(2步)/ ~38.7MB |
|
||||
| 实时节点数据量 | 175,814 条(2步)/ ~28.2MB |
|
||||
| 分析脚本处理时间 | 约 10-20 秒 |
|
||||
| 典型瓶颈数量 | 50-100 条(占总管数 0.05%-0.1%) |
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 水头损失百分位阈值(P80/P90)基于**全管网**统计,如果管网上游存在极端水损(如 400m+),会拉高整体 P 值,导致部分中高水损管段被漏判。极端场景下可考虑对水损做分位数裁剪(如排除 >P99.9 的离群值)后再计算 P80/P90。
|
||||
- **setting 字段不可用**:`data timeseries realtime links` 返回的 `setting` 值为无效数据,本工作流已移除所有基于 setting 的阀门节流 / 水泵出口判定。若需要此类判定,应通过 `network get-link-properties` 逐条获取属性中的 setting 作为替代。
|
||||
- 脚本读取全量 JSON 入内存,峰值内存约 200-300MB,需确保执行环境有足够内存。
|
||||
|
||||
## Learned Patterns
|
||||
- [5cbdaa6bcf4e01c22eb2e544] [2026-08 复验] **schema 适配已固化进脚本**:bottleneck_analysis.py 现已直接使用 link_id/node_id 主键与 UTC target-time,无需再手工改码。本次 91,052 管段管网识别出 56 条瓶颈(0.06%),典型特征:100-110mm 小管径串联瓶颈(多条水损值近等差递减、逐段累计,如 7 段链 478128→460635→479635→508436→506699→484919→406224),宜按整链统一扩径;另有 1.4m 短管水损 67m 的模型异常信号(399832/399820),需核查局部阻塞或模型设置。分析后若用户需要改造建议落地,可按"极危...
|
||||
- [5ba58cd24c9cea84b6ab5861] **数据 schema 实测适配(2026-04 验证)**:`data timeseries realtime links` 返回记录的管道主键为 `link_id`(不是 `id`),`data timeseries realtime nodes` 返回记录的节点主键为 `node_id`(不是 `id`),且 `time` 字段为 UTC 格式(如 `2026-04-01T00:00:00+00:00`)。运行 `bottleneck_analysis.py` 前需:① 脚本内将 `r['id']` 改为 `r['link_id']`、`n['id']` 改为 `n['node_id']`;② `--target-tim...
|
||||
@@ -0,0 +1,80 @@
|
||||
# 数据源与字段映射
|
||||
|
||||
## CLI 命令清单
|
||||
|
||||
### ① 管道静态属性
|
||||
|
||||
```bash
|
||||
tjwater-cli network get-all-pipes-properties
|
||||
```
|
||||
|
||||
返回字段:
|
||||
|
||||
| 字段 | 类型 | 说明 | 分析用途 |
|
||||
|------|------|------|----------|
|
||||
| id | string | 管段 ID | 主键,关联水力数据 |
|
||||
| node1 | string | 起始节点 ID | 拓扑,定位压力 |
|
||||
| node2 | string | 终止节点 ID | 拓扑,定位压力 |
|
||||
| length | float | 管长 (m) | 短管高水损检测 |
|
||||
| diameter | int | 管径 (mm) | 管径分级,扩容建议 |
|
||||
| roughness | int | 粗糙系数 | 内衬修复判定 |
|
||||
| minor_loss | float | 局部水头损失系数 | 暂未使用 |
|
||||
| status | string | OPEN/CLOSED | 管道状态 |
|
||||
|
||||
### ② 管道实时水力
|
||||
|
||||
```bash
|
||||
tjwater-cli data timeseries realtime links --start-time <T> --end-time <T+15min>
|
||||
```
|
||||
|
||||
返回字段:
|
||||
|
||||
| 字段 | 类型 | 说明 | 分析用途 |
|
||||
|------|------|------|----------|
|
||||
| time | string | 时间戳 ISO8601 | 筛选目标时刻 |
|
||||
| id | string | 管段 ID | 关联静态属性 |
|
||||
| flow | float | 流量 | 辅助参考 |
|
||||
| velocity | float | 流速 (m/s) | **核心评分指标** |
|
||||
| headloss | float | 水头损失 (m) | **核心评分指标** |
|
||||
| setting | float | ⚠️ 无效值 | 时序 API 返回的 setting 为无效值,不可用于判定 |
|
||||
| friction | float | 摩擦系数 | 暂未使用 |
|
||||
| quality | float | 水质 | 暂未使用 |
|
||||
| reaction | float | 反应速率 | 暂未使用 |
|
||||
| status | string | OPEN/CLOSED | 管道状态 |
|
||||
|
||||
### ③ 节点实时压力
|
||||
|
||||
```bash
|
||||
tjwater-cli data timeseries realtime nodes --start-time <T> --end-time <T+15min>
|
||||
```
|
||||
|
||||
返回字段:
|
||||
|
||||
| 字段 | 类型 | 说明 | 分析用途 |
|
||||
|------|------|------|----------|
|
||||
| time | string | 时间戳 ISO8601 | 筛选目标时刻 |
|
||||
| id | string | 节点 ID | 关联管段端点 |
|
||||
| pressure | float | 压力 (m) | **低压判定** |
|
||||
| total_head | float | 总水头 (m) | 含高程信息 |
|
||||
| actual_demand | float | 实际需水量 | 暂未使用 |
|
||||
| quality | float | 水质 | 暂未使用 |
|
||||
|
||||
## 数据合并逻辑
|
||||
|
||||
```
|
||||
管道属性 (pipe_map[id]) ←─id─→ 实时水力 (rt[time==TT])
|
||||
│
|
||||
node1, node2
|
||||
│
|
||||
↓
|
||||
节点压力 (node_pressure[id])
|
||||
```
|
||||
|
||||
合并时以**实时水力数据为主表**,左联管道属性,再通过 node1/node2 查找两端压力。
|
||||
|
||||
## 时间处理
|
||||
|
||||
- 模拟步长:15 分钟
|
||||
- 查询窗口建议:T 到 T+15min(覆盖 1-2 步)
|
||||
- 脚本内精确筛选:`r.get('time') == TT` 严格匹配字符串
|
||||
- 若目标时刻(如 08:00)无数据,需先触发 `simulation run --start-time T --duration 15`
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
水力瓶颈管道综合分析
|
||||
数据源:管道属性 + 实时水力 + 节点压力 → 复合评分 → 改造建议
|
||||
注:realtime links 的 setting 字段为无效值,已移除所有基于 setting 的判定。
|
||||
schema 适配(2026-08 实测):realtime links 主键为 link_id,realtime nodes 主键为 node_id,
|
||||
time 为 UTC 格式,--target-time 需传 UTC 时刻(如北京时间 08:00 对应 00:00+00:00)。
|
||||
"""
|
||||
import json, sys, math, argparse
|
||||
from collections import defaultdict
|
||||
|
||||
VELOCITY_THRESHOLDS = {"critical": 3.0, "severe": 2.0, "high": 1.5}
|
||||
|
||||
def pct(d, v):
|
||||
if not d: return 0
|
||||
k = (v/100)*(len(d)-1); f=math.floor(k); c=math.ceil(k)
|
||||
return d[f] if f==c else d[f]*(c-k)+d[c]*(k-f)
|
||||
|
||||
def load_json(path):
|
||||
with open(path, encoding='utf-8') as f: raw = f.read()
|
||||
return json.loads(raw[raw.find('{'):])
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--pipe-props', required=True)
|
||||
ap.add_argument('--realtime', required=True)
|
||||
ap.add_argument('--node-pressures', required=True)
|
||||
ap.add_argument('--target-time', required=True)
|
||||
ap.add_argument('--top', type=int, default=100)
|
||||
args = ap.parse_args()
|
||||
TT = args.target_time
|
||||
|
||||
# 1. Load
|
||||
print("[1/5] Loading data...", file=sys.stderr)
|
||||
props = load_json(args.pipe_props).get('data', [])
|
||||
pipe_map = {p['id']: p for p in props}
|
||||
|
||||
rt = load_json(args.realtime).get('data', [])
|
||||
rt = [r for r in rt if r.get('time') == TT]
|
||||
|
||||
np = load_json(args.node_pressures).get('data', [])
|
||||
np = [n for n in np if n.get('time') == TT]
|
||||
node_pressure = {n['node_id']: n.get('pressure', n.get('value',0)) for n in np}
|
||||
print(f" Pipes: {len(props)}, Realtime: {len(rt)}, Node pressures: {len(node_pressure)}", file=sys.stderr)
|
||||
|
||||
# 2. Merge
|
||||
print("[2/5] Merging...", file=sys.stderr)
|
||||
merged = []
|
||||
for r in rt:
|
||||
pid = r['link_id']; prop = pipe_map.get(pid)
|
||||
if prop:
|
||||
merged.append({**prop, **r, '_prop_id': prop['id'], '_rt_id': r['link_id']})
|
||||
print(f" Merged: {len(merged)}", file=sys.stderr)
|
||||
|
||||
# 3. Score
|
||||
print("[3/5] Scoring...", file=sys.stderr)
|
||||
hl_vals = sorted([abs(m['headloss']) for m in merged])
|
||||
p80 = pct(hl_vals, 80); p90 = pct(hl_vals, 90); p95 = pct(hl_vals, 95)
|
||||
|
||||
scored = []
|
||||
for m in merged:
|
||||
vel = abs(m['velocity']); hl = abs(m['headloss'])
|
||||
diam = m.get('diameter', 0)
|
||||
length = m.get('length', 0); roughness = m.get('roughness', 0)
|
||||
n1, n2 = m['node1'], m['node2']
|
||||
pid = m['id']
|
||||
|
||||
vs = 3 if vel>=3 else (2 if vel>=2 else (1 if vel>=1.5 else 0))
|
||||
vg = "极危" if vs==3 else ("严重" if vs==2 else ("偏高" if vs==1 else "正常"))
|
||||
hs = 2 if hl>p90 else (1 if hl>p80 else 0)
|
||||
hg = "严重" if hs==2 else ("中度" if hs==1 else "正常")
|
||||
composite = vs + hs
|
||||
is_bn = (vs>=1 and hs>=1) or (vs>=2)
|
||||
|
||||
# Node pressures
|
||||
p1 = node_pressure.get(n1); p2 = node_pressure.get(n2)
|
||||
min_p = min(p1, p2) if (p1 is not None and p2 is not None) else None
|
||||
|
||||
# Pipe category
|
||||
if diam <= 50: dcat = "微型(≤50mm)"
|
||||
elif diam <= 100: dcat = "小型(51-100mm)"
|
||||
elif diam <= 200: dcat = "中型(101-200mm)"
|
||||
elif diam <= 400: dcat = "大型(201-400mm)"
|
||||
elif diam <= 800: dcat = "主干(401-800mm)"
|
||||
else: dcat = "干管(>800mm)"
|
||||
|
||||
scored.append({
|
||||
'id': pid, 'node1': n1, 'node2': n2,
|
||||
'velocity': round(vel, 4), 'flow': round(m.get('flow',0), 4),
|
||||
'headloss': round(hl, 4), 'length': round(length, 4),
|
||||
'diameter': int(diam), 'roughness': int(roughness),
|
||||
'vel_grade': vg, 'hl_grade': hg, 'composite_score': composite,
|
||||
'is_bottleneck': is_bn,
|
||||
'n1_pressure': round(p1, 2) if p1 is not None else None,
|
||||
'n2_pressure': round(p2, 2) if p2 is not None else None,
|
||||
'min_pressure': round(min_p, 2) if min_p is not None else None,
|
||||
'diam_cat': dcat,
|
||||
})
|
||||
|
||||
# 4. Filter & sort
|
||||
print("[4/5] Filtering bottlenecks...", file=sys.stderr)
|
||||
bn = [s for s in scored if s['is_bottleneck']]
|
||||
bn.sort(key=lambda x: (-x['composite_score'], -x['velocity']))
|
||||
|
||||
critical = [b for b in bn if b['vel_grade']=='极危']
|
||||
severe = [b for b in bn if b['vel_grade']=='严重']
|
||||
high_vel = [b for b in bn if b['vel_grade']=='偏高']
|
||||
|
||||
low_p = sum(1 for b in bn if b['min_pressure'] and b['min_pressure'] < 20)
|
||||
lowish_p = sum(1 for b in bn if b['min_pressure'] and 20 <= b['min_pressure'] < 25)
|
||||
high_rough = sum(1 for b in bn if b['roughness'] > 130)
|
||||
|
||||
# Diam distribution
|
||||
dd = defaultdict(int)
|
||||
for b in bn:
|
||||
d = b['diameter']
|
||||
if d <= 50: dd['≤50mm']+=1
|
||||
elif d <= 100: dd['51-100mm']+=1
|
||||
elif d <= 200: dd['101-200mm']+=1
|
||||
elif d <= 400: dd['201-400mm']+=1
|
||||
elif d <= 800: dd['401-800mm']+=1
|
||||
else: dd['>800mm']+=1
|
||||
|
||||
# 5. Output
|
||||
print("[5/5] Generating report...", file=sys.stderr)
|
||||
|
||||
# Print text summary to stderr
|
||||
print(f"\n{'='*70}", file=sys.stderr)
|
||||
print(f" 水力瓶颈分析报告 - {TT}", file=sys.stderr)
|
||||
print(f"{'='*70}", file=sys.stderr)
|
||||
print(f" 总管道数: {len(scored)}", file=sys.stderr)
|
||||
print(f" 瓶颈管道: {len(bn)} ({len(bn)/len(scored)*100:.1f}%)", file=sys.stderr)
|
||||
print(f" 极危(>3.0m/s): {len(critical)} 条", file=sys.stderr)
|
||||
print(f" 严重(2.0-3.0): {len(severe)} 条", file=sys.stderr)
|
||||
print(f" 偏高(1.5-2.0): {len(high_vel)} 条", file=sys.stderr)
|
||||
print(f"\n 水头损失阈值: P80={p80:.4f}m P90={p90:.4f}m P95={p95:.4f}m", file=sys.stderr)
|
||||
print(f" 平均={sum(hl_vals)/len(hl_vals):.4f}m 最大={max(hl_vals):.4f}m", file=sys.stderr)
|
||||
print(f"\n 低压节点(<20m): {low_p} 条, 偏低(20-25m): {lowish_p} 条", file=sys.stderr)
|
||||
print(f" 高粗糙度(>130): {high_rough} 条", file=sys.stderr)
|
||||
|
||||
print(f"\n 瓶颈管径分布:", file=sys.stderr)
|
||||
for cat in ['≤50mm','51-100mm','101-200mm','201-400mm','401-800mm','>800mm']:
|
||||
print(f" {cat}: {dd.get(cat,0)} 条", file=sys.stderr)
|
||||
|
||||
# Text table (Top 30) to stderr
|
||||
print(f"\n{'='*120}", file=sys.stderr)
|
||||
print(f"{'排名':<5} {'管道ID':<10} {'流速(m/s)':<10} {'水损(m)':<10} {'管径(mm)':<9} {'管长(km)':<10} {'评分':<4} {'压力1':<8} {'压力2':<8} {'管径类别':<18}", file=sys.stderr)
|
||||
print('-'*120, file=sys.stderr)
|
||||
for i, b in enumerate(bn[:30]):
|
||||
p1s = f"{b['n1_pressure']:.1f}" if b['n1_pressure'] is not None else "-"
|
||||
p2s = f"{b['n2_pressure']:.1f}" if b['n2_pressure'] is not None else "-"
|
||||
print(f"{i+1:<5} {b['id']:<10} {b['velocity']:<10.4f} {b['headloss']:<10.2f} {b['diameter']:<9} {b['length']:<10.4f} {b['composite_score']:<4} {p1s:<8} {p2s:<8} {b['diam_cat']:<18}", file=sys.stderr)
|
||||
|
||||
# Generate suggestions for each bottleneck
|
||||
for b in bn:
|
||||
sug = []
|
||||
if b['velocity'] > 2.0:
|
||||
sug.append(f"流速{b['velocity']:.2f}m/s过高,需扩容或分流")
|
||||
elif b['velocity'] > 1.5:
|
||||
sug.append(f"流速{b['velocity']:.2f}m/s偏高")
|
||||
hl = b['headloss']
|
||||
if hl > p95:
|
||||
sug.append(f"水头损失{hl:.2f}m(>P95)严重超标")
|
||||
elif hl > p90:
|
||||
sug.append(f"水头损失{hl:.2f}m(>P90)")
|
||||
if b['diameter'] < 100:
|
||||
sug.append(f"管径{b['diameter']}mm偏小,建议扩径至≥150mm")
|
||||
elif b['diameter'] < 200:
|
||||
sug.append(f"管径{b['diameter']}mm,评估扩容至250-300mm")
|
||||
if b['roughness'] > 130:
|
||||
sug.append(f"粗糙系数{b['roughness']}偏高,建议内衬修复")
|
||||
if b['min_pressure'] is not None and b['min_pressure'] < 20:
|
||||
sug.append(f"端节点压力{b['min_pressure']:.1f}m(<20m),低压区域需增压")
|
||||
elif b['min_pressure'] is not None and b['min_pressure'] < 25:
|
||||
sug.append(f"端节点压力{b['min_pressure']:.1f}m偏低")
|
||||
if b['length'] < 0.01 and b['headloss'] > 0.5:
|
||||
sug.append("短管高水损,检查是否存在模型异常或局部阻塞")
|
||||
b['suggestions'] = sug
|
||||
|
||||
result = {
|
||||
'target_time': TT,
|
||||
'summary': {
|
||||
'total_pipes': len(scored),
|
||||
'bottleneck_count': len(bn),
|
||||
'critical': len(critical), 'severe': len(severe), 'high_vel': len(high_vel),
|
||||
'low_pressure_nodes': low_p, 'lowish_pressure_nodes': lowish_p,
|
||||
'high_roughness_pipes': high_rough,
|
||||
'headloss_p80': round(p80,4), 'headloss_p90': round(p90,4),
|
||||
'headloss_p95': round(p95,4),
|
||||
'diameter_distribution': dict(dd),
|
||||
},
|
||||
'top_bottlenecks': bn[:args.top],
|
||||
}
|
||||
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
name: service-area-analysis
|
||||
description: 基于实时水力模拟数据的水源追溯供水服务范围分区。通过管段流量确定水流方向,从水库BFS追溯服务节点,环网/零流量节点用无向拓扑补充分配,输出分区可视化。
|
||||
---
|
||||
|
||||
# 供水服务范围分区工作流
|
||||
|
||||
## 概述
|
||||
|
||||
本工作流基于指定时刻的水力模拟结果,通过**流向追溯法**将全部管网节点分配到各水库的服务范围。核心思路:利用管段流量符号判定水流方向,构建有向图从水库逐级追溯,对环网和零流量节点用无向拓扑修正。
|
||||
|
||||
适用场景:供水服务范围评估、DMA分区规划、多水源供水格局分析、管网调度策略评估。
|
||||
|
||||
## 分区方法
|
||||
|
||||
### 第一步:水流方向判定
|
||||
|
||||
对于每条管段,根据实时流量 `flow` 判定水流方向:
|
||||
|
||||
| flow 值 | 水流方向 | 说明 |
|
||||
|---------|----------|------|
|
||||
| `flow > 1e-6` | node1 → node2 | 正向流量 |
|
||||
| `flow < -1e-6` | node2 → node1 | 反向流量 |
|
||||
| `|flow| ≤ 1e-6` | 无方向 | 零流量,不参与有向追溯 |
|
||||
|
||||
### 第二步:多源有向BFS
|
||||
|
||||
1. 以每个水库为根节点,沿水流方向执行 BFS
|
||||
2. 遍历到的节点归属该水库的服务范围
|
||||
3. **先到先得**:一个节点首次被访问到的水库即为归属
|
||||
4. 预期覆盖 **85–90%** 节点
|
||||
|
||||
### 第三步:无向拓扑修正
|
||||
|
||||
有向BFS不可达节点(通常 10–15%)通过无向图邻近性补充分配:
|
||||
|
||||
| 不可达原因 | 说明 |
|
||||
|------------|------|
|
||||
| 环状管网 | 水流回路中下游节点反向连回上游,有向遍历被阻断 |
|
||||
| 零流量管段 | `flow≈0` 的管段无方向,其下游节点断开 |
|
||||
| 多水源交汇 | 交汇区流向往复,非树状拓扑 |
|
||||
|
||||
### 输出统计
|
||||
|
||||
每个分区输出:
|
||||
- `node_count`:分区内节点总数
|
||||
- `total_demand`:总需水量(负数=净供水区)
|
||||
- `avg_pressure`/`min_pressure`/`max_pressure`:压力统计
|
||||
|
||||
## 数据依赖
|
||||
|
||||
| 步骤 | 命令 | 数据量 | 超时 | 关键字段 |
|
||||
|------|------|--------|------|----------|
|
||||
| ① 管道拓扑 | `network get-all-pipes-properties` | ~11.7MB / 91K条 | 120s | id, node1, node2 |
|
||||
| ② 水库属性 | `network get-all-reservoirs-properties` | ~小 | 120s | id, links |
|
||||
| ③ 管段流量 | `data timeseries realtime links --start-time T --end-time T+15min` | ~39MB / 182K条 | 300s | id, flow, time |
|
||||
| ④ 节点数据 | `data timeseries realtime nodes --start-time T --end-time T+15min` | ~28MB / 176K条 | 300s | id, pressure, actual_demand, time |
|
||||
|
||||
> **时间窗口**:模拟步长 15 分钟,查询 T~T+15min 覆盖 1–2 个时间步。脚本按 `--target-time` 精确筛选。
|
||||
|
||||
> **文件输入**:四份调用都使用 `store_result=true`,包括结果较小的水库属性。脚本读取每次返回的 `data_file.file_path`;文件都属于当前对话,禁止使用 `/tmp` 或全局 `tool-output/`。
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### 第 1 步:并行拉取数据
|
||||
|
||||
4 个 `tjwater_cli` 调用(互不依赖),可一次发起:
|
||||
|
||||
```bash
|
||||
# ① 管道静态拓扑
|
||||
tjwater_cli(command="network get-all-pipes-properties", timeout=120, store_result=true)
|
||||
|
||||
# ② 水库属性
|
||||
tjwater_cli(command="network get-all-reservoirs-properties", timeout=120, store_result=true)
|
||||
|
||||
# ③ 目标时刻管段流量
|
||||
tjwater_cli(command="data timeseries realtime links --start-time 2026-04-01T08:00:00+08:00 --end-time 2026-04-01T08:15:00+08:00", timeout=300, store_result=true)
|
||||
|
||||
# ④ 目标时刻节点数据
|
||||
tjwater_cli(command="data timeseries realtime nodes --start-time 2026-04-01T08:00:00+08:00 --end-time 2026-04-01T08:15:00+08:00", timeout=300, store_result=true)
|
||||
```
|
||||
|
||||
### 第 2 步:运行分区脚本
|
||||
|
||||
```bash
|
||||
python3 <skill_dir>/scripts/service_area_partition.py \
|
||||
--pipe-props <data_file.file_path-①> \
|
||||
--reservoirs <data_file.file_path-②> \
|
||||
--links <data_file.file_path-③> \
|
||||
--nodes <data_file.file_path-④> \
|
||||
--target-time '2026-04-01T08:00:00+08:00' \
|
||||
--output ./service_area_partition_wrapper.json
|
||||
```
|
||||
|
||||
**脚本参数**:
|
||||
|
||||
| 参数 | 说明 | 必填 |
|
||||
|------|------|------|
|
||||
| `--pipe-props` | 管道属性 JSON 文件路径 | 是 |
|
||||
| `--reservoirs` | 水库属性 JSON 文件路径 | 是 |
|
||||
| `--links` | 实时管段数据 JSON 文件路径 | 是 |
|
||||
| `--nodes` | 实时节点数据 JSON 文件路径 | 是 |
|
||||
| `--target-time` | 目标时刻 ISO8601 | 是 |
|
||||
| `--output` | 分区结果输出路径 | 是 |
|
||||
|
||||
**输出**:
|
||||
- **stderr**:处理日志 + 各分区统计表格
|
||||
- **stdout**:紧凑 JSON 摘要(total_nodes, reservoirs, areas)
|
||||
- **文件**:符合 `store_render_ref` 要求的 `{metadata, location, data}` 包装 JSON,其中 `data` 包含 `node_area_map`、`area_ids`、`area_colors` 和分析元数据
|
||||
|
||||
### 第 3 步:前端可视化
|
||||
|
||||
```bash
|
||||
# 持久化分区结果
|
||||
store_render_ref(file_path=<output-file>)
|
||||
|
||||
# 渲染节点分区
|
||||
render_junctions(render_ref="res-xxxxxxxx-xxxx-xx")
|
||||
|
||||
# 定位水库
|
||||
locate_features(ids=[...], feature_type="reservoir")
|
||||
|
||||
# 展示统计图表
|
||||
show_chart(title="各水源分区节点数/压力对比", chart_type="bar", ...)
|
||||
```
|
||||
|
||||
## 参考数据规模
|
||||
|
||||
基于 91,000 管段 / 88,000 节点规模的管网模型:
|
||||
|
||||
| 指标 | 实测值 |
|
||||
|------|--------|
|
||||
| 管道拓扑数据量 | 91,052 条 |
|
||||
| 水库数量 | 13 个 |
|
||||
| 总节点数 | 87,907 |
|
||||
| 有向BFS分配节点 | ~76,900 (87.5%) |
|
||||
| 无向修正节点 | ~11,000 (12.5%) |
|
||||
| 分区覆盖率 | 100% |
|
||||
| 脚本处理时间 | ~15-30 秒 |
|
||||
| 峰值内存 | ~400-500MB |
|
||||
|
||||
## 已知限制
|
||||
|
||||
- **水库顺序敏感**:多源 BFS 中先遍历到的水库优先分配,不同水库启动顺序可能影响边界区域分配结果
|
||||
- **单时刻快照**:分区仅反映目标时刻的水力工况,不同时段的泵站启停、阀门切换可能导致分区边界变化
|
||||
- **零流量阈值**:`1e-6` 阈值过滤极低流量管段,若管网有长期小流量管段可能漏判方向
|
||||
|
||||
## Learned Patterns
|
||||
- [6614a914a8f7dcc2fc34c1ba] 实时数据时间匹配必须用 norm_time 归一化为 UTC 再比较(数据 time 为 UTC 格式如 2026-06-03T00:00:00+00:00,target-time 传 +08:00 会因字符串不等而筛出 0 条);links/nodes 记录主键字段为 link_id/node_id(不是 id)。执行分区前先确认目标时刻存在实时数据,可用 `data timeseries realtime simulation-by-id-time` 探测;数据可能只覆盖某几天(本模型覆盖 2026-06-03 附近,2026-04 与 2026-08 均无数据)。
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
供水服务范围分析与分区 — 可复用脚本
|
||||
基于实时水力数据,从水库沿水流方向追溯服务范围,自动发现水库并分区。
|
||||
|
||||
用法:
|
||||
python3 service_area_partition.py \
|
||||
--pipe-props pipes.json \
|
||||
--reservoirs reservoirs.json \
|
||||
--links realtime_links.json \
|
||||
--nodes realtime_nodes.json \
|
||||
--target-time '2026-04-01T08:00:00+08:00' \
|
||||
--output ./service_area_partition_wrapper.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import deque, defaultdict
|
||||
|
||||
COLORS = [
|
||||
"rgba(31,119,180,0.7)", "rgba(255,127,14,0.7)", "rgba(44,160,44,0.7)",
|
||||
"rgba(148,103,189,0.7)", "rgba(140,86,75,0.7)", "rgba(227,119,194,0.7)",
|
||||
"rgba(127,127,127,0.7)", "rgba(188,189,34,0.7)", "rgba(23,190,207,0.7)",
|
||||
"rgba(174,199,232,0.7)", "rgba(255,152,150,0.7)", "rgba(196,156,148,0.7)",
|
||||
"rgba(219,64,82,0.7)", "rgba(153,204,153,0.7)", "rgba(255,204,102,0.7)",
|
||||
"rgba(102,102,204,0.7)", "rgba(204,102,102,0.7)", "rgba(102,204,204,0.7)",
|
||||
"rgba(204,153,204,0.7)", "rgba(153,153,153,0.7)"
|
||||
]
|
||||
|
||||
|
||||
def norm_time(t):
|
||||
"""把 ISO8601 字符串归一化为 UTC 的 ISO 字符串,用于跨时区比较"""
|
||||
from datetime import datetime
|
||||
return datetime.fromisoformat(t.replace("Z", "+00:00")).astimezone(
|
||||
__import__("datetime").timezone.utc).isoformat()
|
||||
|
||||
def load_json(path, label):
|
||||
print(f"Loading {label}...", file=sys.stderr)
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="供水服务范围分区分析")
|
||||
parser.add_argument("--pipe-props", required=True, help="管道属性 JSON 文件")
|
||||
parser.add_argument("--reservoirs", required=True, help="水库属性 JSON 文件")
|
||||
parser.add_argument("--links", required=True, help="实时管段数据 JSON 文件")
|
||||
parser.add_argument("--nodes", required=True, help="实时节点数据 JSON 文件")
|
||||
parser.add_argument("--target-time", required=True, help="目标时刻 ISO8601")
|
||||
parser.add_argument("--output", required=True, help="分区结果输出 JSON 路径")
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Step 1: Load pipe topology ---
|
||||
pdata = load_json(args.pipe_props, "pipe topology")["data"]
|
||||
pipe_topology = {}
|
||||
node_neighbors = defaultdict(set)
|
||||
for p in pdata:
|
||||
pid = p["id"]
|
||||
n1, n2 = p["node1"], p["node2"]
|
||||
pipe_topology[pid] = (n1, n2)
|
||||
node_neighbors[n1].add(n2)
|
||||
node_neighbors[n2].add(n1)
|
||||
print(f" {len(pdata)} pipes, {len(node_neighbors)} unique nodes", file=sys.stderr)
|
||||
|
||||
# --- Step 2: Discover reservoirs ---
|
||||
rdata = load_json(args.reservoirs, "reservoirs")["data"]
|
||||
reservoirs = [r["id"] for r in rdata]
|
||||
print(f" {len(reservoirs)} reservoirs: {reservoirs}", file=sys.stderr)
|
||||
|
||||
# --- Step 3: Load link flow at target time ---
|
||||
ldata = load_json(args.links, "link flows")["data"]
|
||||
target_ts = norm_time(args.target_time)
|
||||
target_links = [l for l in ldata if norm_time(l["time"]) == target_ts]
|
||||
flow_direction = {}
|
||||
pipe_flow = {}
|
||||
for l in target_links:
|
||||
lid = l.get("link_id") or l.get("id")
|
||||
flow_val = l["flow"]
|
||||
pipe_flow[lid] = abs(flow_val)
|
||||
if lid in pipe_topology:
|
||||
n1, n2 = pipe_topology[lid]
|
||||
if flow_val > 1e-6:
|
||||
flow_direction[lid] = (n1, n2)
|
||||
elif flow_val < -1e-6:
|
||||
flow_direction[lid] = (n2, n1)
|
||||
nonzero = len(flow_direction)
|
||||
print(f" {len(target_links)} link records, {nonzero} with non-zero flow", file=sys.stderr)
|
||||
|
||||
# --- Step 4: Load node data at target time ---
|
||||
ndata = load_json(args.nodes, "node data")["data"]
|
||||
target_nodes = [n for n in ndata if norm_time(n["time"]) == target_ts]
|
||||
node_pressure = {}
|
||||
node_demand = {}
|
||||
for n in target_nodes:
|
||||
nid = n.get("node_id") or n.get("id")
|
||||
node_pressure[nid] = n.get("pressure", 0)
|
||||
node_demand[nid] = n.get("actual_demand", 0)
|
||||
print(f" {len(target_nodes)} nodes", file=sys.stderr)
|
||||
|
||||
# --- Step 5: Build downstream graph ---
|
||||
downstream = defaultdict(set)
|
||||
for _lid, (up, dn) in flow_direction.items():
|
||||
downstream[up].add(dn)
|
||||
print(f" downstream graph: {len(downstream)} source nodes", file=sys.stderr)
|
||||
|
||||
# --- Step 6: Multi-source BFS along flow direction ---
|
||||
reservoir_area = {}
|
||||
node_served_by = {}
|
||||
queue = deque()
|
||||
for rid in reservoirs:
|
||||
reservoir_area[rid] = {rid}
|
||||
node_served_by[rid] = rid
|
||||
queue.append((rid, rid, 0))
|
||||
|
||||
while queue:
|
||||
node, source, dist = queue.popleft()
|
||||
for neighbor in downstream.get(node, set()):
|
||||
if neighbor not in node_served_by:
|
||||
node_served_by[neighbor] = source
|
||||
reservoir_area[source].add(neighbor)
|
||||
queue.append((neighbor, source, dist + 1))
|
||||
|
||||
directed_count = len(node_served_by)
|
||||
unassigned = set(node_pressure.keys()) - set(node_served_by.keys())
|
||||
print(f" flow-tracing assigned: {directed_count}, unassigned: {len(unassigned)}", file=sys.stderr)
|
||||
|
||||
# --- Step 7: Undirected proximity fallback ---
|
||||
if unassigned:
|
||||
print(" running proximity fallback...", file=sys.stderr)
|
||||
ua_queue = deque()
|
||||
ua_visited = {}
|
||||
for nid, src in node_served_by.items():
|
||||
ua_visited[nid] = src
|
||||
ua_queue.append((nid, src, 0))
|
||||
|
||||
while ua_queue:
|
||||
node, source, dist = ua_queue.popleft()
|
||||
for neighbor in node_neighbors.get(node, set()):
|
||||
if neighbor not in ua_visited:
|
||||
ua_visited[neighbor] = source
|
||||
reservoir_area[source].add(neighbor)
|
||||
ua_queue.append((neighbor, source, dist + 1))
|
||||
|
||||
still = set(node_pressure.keys()) - set(ua_visited.keys())
|
||||
if still:
|
||||
print(f" WARNING: {len(still)} nodes still unassigned", file=sys.stderr)
|
||||
node_served_by = ua_visited
|
||||
|
||||
# --- Step 8: Compute statistics ---
|
||||
print(f"\n=== 供水服务范围分区统计 ===\n", file=sys.stderr)
|
||||
area_stats = []
|
||||
for rid in reservoirs:
|
||||
nodes_in = reservoir_area.get(rid, set())
|
||||
pressures = [node_pressure[n] for n in nodes_in if n in node_pressure]
|
||||
demands = [node_demand[n] for n in nodes_in if n in node_demand]
|
||||
area_stats.append({
|
||||
"reservoir": rid,
|
||||
"node_count": len(nodes_in),
|
||||
"total_demand": round(sum(demands), 4),
|
||||
"avg_pressure": round(sum(pressures)/len(pressures), 2) if pressures else 0,
|
||||
"min_pressure": round(min(pressures), 2) if pressures else 0,
|
||||
"max_pressure": round(max(pressures), 2) if pressures else 0,
|
||||
})
|
||||
|
||||
area_stats.sort(key=lambda x: x["node_count"], reverse=True)
|
||||
for s in area_stats:
|
||||
print(f" 水源 {s['reservoir']:>8s}: {s['node_count']:>6d} 节点 | "
|
||||
f"总需水={s['total_demand']:.2f} | "
|
||||
f"压力 avg={s['avg_pressure']:.1f}m [{s['min_pressure']:.1f}–{s['max_pressure']:.1f}m]",
|
||||
file=sys.stderr)
|
||||
|
||||
# --- Step 9: Assign colors and write output ---
|
||||
area_colors = {}
|
||||
for i, rid in enumerate(reservoirs):
|
||||
area_colors[rid] = COLORS[i % len(COLORS)]
|
||||
|
||||
output = {
|
||||
"node_area_map": node_served_by,
|
||||
"area_ids": reservoirs,
|
||||
"area_colors": area_colors,
|
||||
"metadata": {
|
||||
"analysis_time": args.target_time,
|
||||
"total_nodes": len(node_served_by),
|
||||
"reservoir_count": len(reservoirs),
|
||||
"directed_assigned": directed_count,
|
||||
"proximity_assigned": len(node_served_by) - directed_count,
|
||||
"method": "flow-direction-source-tracing"
|
||||
}
|
||||
}
|
||||
|
||||
absolute_output = os.path.abspath(args.output)
|
||||
wrapper = {
|
||||
"metadata": {
|
||||
"generated_by": "service_area_partition.py",
|
||||
"schema_version": 1,
|
||||
},
|
||||
"location": {"file_path": absolute_output},
|
||||
"data": output,
|
||||
}
|
||||
with open(absolute_output, "w", encoding="utf-8") as f:
|
||||
json.dump(wrapper, f, ensure_ascii=False)
|
||||
|
||||
summary = {
|
||||
"total_nodes": len(node_served_by),
|
||||
"reservoirs": len(reservoirs),
|
||||
"areas": area_stats,
|
||||
"output_file": absolute_output
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
import { tool } from "@opencode-ai/plugin";
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"开始一个新的业务活动阶段。仅在语义阶段发生变化时调用一次,用 title 概括阶段,用 reason 说明本阶段为何必要;同一阶段内的多个工具动作不要重复调用。该工具只更新用户可见的过程信息,不执行外部操作。",
|
||||
args: {
|
||||
title: tool.schema
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("面向用户的简短业务阶段标题,不包含工具名、函数名、文件名或命令。"),
|
||||
reason: tool.schema
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("本阶段对完成用户目标的必要性,使用一句简洁的自然语言。"),
|
||||
todos: tool.schema
|
||||
.array(
|
||||
tool.schema.object({
|
||||
id: tool.schema.string().optional(),
|
||||
content: tool.schema.string().min(1),
|
||||
status: tool.schema.enum([
|
||||
"pending",
|
||||
"in_progress",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]),
|
||||
priority: tool.schema.enum(["low", "medium", "high"]).optional(),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe(
|
||||
"已有任务计划时提交完整状态快照,使本阶段与任务状态在同一次更新中生效。",
|
||||
),
|
||||
},
|
||||
async execute() {
|
||||
return "活动阶段已更新。";
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { tool } from "@opencode-ai/plugin";
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"在前端地图上对节点或管道图层应用样式,或重置为默认样式。样式参数应尽量与前端样式编辑器字段保持一致。",
|
||||
args: {
|
||||
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}图层样式请求。`;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
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:
|
||||
"在当前对话专属的 Landlock 沙箱中运行 Shell 命令。只能读写当前对话工作区,不能访问网络、密钥、其他对话或应用源码。",
|
||||
args: {
|
||||
command: tool.schema.string().describe("需要在沙箱中执行的 Shell 命令。"),
|
||||
description: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("面向用户的简短命令说明。"),
|
||||
timeout: tool.schema
|
||||
.number()
|
||||
.optional()
|
||||
.describe("超时秒数,默认 120,最大 1800。"),
|
||||
},
|
||||
async execute(args, context) {
|
||||
await context.ask({
|
||||
permission: "bash",
|
||||
patterns: [args.command],
|
||||
always: [args.command],
|
||||
metadata: {
|
||||
command: args.command,
|
||||
...(args.description ? { description: args.description } : {}),
|
||||
},
|
||||
});
|
||||
const response = await fetch(
|
||||
`${internalBaseUrl}/internal/tools/sandbox-shell`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-agent-internal-token": internalToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
session_id: context.sessionID,
|
||||
command: args.command,
|
||||
description: args.description,
|
||||
timeout: args.timeout,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(text);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
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 后端 API。需提供 API 路径、可选的请求方法以及查询参数。",
|
||||
args: {
|
||||
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) {
|
||||
// 工具本身不直接持有用户 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({
|
||||
sessionId: context.sessionID,
|
||||
path: args.path,
|
||||
method: args.method,
|
||||
arguments: args.arguments,
|
||||
}),
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(text);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { tool } from "@opencode-ai/plugin";
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"提交直接展示给用户的最终回答。只能在全部业务动作和其他工具调用完成后调用一次;调用后不得继续调用任何工具或输出额外文本。",
|
||||
args: {
|
||||
answer: tool.schema
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("直接展示给用户的完整最终回答,使用简体中文和 Markdown。"),
|
||||
},
|
||||
async execute() {
|
||||
return "最终回答已提交。";
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
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: {
|
||||
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;
|
||||
},
|
||||
});
|
||||
@@ -3,7 +3,9 @@ import { tool } from "@opencode-ai/plugin";
|
||||
export default tool({
|
||||
description: "在前端地图上定位并高亮指定的管网要素。",
|
||||
args: {
|
||||
ids: tool.schema.array(tool.schema.string()).describe("Feature ids to locate."),
|
||||
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."),
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
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:
|
||||
"管理长期有效的用户偏好或项目事实。支持 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"])
|
||||
.describe("Memory operation to perform."),
|
||||
scope: tool.schema
|
||||
.string()
|
||||
.describe(
|
||||
"Required exact keyword. Use only 'user' for user-level durable preferences/constraints, or 'workspace' for project/environment durable facts. Do not use 'project', Chinese labels, or any alias.",
|
||||
),
|
||||
content: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.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) {
|
||||
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);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { tool } from "@opencode-ai/plugin";
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"在前端地图上对 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: {
|
||||
render_ref: tool.schema
|
||||
.string()
|
||||
.describe(
|
||||
"上一步通过 store_render_ref 获得的渲染引用 ID(res-...)。前端会按该引用拉取完整 payload 并渲染。不可直接传入本地文件路径或完整 JSON 数据。",
|
||||
),
|
||||
},
|
||||
async execute() {
|
||||
// 工具参数里只需要 render_ref;浏览器端会再用该引用回读完整 payload.data 并完成渲染。
|
||||
return "已在地图上应用节点分区渲染。";
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
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:
|
||||
"搜索当前用户和项目范围内的历史会话 transcript。适合回忆过去讨论过的案例、约束和结论,避免把一次性案例写入 memory。",
|
||||
args: {
|
||||
query: tool.schema
|
||||
.string()
|
||||
.describe("What to search for in prior session history."),
|
||||
max_results: tool.schema
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Optional maximum number of hits to return."),
|
||||
},
|
||||
async execute(args, context) {
|
||||
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,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(text);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
});
|
||||
@@ -1,25 +1,36 @@
|
||||
import { tool } from "@opencode-ai/plugin";
|
||||
|
||||
export default tool({
|
||||
description: "在前端对话界面中渲染图表。",
|
||||
description:
|
||||
"在前端对话界面中渲染图表。折线图/柱状图必须使用 x_data 作为横轴标签,series[].data 作为同长度的一维数值数组,不要把折线数据写成 ECharts 的 [x, y] 二维点数组。",
|
||||
args: {
|
||||
title: tool.schema.string().optional().describe("Chart title."),
|
||||
chart_type: tool.schema
|
||||
.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 后直接渲染,不再二次请求后端。
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
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:
|
||||
"维护已验证、可复用、非敏感的 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",
|
||||
"remove_reference",
|
||||
"write_script",
|
||||
"remove_script",
|
||||
])
|
||||
.describe("Skill maintenance operation."),
|
||||
skill_path: tool.schema
|
||||
.string()
|
||||
.describe(
|
||||
"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."),
|
||||
target_id: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Stable learned pattern id used by remove_pattern."),
|
||||
file_path: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Asset file path. For references use references/*.md; for scripts use scripts/*.py."),
|
||||
content: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Content used by write_skill, write_reference, or write_script."),
|
||||
},
|
||||
async execute(args, context) {
|
||||
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);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
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 ?? "";
|
||||
|
||||
type StoreRenderRefArgs = {
|
||||
file_path?: unknown;
|
||||
filePath?: unknown;
|
||||
};
|
||||
|
||||
export function resolveStoreRenderFilePath(args: StoreRenderRefArgs): string {
|
||||
if (typeof args.file_path === "string" && args.file_path.trim() !== "") {
|
||||
return args.file_path;
|
||||
}
|
||||
if (typeof args.filePath === "string" && args.filePath.trim() !== "") {
|
||||
return args.filePath;
|
||||
}
|
||||
throw new Error("file_path is required");
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"导入当前对话工作目录下的受控 JSON 包装文件并返回 render_ref。文件必须是 { metadata: object, location: { file_path: string }, data: { node_area_map, area_ids?, area_colors? } },location.file_path 必须与传入的绝对路径完全一致。只接受当前对话工作目录内的真实文件,不接受其他对话目录、目录外路径或指向目录外的符号链接。",
|
||||
args: {
|
||||
file_path: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"位于当前对话工作目录内的包装 JSON 文件绝对路径。必须包含 metadata、location.file_path 和 data;data 才是 render_junctions 使用的 { node_area_map, area_ids?, area_colors? }。",
|
||||
),
|
||||
filePath: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("兼容旧调用的参数名;新调用应优先使用 file_path。"),
|
||||
},
|
||||
async execute(args, context) {
|
||||
const filePath = resolveStoreRenderFilePath(args);
|
||||
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: filePath,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(text);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
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。命令路径和参数不是可自由拼接的语法;若当前会话或已加载工作流没有经过验证的完整命令,必须先调用 help 或 help <命令族>,再从返回的 command、usage 和 options 中选择。",
|
||||
args: {
|
||||
command: tool.schema
|
||||
.string()
|
||||
.describe(
|
||||
"不含二进制路径。只可使用 help 响应或已验证工作流中出现的完整命令路径和参数,禁止类推不同命令族的层级,例如 analysis runs list 存在不代表 simulation runs list 存在。无法确认时调用 'help'、'help simulation' 或相应前缀的 help。",
|
||||
),
|
||||
timeout: tool.schema
|
||||
.number()
|
||||
.optional()
|
||||
.describe("超时秒数,默认 120。大结果集建议设 300+。"),
|
||||
store_result: tool.schema
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"是否强制把结果保存到当前对话工作区并返回 data_file。分析脚本需要文件输入时设为 true。",
|
||||
),
|
||||
},
|
||||
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,
|
||||
command: args.command,
|
||||
store_result: args.store_result,
|
||||
timeout: args.timeout,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(text);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
});
|
||||
@@ -9,8 +9,14 @@ export default tool({
|
||||
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 参数完成。
|
||||
|
||||
@@ -7,13 +7,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 面板仍在浏览器侧执行,工具结果不承载实际监测数据。
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
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: {
|
||||
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;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { tool } from "@opencode-ai/plugin";
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"在前端地图上缩放定位到坐标。默认坐标为 EPSG:3857;如果来自天地图 geocode 的 lon/lat,传 source_crs='EPSG:4326',前端会转换为 EPSG:3857 后缩放。",
|
||||
args: {
|
||||
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 "已缩放到指定地图坐标。";
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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.
|
||||
|
||||
Automatic approval for `glob` and `grep` must remain limited to canonical paths inside an explicit safe workspace subtree. Broad source-workspace searches, symlink escapes, external paths, and `.env`, ordinary `data/`, or `logs/` targets must stay interactive. The current session's canonical `data/conversation-workspaces/<conversation-id>/` directory is the only `data/` exception; access must still reject symlinks and every other conversation directory.
|
||||
+111
-16
@@ -1,33 +1,128 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM smanx/opencode:1.18.13@sha256:b976acda21efffacd44abd7847dac7d646910dbaa477d1877e2881b39cf22a91 AS base
|
||||
USER root
|
||||
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
|
||||
ENV PATH="$VIRTUAL_ENV/bin:/root/.local/bin:$PATH"
|
||||
ENV PIP_INDEX_URL=${PYPI_INDEX_URL}
|
||||
ENV PIP_TRUSTED_HOST=${PYPI_TRUSTED_HOST}
|
||||
ENV UV_INDEX_URL=${PYPI_INDEX_URL}
|
||||
|
||||
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 \
|
||||
libseccomp2 \
|
||||
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 && \
|
||||
mkdir -p /root/.config/pip && \
|
||||
printf "[global]\nindex-url = %s\ntrusted-host = %s\n" "$PIP_INDEX_URL" "$PIP_TRUSTED_HOST" > /root/.config/pip/pip.conf && \
|
||||
uv venv "$VIRTUAL_ENV" && \
|
||||
uv pip install --python "$VIRTUAL_ENV/bin/python" \
|
||||
--index-url "$UV_INDEX_URL" \
|
||||
pip \
|
||||
setuptools \
|
||||
wheel \
|
||||
requests \
|
||||
httpx \
|
||||
pydantic \
|
||||
python-dotenv \
|
||||
rich \
|
||||
ipython \
|
||||
pytest && \
|
||||
(getent group 10001 >/dev/null || groupadd --gid 10001 tjwater-sandbox) && \
|
||||
(getent passwd 10001 >/dev/null || useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin tjwater-sandbox) && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM base AS opencode-builder
|
||||
|
||||
ARG OPENCODE_SOURCE_COMMIT=a105350812f05f914c768e468559dbd6bd508d8e
|
||||
ARG OPENCODE_PATCH_VERSION=1.18.13-tjwater.1
|
||||
WORKDIR /tmp/opencode-src
|
||||
|
||||
RUN git init . && \
|
||||
git remote add origin https://github.com/anomalyco/opencode.git && \
|
||||
git fetch --depth 1 origin "$OPENCODE_SOURCE_COMMIT" && \
|
||||
git checkout --detach FETCH_HEAD
|
||||
|
||||
COPY patches/opencode-1.18.13-message-phase.patch /tmp/opencode-message-phase.patch
|
||||
|
||||
RUN git apply --check /tmp/opencode-message-phase.patch && \
|
||||
git apply /tmp/opencode-message-phase.patch && \
|
||||
bun install --frozen-lockfile --ignore-scripts && \
|
||||
bun test --cwd packages/llm test/provider/openai-responses.test.ts && \
|
||||
OPENCODE_VERSION="$OPENCODE_PATCH_VERSION" bun run --cwd packages/opencode build --single --skip-install --skip-embed-web-ui && \
|
||||
case "$(uname -m)" in \
|
||||
x86_64) binary=packages/opencode/dist/opencode-linux-x64/bin/opencode ;; \
|
||||
aarch64|arm64) binary=packages/opencode/dist/opencode-linux-arm64/bin/opencode ;; \
|
||||
*) echo "unsupported OpenCode build architecture: $(uname -m)" >&2; exit 1 ;; \
|
||||
esac && \
|
||||
install -D -m 0755 "$binary" /out/opencode
|
||||
|
||||
FROM base AS deps
|
||||
|
||||
FROM oven/bun:1.3.13 AS deps
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock ./
|
||||
COPY .opencode/package.json .opencode/bun.lock ./.opencode/
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
FROM deps AS build
|
||||
FROM base AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY tsconfig.json opencode.json README.md ./
|
||||
COPY src ./src
|
||||
COPY .opencode ./.opencode
|
||||
RUN bun run check
|
||||
|
||||
FROM oven/bun:1.3.13 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 tsconfig.json opencode.json README.md .gitignore Dockerfile ./
|
||||
COPY src ./src
|
||||
COPY cli ./cli
|
||||
COPY scripts ./scripts
|
||||
COPY .opencode ./.opencode
|
||||
RUN bun run check
|
||||
|
||||
FROM build AS test
|
||||
COPY --from=opencode-builder /out/opencode /usr/local/bin/opencode
|
||||
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
|
||||
|
||||
COPY --from=opencode-builder /out/opencode /usr/local/bin/opencode
|
||||
RUN opencode --version
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=8787
|
||||
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 ./cli/tjwater-cli
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
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"]
|
||||
|
||||
@@ -1,273 +1,129 @@
|
||||
# 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 相关依赖 |
|
||||
仓库跟踪 `.opencode/skills/` 中经过评审的默认工作流基线;部署环境仍可通过持久化卷保留 `skill_manager` 在运行中沉淀的增量内容。默认基线不得包含真实客户数据、认证信息或本地执行产物。
|
||||
|
||||
## `TJWaterAgent/` 根目录的职责
|
||||
## 本地开发
|
||||
|
||||
根目录是 Node/TypeScript 服务本体,主要负责:
|
||||
|
||||
1. 启动 HTTP 服务。
|
||||
2. 通过 `@opencode-ai/sdk` 启动内嵌 opencode server,或连接外部 opencode server。
|
||||
3. 管理前端 `session_id -> opencode sessionId` 的映射。
|
||||
4. 保存并传递用户 `Authorization`、`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` CLI。服务会通过 `@opencode-ai/sdk` 的 embedded 模式启动 opencode server。
|
||||
|
||||
根目录的 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
|
||||
```
|
||||
|
||||
因此修改 agent prompt、tools、skills、模型配置或本地环境变量后,不需要手动重启 `bun run dev`。
|
||||
|
||||
本地开发可以在项目根目录的 `.local.env` 中配置环境变量:
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
DEEPSEEK_API_KEY=sk-xxx
|
||||
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 .
|
||||
```
|
||||
|
||||
- `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`:先类型检查,再启动服务。
|
||||
|
||||
## 运行模式
|
||||
|
||||
当前运行时使用 OpenCode 稳定版 1.x CLI,并通过稳定版 SDK 的 `@opencode-ai/sdk/v2` HTTP 客户端访问运行时;这与 `opencode2` 及 `@opencode-ai/client` 的 2.0 beta 运行时不同。Embedded 模式由服务进程拉起本机 OpenCode:
|
||||
|
||||
```bash
|
||||
OPENCODE_MODE=embedded
|
||||
TJWATER_API_BASE_URL=http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
服务启动时会自动读取 `.local.env`,但系统环境变量优先级更高,适合在本机保存开发用 key。
|
||||
当前仅支持 Embedded 模式,不支持连接外部 OpenCode server。
|
||||
|
||||
### 生产启动
|
||||
生产镜像会从固定的 OpenCode `v1.18.13` 源码提交构建 CLI,并应用仓库内的
|
||||
`patches/opencode-1.18.13-message-phase.patch`。该补丁只透传 OpenAI Responses
|
||||
输出项已有的 `commentary` / `final_answer` phase,不改变模型行为:过程文本继续写入
|
||||
可折叠的 Agent 过程卡,`final_answer` 到达后立即按增量写入正式回答。未提供 phase 的
|
||||
DeepSeek 模型启用 OpenCode 1.18.13 内置的 JSON Schema 最终回答工具
|
||||
`StructuredOutput`:模型必须先完成全部分析和工具调用,再把完整回答写入 `answer`;
|
||||
该工具成功后 OpenCode 会直接结束运行循环,不再进入下一轮模型或工具调用。Agent 将
|
||||
`answer` 映射为正式文本推送;若模型未按协议调用该工具,仍保留会话 idle 后提取最终
|
||||
文本的兼容兜底。
|
||||
本地直接运行 `bun --watch src/server.ts` 时,`PATH` 中也需要放置应用了同一补丁的
|
||||
`opencode` CLI,才能启用 phase 驱动的正式文本流式输出。
|
||||
|
||||
## 认证续期与学习工具
|
||||
|
||||
后端工具调用遇到即将过期的 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 明确允许的白名单,Shell 和写操作逐次交给用户确认;“自动批准”额外自动放行低风险业务工具、skill、沙箱 Shell,以及真实路径位于当前 conversation workspace 内且通过 realpath/symlink 校验的 read/edit/glob/grep;“始终允许”自动放行当前对话中所有未被 OpenCode 明确禁止的权限请求。自动放行统一使用单次批准,切换整体模式后立即恢复对应策略,不会写入持久授权。
|
||||
|
||||
单次权限请求支持“允许一次”“保存授权”和“拒绝”。“保存授权”使用 OpenCode 的 `always` 回复,仅保存 OpenCode 为本次请求建议的权限范围,并只在当前 OpenCode 会话内生效。任意外部目录默认仍由静态配置禁止;`.env`、普通 `data/`、`logs/` 和其他对话目录保持禁止。真实聊天会话使用 `data/conversation-workspaces/<随机目录>/` 作为独立工作目录。普通 `rm <文件>`、`rmdir` 和非强制递归删除可在沙箱内执行,`rm -rf`/`rm -fr` 及等价的递归强制删除形式会在执行前拒绝。
|
||||
|
||||
OpenCode 的内置 Bash 由同名自定义工具覆盖。命令经内部鉴权路由进入独立子进程,切换到专用非 root UID 后应用 Landlock 文件规则和 seccomp 网络规则:当前 conversation workspace 可读写,系统/Python/skills 只读,其他应用文件、其他对话和全局 `tool-output` 不可见;IPv4/IPv6 TCP 与 UDP socket 均被拒绝。启动时会探测 Landlock ABI(要求 ≥4)和 libseccomp,失败时 Agent 直接启动失败,不会回退到未沙箱化 Shell。Shell 环境不包含模型 key、内部 token 或用户 access token,`HOME`、`TMPDIR` 和 Python 缓存均位于当前对话目录。
|
||||
|
||||
`store_render_ref` 只会从当前对话绑定的工作目录导入包装格式 JSON;工作区根目录固定为项目内的 `./data/conversation-workspaces`,以确保 OpenCode 能继续发现项目配置和工具。文件必须包含 `metadata`、`location.file_path` 和 `data`,且真实路径不能越出当前对话目录;单文件默认上限为 128 MiB,成功导入后只删除这一份源包装文件。升级前已经存在的会话没有独立工作目录,需要新建对话后才能使用该导入能力。
|
||||
|
||||
CLI 桥接层对 stdout 设置独立的 128 MiB 硬上限(`MAX_CLI_OUTPUT_BYTES`);stderr 最多保留 256 KiB(`MAX_CLI_STDERR_BYTES`),超出后截断但不会终止 CLI。`MAX_INLINE_RESULT_BYTES`(默认 12000 字节)仅决定内联还是落盘:较大结果写入当前对话的 `tool-data/` 并返回 `data_file.file_path`,不会因为超过 12000 字节杀掉 CLI;分析脚本需要文件输入时可由 `tjwater_cli(store_result=true)` 强制落盘小结果。会话暂存数据不自动删除。OpenCode 自身为其他工具生成的全局 `tool-output` 仍按 `RESULT_REF_TTL_HOURS`(默认 7 天)清理,但沙箱命令不能访问该目录。
|
||||
|
||||
## 配置与安全
|
||||
|
||||
不要提交 `.env`、`.local.env`、`data/`、`logs/`、会话记录、模型输出、访问令牌或 `node_modules/`。部署凭据、镜像仓库账号和 webhook 地址应放在 Gitea secrets 或部署环境变量中。
|
||||
|
||||
## 发布
|
||||
|
||||
Gitea 包工作流位于 `.gitea/workflows/package.yml`。发布前至少运行:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 常用脚本
|
||||
|
||||
| 命令 | 作用 |
|
||||
| --- | --- |
|
||||
| `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` 依赖 |
|
||||
|
||||
### 模型与 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_BASE_URL=http://127.0.0.1:4096
|
||||
```
|
||||
|
||||
配置后,`TJWaterAgent` 会连接该外部 opencode server,而不是自行启动 embedded opencode server。
|
||||
>>>>>>> 414247d (新增 skills、README,指定 opencode 的启动行为)
|
||||
|
||||
@@ -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,15 +14,19 @@
|
||||
"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",
|
||||
"bun-types": "^1.3.3",
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"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=="],
|
||||
|
||||
@@ -56,6 +60,8 @@
|
||||
|
||||
"body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
@@ -172,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=="],
|
||||
@@ -256,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=="],
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { CliError } from "../core/errors.js";
|
||||
import { emitApi } 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 type { HandlerMap, RuntimeContext } from "../core/types.js";
|
||||
|
||||
function analysisBurst(ctx: RuntimeContext, argv: string[]): Promise<void> {
|
||||
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 analysis runs list"]);
|
||||
}
|
||||
|
||||
function analysisValveIsolation(ctx: RuntimeContext, argv: string[]): Promise<void> {
|
||||
const { values } = parseOptions(argv, { element: "repeat", "disabled-valve": "repeat" });
|
||||
const elements = optionalStringArray(values, "element");
|
||||
if (!elements) throw new CliError("CLI 参数错误", "INVALID_VALVE_ISOLATION_ARGS", "at least one --element is required", 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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
const { values } = parseOptions(argv, { duration: "integer", concentration: "number" });
|
||||
const params: Record<string, unknown> = {
|
||||
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 sensorPlacementRun(ctx: RuntimeContext, argv: string[]): Promise<void> {
|
||||
const { values } = parseOptions(argv, { count: "integer", "min-diameter": "integer" });
|
||||
return emitApi(ctx, "传感器选址执行成功", {
|
||||
method: "POST",
|
||||
path: "/sensor-placement-runs",
|
||||
body: {
|
||||
run_name: requiredString(values, "run-name"),
|
||||
sensor_type: "pressure",
|
||||
method: validateChoice(requiredString(values, "method"), ["sensitivity", "kmeans"] as const, "--method"),
|
||||
sensor_count: 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<void> {
|
||||
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 analysisRunGet(ctx: RuntimeContext, argv: string[]): Promise<void> {
|
||||
const { values } = parseOptions(argv);
|
||||
const runId = requiredString(values, "run-id");
|
||||
return emitApi(ctx, "读取分析运行成功", {
|
||||
method: "GET",
|
||||
path: `/analysis/runs/${encodeURIComponent(runId)}`,
|
||||
requireProject: true,
|
||||
});
|
||||
}
|
||||
|
||||
function analysisRunResults(ctx: RuntimeContext, argv: string[]): Promise<void> {
|
||||
const { values } = parseOptions(argv);
|
||||
const runId = requiredString(values, "run-id");
|
||||
return emitApi(ctx, "读取分析结果成功", {
|
||||
method: "GET",
|
||||
path: `/analysis/runs/${encodeURIComponent(runId)}/results`,
|
||||
params: { result_type: optionalString(values, "result-type") },
|
||||
requireProject: true,
|
||||
});
|
||||
}
|
||||
|
||||
function burstLocation(ctx: RuntimeContext, argv: string[]): Promise<void> {
|
||||
const { values } = parseOptions(argv, { "burst-leakage": "number", "pressure-scada-id": "repeat", "flow-scada-id": "repeat", "use-scada-flow": "boolean" });
|
||||
const body: Record<string, unknown> = {
|
||||
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 });
|
||||
}
|
||||
|
||||
export const analysisHandlers: HandlerMap = {
|
||||
"analysis burst": analysisBurst,
|
||||
"analysis valve isolation": analysisValveIsolation,
|
||||
"analysis flushing": analysisFlushing,
|
||||
"analysis age": analysisAge,
|
||||
"analysis contaminant": analysisContaminant,
|
||||
"analysis sensor-placement run": sensorPlacementRun,
|
||||
"analysis sensor-placement list": (ctx) => emitApi(ctx, "读取传感器选址运行列表成功", { method: "GET", path: "/sensor-placement-runs", requireProject: true }),
|
||||
"analysis sensor-placement get": (ctx, argv) => {
|
||||
const { values } = parseOptions(argv);
|
||||
return emitApi(ctx, "读取传感器选址运行成功", { method: "GET", path: `/sensor-placement-runs/${encodeURIComponent(requiredString(values, "run-id"))}`, requireProject: true });
|
||||
},
|
||||
"analysis runs list": (ctx) => emitApi(ctx, "读取分析运行列表成功", { method: "GET", path: "/analysis/runs", requireProject: true }),
|
||||
"analysis runs get": analysisRunGet,
|
||||
"analysis runs results": analysisRunResults,
|
||||
"analysis leakage identify": (ctx, argv) => schemeAnalysis(ctx, argv, "漏损识别执行成功", "/leakage-identifications", "scada_start", "scada_end"),
|
||||
"analysis burst-detection detect": (ctx, argv) => schemeAnalysis(ctx, argv, "爆管检测执行成功", "/burst-detections", "scada_start", "scada_end"),
|
||||
"analysis burst-location locate": burstLocation,
|
||||
};
|
||||
@@ -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<void> {
|
||||
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<string, unknown> = {};
|
||||
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),
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
import { SCADA_FIELDS, type ElementType } from "../core/constants.js";
|
||||
import { CliError } from "../core/errors.js";
|
||||
import { emitApi, requestAllPages } from "../core/http.js";
|
||||
import { fieldsFor, optionalNumber, optionalString, parseOptions, requiredString, requiredStringArray, validateChoice } from "../core/options.js";
|
||||
import { success } from "../core/output.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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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 analysisSeries(ctx: RuntimeContext, argv: string[], type: ElementType): Promise<void> {
|
||||
const { values } = parseOptions(argv);
|
||||
const runId = requiredString(values, "run-id");
|
||||
const idOption = type === "pipe" ? "link" : "node";
|
||||
const elementId = requiredString(values, idOption);
|
||||
const elementPath = type === "pipe" ? "links" : "nodes";
|
||||
return emitApi(ctx, type === "pipe" ? "读取分析管道字段成功" : "读取分析节点字段成功", {
|
||||
method: "GET",
|
||||
path: `/timeseries/analysis/runs/${encodeURIComponent(runId)}/${elementPath}/${encodeURIComponent(elementId)}`,
|
||||
params: {
|
||||
field: validateChoice(requiredString(values, "field"), fieldsFor(type), "--field"),
|
||||
start_time: parseTime(requiredString(values, "start-time"), "--start-time"),
|
||||
end_time: parseTime(requiredString(values, "end-time"), "--end-time"),
|
||||
},
|
||||
requireProject: true,
|
||||
});
|
||||
}
|
||||
|
||||
function analysisValues(ctx: RuntimeContext, argv: string[]): Promise<void> {
|
||||
const { values } = parseOptions(argv);
|
||||
const type = validateChoice(requiredString(values, "type"), ["pipe", "junction"] as const, "--type");
|
||||
return emitApi(ctx, "读取分析运行指定时刻结果成功", {
|
||||
method: "GET",
|
||||
path: `/timeseries/analysis/runs/${encodeURIComponent(requiredString(values, "run-id"))}/values`,
|
||||
params: {
|
||||
result_time: parseTime(requiredString(values, "time"), "--time"),
|
||||
element_type: backendElementType(type),
|
||||
field: validateChoice(requiredString(values, "field"), fieldsFor(type), "--field"),
|
||||
},
|
||||
requireProject: true,
|
||||
});
|
||||
}
|
||||
|
||||
function scadaQuery(ctx: RuntimeContext, argv: string[]): Promise<void> {
|
||||
const { values } = parseOptions(argv, { "device-id": "repeat" });
|
||||
const params: Record<string, unknown> = {
|
||||
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<void> {
|
||||
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<string, unknown> = {
|
||||
start_time: parseTime(requiredString(values, "start-time"), "--start-time"),
|
||||
end_time: parseTime(requiredString(values, "end-time"), "--end-time"),
|
||||
};
|
||||
const runId = optionalString(values, "run-id");
|
||||
if (runId && kind !== "element-scada") params.run_id = runId;
|
||||
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<void> {
|
||||
const { values } = parseOptions(argv);
|
||||
return emitApi(ctx, "读取管道健康预测成功", {
|
||||
method: "GET",
|
||||
path: "/pipeline-health-predictions",
|
||||
params: { query_time: parseTime(requiredString(values, "time"), "--time") },
|
||||
requireProject: true,
|
||||
});
|
||||
}
|
||||
|
||||
function dataScadaGet(ctx: RuntimeContext, argv: string[]): Promise<void> {
|
||||
const { values } = parseOptions(argv);
|
||||
return emitApi(ctx, "读取 SCADA 设备成功", { method: "GET", path: "/scada-devices/detail", params: { device_id: requiredString(values, "device-id") }, requireProject: true });
|
||||
}
|
||||
|
||||
async function dataScadaList(ctx: RuntimeContext, argv: string[]): Promise<void> {
|
||||
const { values } = parseOptions(argv, { "page-size": "integer" });
|
||||
const pageSize = Math.min(1000, Math.max(1, optionalNumber(values, "page-size") ?? 1000));
|
||||
const [data, durationMs] = await requestAllPages(
|
||||
ctx,
|
||||
{ method: "GET", path: "/scada-devices", requireProject: true },
|
||||
pageSize,
|
||||
);
|
||||
success("读取 SCADA 设备列表成功", data, ctx, durationMs);
|
||||
}
|
||||
|
||||
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 analysis link-field": (ctx, argv) => analysisSeries(ctx, argv, "pipe"),
|
||||
"data timeseries analysis node-field": (ctx, argv) => analysisSeries(ctx, argv, "junction"),
|
||||
"data timeseries analysis values": analysisValues,
|
||||
"data timeseries scada query": scadaQuery,
|
||||
"data timeseries composite": composite,
|
||||
"data pipeline-health": pipelineHealth,
|
||||
"data scada get": dataScadaGet,
|
||||
"data scada list": dataScadaList,
|
||||
"data scada schema": (ctx) => emitApi(ctx, "读取 SCADA 设备 schema 成功", { method: "GET", path: "/network-schemas/scada-device", requireProject: true }),
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { emitApi, requestAllPages } from "../core/http.js";
|
||||
import { optionalNumber, parseOptions, requiredString } from "../core/options.js";
|
||||
import { success } from "../core/output.js";
|
||||
import type { HandlerMap, RuntimeContext } from "../core/types.js";
|
||||
|
||||
function apiGet(ctx: RuntimeContext, argv: string[], summary: string, path: string, key: string): Promise<void> {
|
||||
const { values } = parseOptions(argv);
|
||||
return emitApi(ctx, summary, { method: "GET", path, params: { [key]: requiredString(values, key) }, requireProject: true });
|
||||
}
|
||||
|
||||
async function apiGetAll(ctx: RuntimeContext, argv: string[], summary: string, path: string): Promise<void> {
|
||||
const { values } = parseOptions(argv, { limit: "integer", "page-size": "integer" });
|
||||
const requestedPageSize = optionalNumber(values, "page-size") ?? optionalNumber(values, "limit") ?? 1000;
|
||||
const pageSize = Math.min(1000, Math.max(1, requestedPageSize));
|
||||
const [data, durationMs] = await requestAllPages(
|
||||
ctx,
|
||||
{ method: "GET", path, requireProject: true },
|
||||
pageSize,
|
||||
);
|
||||
success(summary, data, ctx, durationMs);
|
||||
}
|
||||
|
||||
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, argv) => apiGetAll(ctx, argv, "读取全部管道属性成功", "/pipes"),
|
||||
"network get-reservoir-properties": (ctx, argv) => apiGet(ctx, argv, "读取水库属性成功", "/reservoirs/properties", "reservoir"),
|
||||
"network get-all-reservoirs-properties": (ctx, argv) => apiGetAll(ctx, argv, "读取全部水库属性成功", "/reservoirs"),
|
||||
"network get-tank-properties": (ctx, argv) => apiGet(ctx, argv, "读取水箱属性成功", "/tanks/properties", "tank"),
|
||||
"network get-all-tanks-properties": (ctx, argv) => apiGetAll(ctx, argv, "读取全部水箱属性成功", "/tanks"),
|
||||
"network get-pump-properties": (ctx, argv) => apiGet(ctx, argv, "读取水泵属性成功", "/pumps/properties", "pump"),
|
||||
"network get-all-pumps-properties": (ctx, argv) => apiGetAll(ctx, argv, "读取全部水泵属性成功", "/pumps"),
|
||||
"network get-valve-properties": (ctx, argv) => apiGet(ctx, argv, "读取阀门属性成功", "/valves/properties", "valve"),
|
||||
"network get-all-valves-properties": (ctx, argv) => apiGetAll(ctx, argv, "读取全部阀门属性成功", "/valves"),
|
||||
};
|
||||
@@ -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<void> {
|
||||
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,
|
||||
};
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { CliError } from "./errors.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
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<string, unknown>).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<string, unknown>).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<string, unknown>, 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
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<string, string> {
|
||||
const out: Record<string, string> = {
|
||||
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<string, unknown> = {}): 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<string, unknown>) : {};
|
||||
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<string, unknown>;
|
||||
if (record.status === "error") throw new CliError("服务端错误", "SERVER_ERROR", String(record.message || "server returned error status"), 7, false, payload);
|
||||
}
|
||||
return [payload, durationMs];
|
||||
}
|
||||
|
||||
export async function requestAllPages(
|
||||
ctx: RuntimeContext,
|
||||
request: RequestOptions,
|
||||
pageSize: number,
|
||||
): Promise<[unknown[], number]> {
|
||||
const items: unknown[] = [];
|
||||
let durationMs = 0;
|
||||
let offset = 0;
|
||||
let expectedTotal: number | null = null;
|
||||
|
||||
while (expectedTotal === null || offset < expectedTotal) {
|
||||
const [payload, pageDurationMs] = await requestJson(ctx, {
|
||||
...request,
|
||||
params: {
|
||||
...request.params,
|
||||
limit: pageSize,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
durationMs += pageDurationMs;
|
||||
const page = normalizePage(payload);
|
||||
if (!page) {
|
||||
throw new CliError(
|
||||
"服务端错误",
|
||||
"INVALID_PAGINATION_RESPONSE",
|
||||
"backend collection response must contain items, total, limit, and offset",
|
||||
7,
|
||||
false,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
if (expectedTotal === null) {
|
||||
expectedTotal = page.total;
|
||||
} else if (page.total !== expectedTotal) {
|
||||
throw new CliError(
|
||||
"服务端错误",
|
||||
"PAGINATION_TOTAL_CHANGED",
|
||||
`backend collection total changed from ${expectedTotal} to ${page.total}`,
|
||||
7,
|
||||
true,
|
||||
);
|
||||
}
|
||||
if (page.offset !== offset) {
|
||||
throw new CliError(
|
||||
"服务端错误",
|
||||
"PAGINATION_OFFSET_MISMATCH",
|
||||
`backend collection returned offset ${page.offset}, expected ${offset}`,
|
||||
7,
|
||||
true,
|
||||
);
|
||||
}
|
||||
if (page.items.length === 0 && offset < expectedTotal) {
|
||||
throw new CliError(
|
||||
"服务端错误",
|
||||
"PAGINATION_STALLED",
|
||||
`backend collection returned an empty page at offset ${offset} before total ${expectedTotal}`,
|
||||
7,
|
||||
true,
|
||||
);
|
||||
}
|
||||
items.push(...page.items);
|
||||
offset += page.items.length;
|
||||
}
|
||||
|
||||
return [items.slice(0, expectedTotal ?? 0), durationMs];
|
||||
}
|
||||
|
||||
function normalizePage(
|
||||
payload: unknown,
|
||||
): { items: unknown[]; limit: number; offset: number; total: number } | null {
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
||||
const page = payload as Record<string, unknown>;
|
||||
if (
|
||||
!Array.isArray(page.items) ||
|
||||
typeof page.limit !== "number" ||
|
||||
!Number.isInteger(page.limit) ||
|
||||
typeof page.offset !== "number" ||
|
||||
!Number.isInteger(page.offset) ||
|
||||
typeof page.total !== "number" ||
|
||||
!Number.isInteger(page.total) ||
|
||||
page.limit <= 0 ||
|
||||
page.offset < 0 ||
|
||||
page.total < 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
items: page.items,
|
||||
limit: page.limit,
|
||||
offset: page.offset,
|
||||
total: page.total,
|
||||
};
|
||||
}
|
||||
|
||||
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<void> {
|
||||
const [data, durationMs] = await requestJson(ctx, request);
|
||||
success(summary, data, ctx, durationMs, nextCommands);
|
||||
}
|
||||
@@ -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<string, ParsedOptionValue> = {};
|
||||
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<string, ParsedOptionValue>, 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<string, ParsedOptionValue>, 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<string, ParsedOptionValue>, 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<string, ParsedOptionValue>, 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<string, ParsedOptionValue>, 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<string, ParsedOptionValue>, 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<string, ParsedOptionValue>, 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<T extends readonly string[]>(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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user