Author SHA1 Message Date
jiang 80cfc1f2ab feat(agent): sandbox conversation analysis
Generic Container CI/CD / test-build-publish (push) Successful in 2m44s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 2m44s
2026-08-25 16:08:33 +08:00
jiang ce04704af2 fix(agent): isolate conversation workspaces
Generic Container CI/CD / test-build-publish (push) Successful in 2m2s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 2m2s
2026-08-25 13:18:39 +08:00
jiang 004c9bb72d fix(agent): stabilize large tool results
Generic Container CI/CD / test-build-publish (push) Successful in 1m53s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m53s
2026-08-25 12:02:48 +08:00
jiang 774f39cbbe fix(chat): restore tool execution details
Generic Container CI/CD / test-build-publish (push) Successful in 2m4s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 2m4s
2026-08-24 18:45:45 +08:00
jiang c6efccb88a fix(chat): only expose final agent response
Generic Container CI/CD / test-build-publish (push) Successful in 1m4s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m4s
2026-08-24 18:30:24 +08:00
jiang 11ebf428bb merge: integrate tjwater-cli into main
Merge PR #1 after CLI, contract, test, and container gates passed.
2026-08-18 17:56:44 +08:00
jiang 18e8b25f48 fix(agent): bound CLI subprocess execution 2026-08-18 17:00:23 +08:00
jiang 9aa5a96e60 merge(agent): integrate main into tjwater-cli 2026-08-18 16:42:15 +08:00
jiang b49290bd91 fix(container): isolate Agent runtime state
Generic Container CI/CD / test-build-publish (push) Successful in 3m1s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 3m1s
2026-08-11 10:10:30 +08:00
jiang 31fdb36e48 ci: activate Agent v2 deployment workflow 2026-08-11 09:29:30 +08:00
jiang c565e89d60 ci(agent): 添加新版发布与传输验证工作流 2026-08-07 18:00:54 +08:00
jiang 4cbeca4e09 fix(permissions): require approval for backend queries 2026-08-06 20:03:20 +08:00
jiang f66b9c3e9d fix(agent): disable unobservable task subagents 2026-08-06 19:24:03 +08:00
jiang b19af8846a feat(agent): 完善权限与结果引用安全 2026-08-06 18:41:52 +08:00
jiang a5e91ac2b8 test(cli): remove Python parity dependency 2026-08-06 15:58:47 +08:00
jiang 258f4996eb feat(agent): add credential refresh and unify learning tools 2026-08-06 10:16:50 +08:00
jiang 2dc37e3fd8 fix(agent): preserve runtime request context
OpenCode tools run in a child service, so they cannot read the Agent process-local session map. Hydrate a sanitized context through the authenticated internal bridge and centralize backend project headers to prevent context loss across both boundaries.
2026-08-05 18:39:09 +08:00
jiang a53839e157 fix(cli): map timeseries element types for backend 2026-08-05 18:39:00 +08:00
jiang 1407dd3bbe fix(opencode): restore stable v1 runtime 2026-08-05 17:59:35 +08:00
jiang 764a1f4e82 feat(opencode): migrate agent runtime to v2 2026-08-04 16:56:04 +08:00
jiang 07016451d6 fix(agent): complete opencode warmup before serving
The previous fire-and-forget startup only created the SDK client, leaving the first chat to await project and tool initialization. Warm the real session/tool path and gate port listening on completion.
2026-08-04 15:25:00 +08:00
jiang 94529cb141 feat(api): expose REST-only agent routes
Agent CI/CD / docker-image (push) Failing after 35s
Agent CI/CD / deploy-fallback-log (push) Successful in 1s
2026-07-30 20:38:52 +08:00
jiang 2415f75841 docs: 编写中文 README 2026-07-22 11:26:06 +08:00
jiang d7faaa2ecb fix(docker): make ubuntu apt mirror optional
Agent CI/CD / docker-image (push) Successful in 3m48s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-07-17 17:59:01 +08:00
jiang 78f9f6fb42 fix(ci): build docker image with host network
Agent CI/CD / docker-image (push) Failing after 26s
Agent CI/CD / deploy-fallback-log (push) Successful in 1s
2026-07-17 17:46:53 +08:00
jiang 9aad38acb1 fix(docker): vendor bun binary for image builds
Agent CI/CD / docker-image (push) Failing after 24s
Agent CI/CD / deploy-fallback-log (push) Successful in 1s
2026-07-17 17:42:44 +08:00
jiang d782b7fa11 fix(session): propagate network context
Agent CI/CD / docker-image (push) Failing after 15m0s
Agent CI/CD / deploy-fallback-log (push) Successful in 0s
2026-07-17 16:33:11 +08:00
jiang 40c0395fb1 fix(style): validate layer style configuration 2026-07-17 15:12:15 +08:00
jiang d75b98a61b fix(cli): use renamed backend APIs 2026-06-13 13:56:44 +08:00
jiang ead76185b7 refactor(agent): normalize API naming 2026-06-13 11:15:59 +08:00
jiang 8857b18dc9 feat(auth): validate agent context upstream 2026-06-12 10:18:41 +08:00
jiang 4b572d9264 chore(model): use flash agent model 2026-06-11 15:53:41 +08:00
jiang fc7393fa2b refactor(sessions): remove transcript truncate 2026-06-11 15:52:34 +08:00
jiang c823e3935e feat(chat): expose model options config
Agent CI/CD / docker-image (push) Failing after 24s
Agent CI/CD / deploy-fallback-log (push) Successful in 1s
2026-06-10 19:50:40 +08:00
jiang 366c05b752 chore(model): default to deepseek flash 2026-06-10 19:33:08 +08:00
jiang fa2c28c1c0 refactor(chat): centralize session persistence 2026-06-10 19:29:42 +08:00
jiang cf6cada538 fix(tools): clarify chart data format
Agent CI/CD / docker-image (push) Successful in 49s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-10 16:19:39 +08:00
jiang d1b91a4b1e docs: add repository guidelines
Agent CI/CD / docker-image (push) Successful in 1m1s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-09 18:18:22 +08:00
jiang 20b93c688f feat(tools): add search and map tools 2026-06-09 17:54:46 +08:00
jiang 873c169c2c fix(agent): warm up opencode on startup
Agent CI/CD / docker-image (push) Successful in 1m59s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-08 20:19:46 +08:00
jiang 8ed73b1da6 fix(chat): hide raw permission metadata
Agent CI/CD / docker-image (push) Successful in 2m38s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-08 20:12:08 +08:00
jiang 60b9080c47 refactor(chat): drop branch groups state
Agent CI/CD / docker-image (push) Successful in 49s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-08 19:47:19 +08:00
jiang 801f611ce5 fix(chat): restore forked context
Agent CI/CD / docker-image (push) Successful in 1m38s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-08 19:33:13 +08:00
jiang 15c3263369 fix(chat): handle question and todo state 2026-06-08 18:10:28 +08:00
jiang f20847399a 更新 store_render_ref.ts render_junctions.ts 工具使用说明 2026-06-08 17:40:40 +08:00
jiang f3b62ed108 fix: regenerate from target turn
Agent CI/CD / docker-image (push) Successful in 49s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-08 16:07:39 +08:00
jiang 0e1ca2418f fix(chat): 支持重新生成前撤销消息
Agent CI/CD / docker-image (push) Successful in 5m14s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-08 14:38:52 +08:00
jiang f61389ab07 feat(chat): 支持自动始终允许权限 2026-06-08 14:14:52 +08:00
jiang 05d36aa8ca feat: handle opencode permission requests 2026-06-08 13:32:50 +08:00
jiang 4e31b141e7 添加不使用自代理的要求 2026-06-08 12:31:16 +08:00
jiang 7dbbeb4aa9 升级@opencode-ai版本到1.16.2 2026-06-08 11:46:24 +08:00
jiang 6f3b72628f fix(chat): guard abort and early idle races 2026-06-07 20:22:05 +08:00
jiang 2295bdcb97 暂时移除后端的认证校验
Agent CI/CD / docker-image (push) Successful in 1m5s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-07 19:58:59 +08:00
jiang 359b1e6e55 build(agent): include cli in build stage
Agent CI/CD / docker-image (push) Successful in 55s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-07 19:44:58 +08:00
jiang 93d70da8be refactor(cli): split tjwater cli modules
Agent CI/CD / deploy-fallback-log (push) Has been cancelled
Agent CI/CD / docker-image (push) Has been cancelled
2026-06-07 19:43:44 +08:00
jiang ff87817fb5 fix(cli): show group help 2026-06-07 18:57:48 +08:00
jiang 4b03aa3a91 build(cli): replace bundled binary cli 2026-06-07 18:53:10 +08:00
jiang 8a7964dc57 build(docker): include tjwater cli 2026-06-07 18:02:42 +08:00
jiang 741e39b444 build(docker): fix agent image build
Agent CI/CD / docker-image (push) Successful in 2m3s
Agent CI/CD / deploy-fallback-log (push) Has been skipped
2026-06-07 17:53:19 +08:00
jiang 5020e58b7e feat(auth): validate agent requests 2026-06-07 17:15:40 +08:00
jiang ba46258845 style(opencode): format tool definitions 2026-06-07 17:08:17 +08:00
jiang 9d4e5486e9 refactor: keep runtime context in memory 2026-06-07 17:07:14 +08:00
jiang 1ed7e56f35 refactor: remove legacy data compatibility 2026-06-07 16:56:23 +08:00
jiang 5e0c16f8b2 更新 .gitignore 2026-06-05 14:31:18 +08:00
jiang 67d027e60c Remove .opencode/skills/ from git tracking 2026-06-05 14:30:05 +08:00
jiang 8f0e93ceec 提示词禁止使用 read/cat 读取文件,避免输出过长信息到终端 2026-06-05 13:20:38 +08:00
jiang ad31956f53 完善 skill_manager 的技能维护能力 2026-06-05 13:03:39 +08:00
jiang fc0e76439d fix(chat): 解决token传输、本地文件存储顺序、读取的问题 2026-06-04 18:19:29 +08:00
jiang 10c11a5254 refactor(agent): 移除旧工具桥 2026-06-04 18:02:38 +08:00
jiang f4749d6e2e 增加流式信息中断处理机制 2026-06-04 16:27:13 +08:00
jiang 8a1785c244 更新memory读取机制,新增前需要先list阅读已有的内容 2026-06-04 15:35:01 +08:00
jiang 0188240d62 重建会话记录逻辑 2026-06-04 15:26:23 +08:00
jiang 0ecb2babf3 refactor: unify agent session persistence 2026-06-04 15:02:27 +08:00
jiang 04ded0ceb0 重构会话管理,简化上下文存储逻辑 2026-06-03 17:14:55 +08:00
jiang 76d4b510f4 避免abort后创建新的session 2026-06-03 10:04:00 +08:00
jiang 96e5d25518 更新tjwater-cli skill和环境 2026-06-03 09:49:37 +08:00
jiang a825c3c31d 重新整理提示词和工具说明。 2026-06-02 17:42:02 +08:00
jiang 5b285ad7a5 后端服务将通过tjwater-cli形式访问 2026-06-02 15:31:21 +08:00
jiang 20329bb771 新增应用样式工具 2026-05-29 10:28:08 +08:00
188 changed files with 16981 additions and 5082 deletions
+4 -1
View File
@@ -1,8 +1,11 @@
.git .git
.gitignore
node_modules node_modules
.opencode/node_modules .opencode/node_modules
.env
.env.*
.local.env .local.env
data/
logs/
dist dist
.vscode .vscode
*.log *.log
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
dockerfile: Dockerfile dockerfile: Dockerfile
build_context: . build_context: .
cache_image: gitea.waternetwork.cn/orgtjwater/tjwateragent:ci-cache cache_image: gitea.waternetwork.cn/orgtjwater/tjwateragent:ci-cache
test_target: build test_target: test
deploy_service: agent deploy_service: agent
deploy_host: 192.168.1.114 deploy_host: 192.168.1.114
secrets: secrets:
+3
View File
@@ -1,7 +1,10 @@
node_modules/ node_modules/
__pycache__/
.opencode/node_modules/ .opencode/node_modules/
.local.env .local.env
.vscode .vscode
docker-compose.yml docker-compose.yml
data/ data/
logs/ logs/
AGENT_HARNESS_REPORT.md
HARNESS_INTRODUCTION.md
+83 -28
View File
@@ -1,34 +1,89 @@
--- ---
description: TJWater Agent,用于供水网络分析和操作员工作流 description: TJWater Agent,用于供水网络分析和操作员工作流
mode: primary mode: primary
model: deepseek/deepseek-v4-pro model: deepseek/deepseek-v4-flash
temperature: 0.2
--- ---
您是运行在 opencode 上的默认 TJWater Agent,运用水力相关知识,使用简体中文回复用户的问题 你是 TJWater 供水管网分析 Agent,运用水力专业知识,回复用户时使用简体中文,内容要求简洁准确
按照以下规则操作: ## 回复要求
1. 使用 `.opencode/skills/tjwater-skills-root-index` 作为 TJWater 技能树,仅在任务需要该领域知识时加载特定技能。对分析类问题,优先检查 `workflow` 域下是否已有固定工作流(例如 `bottleneck-analysis`);只有在 workflow 不存在、信息不足或需要补充原子能力时,才继续查询其他 API / action skills。 - 工具执行期间不输出过程说明,全部完成后只回复最终结果
2. 当您需要后端数据用于推理、总结、诊断或分析时,优先使用 `dynamic_http_call` - 直接给出结论、关键数据和可执行建议,默认仅展示最重要的 Top 5;数据不足或任务失败时简要说明影响和下一步
3. 当用户主要需要 UI 操作或可视化时,优先使用前端工具(`locate_features``view_history``view_scada``show_chart`)。 - 多步骤或预计超过 30 秒的任务,开始时使用 `todowrite` 给用户展示计划,并在每个里程碑更新状态;简单问答不创建计划
4. 仅将前端工具视为显示/交互工具,不要假设它们返回数据。
5. 保持回复准确、简洁,对供水网络用户在操作上有用。 ## 工作流生命周期
6. 尊重用户授权和项目隔离,工具调用失败或无可用数据时,切勿编造后端结果。
7. 每次调用任意工具时,必须在工具参数 `reason` 字段中填写本次调用理由,理由需具体且与当前用户问题直接相关。 Skills 树是**动态生长的**——工作流不是预置的,而是从实际任务中沉淀出来的:
8. 每次按需加载技能(skills)前,先明确说明加载理由,并只加载与当前任务直接相关的最小技能集合。默认遵循 **workflow-first**:先查固定工作流 skill,再按需回落到原子 API skills。 ```
9.`dynamic_http_call` 返回 `result_mode = referenced``result_ref` 时,说明当前只拿到了预览;如果后续推理仍需要完整结果,必须调用 `fetch_result_ref` 回读,不能把 preview 当成完整数据。 初次遇到问题 → tjwater_cli + Python 脚本拼装 → 验证有效 →
10.`render_ref``result_ref` 或其他引用型结果,默认只使用 preview、摘要、局部字段,或直接把引用传给前端工具;如果引用仅用于渲染/展示(例如 `render_junctions`),直接传引用,不要先读取完整内容再重组。 → 立即调用 skill_manager 保存到 skills/workflow/<name>/
11. 对任何可能很大的引用文件、结果文件或普通大文件,禁止完整读取;优先使用预览、分页、截断、按字段读取、按片段读取或采样读取。只有在没有其他办法且当前推理确实必须依赖完整内容时,才允许读取完整内容,并先明确说明必要性。 → 下次遇到同类问题直接加载该 skill,按既定步骤执行
12. 不得通过 sub-agent、并行代理或任何间接方式,去读取引用文件或大文件的完整内容;主 agent 与其调用链中的其他代理都必须遵守同样限制。 ```
13. 当且仅当出现**长期有效且高价值**的信号时,才允许调用在线学习工具:
- `memory_manager`:用户明确长期偏好/约束,或当前项目/环境的稳定事实 ## 任务执行决策
- `skill_manager`:已经被证明有效且可复用的 workflow / 方法模式;由您自己判断应写入 `.opencode/skills` 树中的哪个 skill 位置
14. 不要把一次性问题、临时上下文、未经验证的猜测写入任何学习工具。 收到用户请求时,按以下顺序决策:
15. 严禁把 token、password、secret、API key、system prompt、隐私数据写入 `memory_manager``skill_manager`
16. 如果内容只是一次性案例、临时纠错或局部证据,当前不要持久化。 1. **查已有工作流** — 检查 `skills/workflow/` 下是否存在匹配的 SKILL.md,有则加载并按步骤执行
17. 只有在 workflow 经过验证、足够稳定、可被未来同类任务复用时,才调用 `skill_manager`;并优先写入最贴近现有 skill 树语义的位置,中低置信度内容不要落库。 2. **历史参考** — 用 `session_search` 检索历史相似案例,避免重复试错
18. 在以下任一情况出现时,主动进行一次轻量复盘:连续多轮对话后、完成复杂多工具任务后、用户明确纠正你后、发现了稳定可复用 workflow 后。复盘的目标是判断是否需要沉淀 memory 或 skill,而不是向用户重复总结。 3. **从零拼装** — 无匹配工作流时,自行组合 `tjwater_cli` 命令 + Python 脚本完成
19. 长期知识严格分流:`memory_manager` 仅保存用户长期偏好与稳定 workspace 事实;`skill_manager` 仅保存可复用方法;一次性案例、会话过程与临时结论应优先保留在 session history,需要时使用 `session_search` 检索,不要误写入 memory 或 skill。 4. **完成后复盘** — 判断当前流程是否稳定、可复用,决定是否沉淀为 workflow
20. 写入 `memory_manager` 时,将内容写成简短陈述事实,不要写成命令句、提醒句或流程步骤。
21. 更新 skill 时,优先补充现有 skill 的 `Learned Patterns``references/``scripts/`;可复用脚本仅允许写到当前 skill 自己的 `scripts/*.py`,不要放到 `data/` 或其他 skill 目录。 ## 工具选择
22. 当用户问题依赖过去会话中的案例、约束、决策或相似问题时,优先调用 `session_search`,避免让用户重复描述,也避免把历史案例误当成长期 memory。
| 场景 | 工具 |
|------|------|
| 获取后端数据(数据源、推理、分析) | `tjwater_cli` |
| 发现可用命令 | `tjwater_cli(command="help")` |
| 查询实时公开网页信息 | `web_search` |
| 地址/地点转经纬度 | `geocode` |
| UI 操作 / 可视化 | `locate_features``zoom_to_map``view_scada``show_chart``render_junctions``view_history``apply_layer_style` |
| 持久化渲染数据 | ①准备 { node_area_map } JSON → ②`store_render_ref` 存为受控 ref → ③`render_junctions` 渲染到前端 |
**前端工具仅做显示,不返回数据**,不要假设其返回内容。
## 执行约束
1. 每次工具调用必须在 `reason` 字段填写具体理由
2. `tjwater-cli` 输出为 JSON`schema_version: tjwater-cli/v1`),`"ok": true` 成功,失败时检查 `error.code`
3. 大结果集禁止完整读取,优先采样/截断/按字段读取
4. 避免直接用 `Read``cat` 读取结果文件,尤其是大文件;优先用 `head`/`tail`/`rg` 截断查看,或用 Python 只向 stdout 输出精简 JSON,避免大文件冲击 stdin/stdout
5. 无可用数据时不得编造结果
6. 禁止使用 `task` 子代理;当前前端无法观测和干预子代理的具体工作过程
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`
+23 -9
View File
@@ -4,7 +4,7 @@
"workspaces": { "workspaces": {
"": { "": {
"dependencies": { "dependencies": {
"@opencode-ai/plugin": "1.14.41", "@opencode-ai/plugin": "1.18.13",
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.7.2", "@types/node": "^24.7.2",
@@ -13,11 +13,23 @@
}, },
}, },
"packages": { "packages": {
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.14.41", "", { "dependencies": { "@opencode-ai/sdk": "1.14.41", "effect": "4.0.0-beta.59", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.2.2", "@opentui/solid": ">=0.2.2" }, "optionalPeers": ["@opentui/core", "@opentui/solid"] }, "sha512-Q/QdDKSfHyYX+Xqd79o4XgyZKqF8h5qgqgfmOQbKVLhbduc9zMYdpV2yvWT6gaJPrpOftpka/kpr56PCqzetYQ=="], "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.14.41", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-RYb2dCUv0TWIvBNnnO6ANbAPYri6rKuWizSoVFw/Pw+SCDj9ASHM5gAZ+jkskp8gYMfLLHe/Fpkun/9mr8m0IQ=="], "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="],
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="],
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="],
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.18.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "1.18.13", "effect": "4.0.0-beta.83", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.4.5", "@opentui/keymap": ">=0.4.5", "@opentui/solid": ">=0.4.5" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-2H9YT80M1PYElpG+lmd/9kGqsNouiJIBCUhLblmgFwoSrB4wyahgkCS6NcFQR/AYXNH4I1Yd3lmQcVaEPu1qNg=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.13", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-JY9etiVcu1G/pZjaH2vjK/b8z54ujxaWCD1GziO4ADUhRM6m6zm2332bPGcxEfA6TwweiJfNlK6wVZQ0f/X4KQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -27,21 +39,23 @@
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"effect": ["effect@4.0.0-beta.59", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA=="], "effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="],
"fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], "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=="], "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
"ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "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=="], "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
"msgpackr": ["msgpackr@1.11.12", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg=="], "msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="],
"msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
"multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
@@ -61,7 +75,7 @@
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"uuid": ["uuid@13.0.2", "", { "bin": "dist-node/bin/uuid" }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="], "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+1 -1
View File
@@ -4,7 +4,7 @@
"typecheck": "tsc --noEmit -p tsconfig.json" "typecheck": "tsc --noEmit -p tsconfig.json"
}, },
"dependencies": { "dependencies": {
"@opencode-ai/plugin": "1.14.41" "@opencode-ai/plugin": "1.18.13"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.7.2", "@types/node": "^24.7.2",
-34
View File
@@ -1,34 +0,0 @@
---
name: tjwater-skills-root-index
description: TJWater Skills 分层索引(Domain -> Scenario -> Action)。
version: 1.2.0
---
# TJWater Skills
## 简介
按“领域 (Domain) -> 场景 (Scenario) -> 操作 (Action)”组织技能文档,逐层进入具体能力。
## 子模块索引 (渐进式引导)
- **analytics**: 见 `./analytics/SKILL.md`
- **business**: 见 `./business/SKILL.md`
- **data**: 见 `./data/SKILL.md`
- **platform**: 见 `./platform/SKILL.md`
- **workflow**: 见 `./workflow/SKILL.md`
## 加载策略
- 先按用户问题判断最可能的 Domain,再进入最小必要的 Scenario / Action。
- 对分析、诊断、建议类问题,优先检查 `workflow/` 下是否已有固定工作流 skill;若存在,可先按 workflow 执行,再回补所需原子 skills。
- 如果当前节点已经足以指导工具选择,不继续下钻到更多 skill。
- 如果 workflow 已覆盖主要步骤,则不要先从大量 API skills 开始拼装流程;仅在 workflow 缺失、步骤不全或需要额外原子能力时,才继续下钻。
- 优先更新已有 skill,而不是为一次性问题新增新的 skill 目录。
- learned pattern 应写成可复用的方法或坑点,不应写成某次会话的流水账。
- 某个 workflow 反复验证过的私有辅助脚本,应放在该 skill 目录下的 `scripts/*.py`,并随 skill 一起维护;不要写入 `data/`
## 参考
- 示例:`./examples.md`
- 运行手册:`./runbook.md`
-15
View File
@@ -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,107 +0,0 @@
---
name: tjwater-action-analytics-scada-operations-scada
description: analytics/scada-operations 下 scada 操作技能。
version: 3.0.0
---
# scada Action Skill
## 简介
负责 `analytics/scada-operations` 场景下 `scada` 的具体接口调用,分为**设备配置(静态元数据)**、**时序监测数据(TimescaleDB**、**实时模拟数据**、**方案数据**和**复合查询**五类。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
### SCADA 设备配置(静态元数据)
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/getscadadeviceschema/` | 获取SCADA设备架构 | network (query) | - |
| GET | `/api/v1/getscadadevice/` | 获取SCADA设备 | network (query), id (query) | - |
| GET | `/api/v1/getallscadadeviceids/` | 获取所有SCADA设备ID | network (query) | - |
| GET | `/api/v1/getallscadadevices/` | 获取所有SCADA设备 | network (query) | - |
| POST | `/api/v1/addscadadevice/` | 添加SCADA设备 | network (query) | - |
| POST | `/api/v1/setscadadevice/` | 更新SCADA设备 | network (query) | - |
| POST | `/api/v1/deletescadadevice/` | 删除SCADA设备 | network (query) | - |
| POST | `/api/v1/cleanscadadevice/` | 清空SCADA设备表 | network (query) | - |
| GET | `/api/v1/getscadadevicedataschema/` | 获取SCADA设备数据架构 | network (query) | - |
| GET | `/api/v1/getscadadevicedata/` | 获取SCADA设备数据 | network (query), device_id (query) | - |
| POST | `/api/v1/addscadadevicedata/` | 添加SCADA设备数据 | network (query) | - |
| POST | `/api/v1/setscadadevicedata/` | 更新SCADA设备数据 | network (query) | - |
| POST | `/api/v1/deletescadadevicedata/` | 删除SCADA设备数据 | network (query) | - |
| POST | `/api/v1/cleanscadadevicedata/` | 清空SCADA设备数据表 | network (query) | - |
| GET | `/api/v1/getscadaelementschema/` | 获取SCADA元素架构 | network (query) | - |
| GET | `/api/v1/getscadaelement/` | 获取单个SCADA元素映射 | network (query), id (query) | - |
| GET | `/api/v1/getscadaelements/` | 获取所有SCADA元素映射 | network (query) | - |
| POST | `/api/v1/addscadaelement/` | 添加SCADA元素映射 | network (query) | - |
| POST | `/api/v1/setscadaelement/` | 更新SCADA元素映射 | network (query) | - |
| POST | `/api/v1/deletescadaelement/` | 删除SCADA元素映射 | network (query) | - |
| POST | `/api/v1/cleanscadaelement/` | 清空SCADA元素映射表 | network (query) | - |
| GET | `/api/v1/getscadainfoschema/` | 获取SCADA信息架构 | network (query) | - |
| GET | `/api/v1/getscadainfo/` | 获取SCADA信息 | network (query), id (query) | - |
| GET | `/api/v1/getallscadainfo/` | 获取所有SCADA信息 | network (query) | - |
| GET | `/api/v1/getscadaproperties/` | 获取SCADA属性 | network (query), scada (query) | - |
| GET | `/api/v1/getallscadaproperties/` | 获取所有SCADA属性 | network (query) | - |
### SCADA 时序监测数据(TimescaleDB
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/scada/batch` | 批量插入SCADA监测数据 | data (body) | - |
| GET | `/api/v1/scada/by-ids-time-range` | 按设备ID和时间范围查询SCADA数据 | start_time (query), end_time (query), device_ids (query) | - |
| GET | `/api/v1/scada/by-ids-field-time-range` | 按设备ID、字段和时间范围查询SCADA数据 | start_time (query), end_time (query), field (query), device_ids (query) | - |
| PATCH | `/api/v1/scada/{device_id}/field` | 更新SCADA设备字段 | device_id (path), time (query), field (query), value (query) | - |
| DELETE | `/api/v1/scada/by-id-time-range` | 按设备ID和时间范围删除SCADA数据 | device_id (query), start_time (query), end_time (query) | - |
### 实时模拟数据(TimescaleDB - Realtime
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/realtime/links/batch` | 批量插入实时管道数据 | data (body) | - |
| GET | `/api/v1/realtime/links` | 查询实时管道数据 | start_time (query), end_time (query) | link_ids (query) |
| DELETE | `/api/v1/realtime/links` | 删除实时管道数据 | start_time (query), end_time (query) | - |
| PATCH | `/api/v1/realtime/links/{link_id}/field` | 更新实时管道字段 | link_id (path), time (query), field (query), value (query) | - |
| POST | `/api/v1/realtime/nodes/batch` | 批量插入实时节点数据 | data (body) | - |
| GET | `/api/v1/realtime/nodes` | 查询实时节点数据 | start_time (query), end_time (query) | node_ids (query) |
| DELETE | `/api/v1/realtime/nodes` | 删除实时节点数据 | start_time (query), end_time (query) | - |
| POST | `/api/v1/realtime/simulation/store` | 存储实时模拟结果 | data (body) | - |
| GET | `/api/v1/realtime/query/by-time-property` | 按时间和属性查询实时数据 | time (query), property (query) | - |
| GET | `/api/v1/realtime/query/by-id-time` | 按ID和时间查询实时模拟数据 | element_id (query), time (query) | - |
### 方案模拟数据(TimescaleDB - Scheme
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/scheme/links/batch` | 批量插入方案管道数据 | data (body) | - |
| GET | `/api/v1/scheme/links` | 查询方案管道数据 | scheme_type (query) | link_ids (query) |
| GET | `/api/v1/scheme/links/{link_id}/field` | 查询方案管道字段数据 | link_id (path), scheme_type (query), field (query) | - |
| PATCH | `/api/v1/scheme/links/{link_id}/field` | 更新方案管道字段 | link_id (path), scheme_type (query), field (query), value (query) | - |
| DELETE | `/api/v1/scheme/links` | 删除方案管道数据 | scheme_type (query) | - |
| POST | `/api/v1/scheme/nodes/batch` | 批量插入方案节点数据 | data (body) | - |
| GET | `/api/v1/scheme/nodes/{node_id}/field` | 查询方案节点字段数据 | node_id (path), scheme_type (query), field (query) | - |
| PATCH | `/api/v1/scheme/nodes/{node_id}/field` | 更新方案节点字段 | node_id (path), scheme_type (query), field (query), value (query) | - |
| DELETE | `/api/v1/scheme/nodes` | 删除方案节点数据 | scheme_type (query) | - |
| POST | `/api/v1/scheme/simulation/store` | 存储方案模拟结果 | scheme_type (query), data (body) | - |
| GET | `/api/v1/scheme/query/by-id-time` | 按ID和时间查询方案模拟数据 | element_id (query), scheme_type (query), time (query) | - |
### 复合查询(TimescaleDB - Composite
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/composite/scada-simulation` | 获取SCADA关联的模拟数据 | network (query), start_time (query) | end_time (query) |
| GET | `/api/v1/composite/element-simulation` | 获取管网元素的模拟数据 | network (query), element_id (query), start_time (query) | end_time (query) |
| GET | `/api/v1/composite/element-scada` | 获取管网元素关联的SCADA监测数据 | element_id (query), start_time (query) | end_time (query) |
| POST | `/api/v1/composite/clean-scada` | 清洗SCADA监测数据 | data (body) | - |
| GET | `/api/v1/composite/pipeline-health-prediction` | 预测管道健康状况 | network (query), time (query) | - |
- 覆盖方法:`DELETE, GET, PATCH, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /scada/by-ids-time-range` | 查询多个设备在指定时间范围内的所有监测字段数据,device_ids 为逗号分隔的ID字符串 |
| `GET /scada/by-ids-field-time-range` | 查询多个设备在指定时间范围内的特定字段数据(如只查压力或只查流量) |
| `POST /realtime/simulation/store` | 将水力模拟结果以实时数据形式存入TimescaleDB,供前端实时展示 |
| `GET /realtime/query/by-time-property` | 按特定时间点和属性名查询管网实时模拟结果 |
| `GET /composite/scada-simulation` | 同时返回指定管网的SCADA监测数据和对应的水力模拟数据,便于对比分析 |
| `GET /composite/element-scada` | 查询特定管网元素(管道或节点)关联的SCADA监测时序数据 |
| `GET /composite/pipeline-health-prediction` | 基于历史SCADA数据和模型预测管道健康状态 |
| `POST /composite/clean-scada` | 对指定设备的SCADA原始数据进行清洗处理(去异常值等),支持传 'all' 清洗所有设备 |
@@ -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,43 +0,0 @@
---
name: tjwater-action-analytics-simulation-analysis-burst-detection
description: analytics/simulation-analysis 下 burst-detection 操作技能。
version: 3.0.0
---
# burst-detection Action Skill
## 简介
负责 `analytics/simulation-analysis` 场景下 `burst-detection` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/burst-detection/detect/` | 执行爆管检测 | data (body) | - |
| GET | `/api/v1/burst-detection/schemes/` | 查询爆管检测方案列表 | network (query) | query_date (query) |
| GET | `/api/v1/burst-detection/schemes/{scheme_name}` | 获取爆管检测方案详情 | network (query), scheme_name (path) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `POST /detect/` | 基于压力观测数据执行爆管检测分析。使用异常检测算法(隔离森林 IsolationForest)识别压力时间序列中的异常,判定为潜在爆管事件。请求体支持列式字典、逐时刻对象数组、二维数组三种格式的压力数据,可指定数据来源(monitoring 监测 / simulation 模拟)。 |
| `GET /schemes/` | 获取指定管网的所有爆管检测方案列表,可通过 query_date 按日期筛选。 |
| `GET /schemes/{scheme_name}` | 获取指定名称的爆管检测方案详细配置信息,包含传感器节点、算法参数等。 |
## 请求体关键字段(POST /detect/
| 字段 | 类型 | 说明 |
|---|---|---|
| `network` | str | 管网名称(数据库名) |
| `observed_pressure_data` | dict/list/null | 压力观测数据,支持列式字典 `{sensor_id: [values]}` 或逐行数组 |
| `points_per_day` | int | 每天数据点数,默认1440 |
| `mu` | int | 异常值检测参数,默认100 |
| `iforest_params` | dict/null | 隔离森林算法参数,可选 |
| `scada_start` / `scada_end` | datetime/null | 从SCADA数据库查询的时间范围 |
| `sensor_nodes` | list/null | 指定传感器节点,null为全部 |
| `data_source` | str | 数据来源:`monitoring`(监测)或 `simulation`(模拟),默认monitoring |
@@ -1,47 +0,0 @@
---
name: tjwater-action-analytics-simulation-analysis-burst-location
description: analytics/simulation-analysis 下 burst-location 操作技能。
version: 3.0.0
---
# burst-location Action Skill
## 简介
负责 `analytics/simulation-analysis` 场景下 `burst-location` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/burst-location/locate/` | 执行爆管定位 | data (body) | - |
| GET | `/api/v1/burst-location/schemes/` | 查询爆管定位方案列表 | network (query) | query_date (query) |
| GET | `/api/v1/burst-location/schemes/{scheme_name}` | 获取爆管定位方案详情 | network (query), scheme_name (path) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `POST /locate/` | 基于压力和流量SCADA数据定位管网中的爆管位置。通过对比爆管时与正常状态下的压力/流量差异,计算最可能的爆管节点。 |
| `GET /schemes/` | 获取指定管网的所有爆管定位方案列表,可通过 query_date 按日期筛选。 |
| `GET /schemes/{scheme_name}` | 获取指定名称的爆管定位方案详细配置,包含传感器布置、阈值参数等。 |
## 请求体关键字段(POST /locate/
| 字段 | 类型 | 说明 |
|---|---|---|
| `network` | str | 管网名称(数据库名) |
| `data_source` | str | 数据来源:`monitoring`(监测)或 `simulation`(模拟),默认monitoring |
| `pressure_scada_ids` | list/null | 压力SCADA传感器ID列表 |
| `burst_pressure` | dict/list/null | 爆管时的压力数据 |
| `normal_pressure` | dict/list/null | 正常时的压力数据 |
| `burst_leakage` | float | 爆管时的漏水量(必填) |
| `flow_scada_ids` | list/null | 流量SCADA传感器ID列表 |
| `burst_flow` / `normal_flow` | dict/list/null | 爆管/正常时的流量数据 |
| `min_dpressure` | float | 最小压力差(bar),默认2.0 |
| `basic_pressure` | float | 基准压力(bar),默认10.0 |
| `scada_burst_start` / `scada_burst_end` | datetime/null | 从SCADA数据库查询的爆管时间范围 |
| `use_scada_flow` | bool | 是否使用SCADA流量数据,默认false |
@@ -1,46 +0,0 @@
---
name: tjwater-action-analytics-simulation-analysis-leakage
description: analytics/simulation-analysis 下 leakage 操作技能。
version: 3.0.0
---
# leakage Action Skill
## 简介
负责 `analytics/simulation-analysis` 场景下 `leakage` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/leakage/identify/` | 执行漏损识别 | data (body) | - |
| GET | `/api/v1/leakage/schemes/` | 查询漏损识别方案列表 | network (query) | query_date (query) |
| GET | `/api/v1/leakage/schemes/{scheme_name}` | 获取漏损识别方案详情 | network (query), scheme_name (path) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `POST /identify/` | 基于压力观测数据和遗传算法识别管网中的漏损位置和大小。通过对比模型计算与实测压力数据,迭代优化找到最匹配的漏损节点和漏水量。 |
| `GET /schemes/` | 获取指定管网的所有漏损识别方案列表,可通过 query_date 按日期筛选。 |
| `GET /schemes/{scheme_name}` | 获取指定名称的漏损识别方案详细配置,包含传感器节点、算法参数等。 |
## 请求体关键字段(POST /identify/
| 字段 | 类型 | 说明 |
|---|---|---|
| `network` | str | 管网名称(数据库名) |
| `observed_pressure_data` | str/dict/list/null | 观测压力数据 |
| `start_time` | float | 起始时间(小时),默认0 |
| `duration` | float | 持续时间(小时),默认24 |
| `timestep` | float | 时间步长(分钟),默认5 |
| `q_sum` | float | 总流量(m³/s),默认0.2 |
| `pop_size` | int | 遗传算法种群大小,默认50 |
| `max_gen` | int | 遗传算法最大代数,默认100 |
| `n_workers` | int | 并行工作线程数,默认CPU数-1(最大4) |
| `scada_start` / `scada_end` | datetime/null | 从SCADA数据库查询的时间范围 |
| `sensor_nodes` | list/null | 传感器节点列表,null为全部 |
@@ -1,34 +0,0 @@
---
name: tjwater-action-analytics-simulation-analysis-risk
description: analytics/simulation-analysis 下 risk 操作技能。
version: 3.0.0
---
# risk Action Skill
## 简介
负责 `analytics/simulation-analysis` 场景下 `risk` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/getpiperiskprobabilitynow/` | 获取管道当前风险概率 | network (query), pipe_id (query) | - |
| GET | `/api/v1/getpiperiskprobability/` | 获取管道风险概率历史 | network (query), pipe_id (query) | - |
| GET | `/api/v1/getpipesriskprobability/` | 批量获取多条管道风险概率 | network (query), pipe_ids (query) | - |
| GET | `/api/v1/getnetworkpiperiskprobabilitynow/` | 获取整个网络的管道风险概率 | network (query) | - |
| GET | `/api/v1/getpiperiskprobabilitygeometries/` | 获取管道风险几何信息 | network (query) | - |
- 覆盖方法:`GET`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getpiperiskprobabilitynow/` | 查询指定管道在当前时刻的风险概率值 |
| `GET /getpiperiskprobability/` | 查询指定管道的历史风险概率时间序列数据 |
| `GET /getpipesriskprobability/` | 批量查询多条管道的风险概率,pipe_ids为逗号分隔的ID字符串(如 `pipe1,pipe2,pipe3` |
| `GET /getnetworkpiperiskprobabilitynow/` | 查询整个管网中所有管道的当前风险概率,返回列表 |
| `GET /getpiperiskprobabilitygeometries/` | 查询管网中管道的地理位置和风险相关几何数据,适合地图可视化 |
@@ -1,74 +0,0 @@
---
name: tjwater-action-analytics-simulation-analysis-simulation
description: analytics/simulation-analysis 下 simulation 操作技能。
version: 3.0.0
---
# simulation Action Skill
## 简介
负责 `analytics/simulation-analysis` 场景下 `simulation` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/runproject/` | 运行项目模拟 | network (query) | - |
| GET | `/api/v1/runprojectreturndict/` | 运行项目模拟(返回字典) | network (query) | - |
| GET | `/api/v1/runinp/` | 运行INP文件 | network (query) | - |
| GET | `/api/v1/dumpoutput/` | 导出模拟输出 | output (query) | - |
| GET | `/api/v1/burstanalysis/` | 爆管分析(基础) | network (query), pipe_id (query), start_time (query), end_time (query), burst_flow (query) | - |
| GET | `/api/v1/burst_analysis/` | 爆管分析(高级) | network (query), modify_pattern_start_time (query), burst_ID (query), burst_size (query), modify_total_duration (query), scheme_name (query) | - |
| GET | `/api/v1/valvecloseanalysis/` | 阀门关闭分析(基础) | network (query), valve_id (query), start_time (query), end_time (query) | - |
| GET | `/api/v1/valve_close_analysis/` | 阀门关闭分析(高级) | network (query), start_time (query), valves (query) | duration (query) |
| GET | `/api/v1/valve_isolation_analysis/` | 阀门隔离分析 | network (query), accident_element (query) | disabled_valves (query) |
| GET | `/api/v1/flushinganalysis/` | 冲洗分析(基础) | network (query), pipe_id (query), start_time (query), duration (query), flow (query) | - |
| GET | `/api/v1/flushing_analysis/` | 冲洗分析(高级) | network (query), start_time (query), valves (query), valves_k (query), drainage_node_ID (query) | flush_flow (query), duration (query), scheme_name (query) |
| GET | `/api/v1/contaminant_simulation/` | 污染物模拟 | network (query), start_time (query), source (query), concentration (query), duration (query) | scheme_name (query), pattern (query) |
| GET | `/api/v1/ageanalysis/` | 水龄分析(基础) | network (query) | - |
| GET | `/api/v1/age_analysis/` | 水龄分析(高级) | network (query), start_time (query), end_time (query), duration (query) | - |
| GET | `/api/v1/pressureregulation/` | 压力调节(基础) | network (query), target_node (query), target_pressure (query) | - |
| POST | `/api/v1/pressure_regulation/` | 压力调节(高级) | data (body) | - |
| GET | `/api/v1/projectmanagement/` | 项目管理(基础) | network (query) | - |
| POST | `/api/v1/project_management/` | 项目管理(高级) | data (body) | - |
| POST | `/api/v1/scheduling_analysis/` | 排程分析 | data (body) | - |
| POST | `/api/v1/daily_scheduling_analysis/` | 日排程分析 | data (body) | - |
| POST | `/api/v1/network_project/` | 导入网络项目 | file (file) | - |
| GET | `/api/v1/networkupdate/` | 管网更新(基础) | network (query) | - |
| POST | `/api/v1/network_update/` | 管网更新(高级) | file (file) | - |
| POST | `/api/v1/pump_failure/` | 泵故障管理 | data (body) | - |
| GET | `/api/v1/pressuresensorplacementsensitivity/` | 压力传感器放置-灵敏度分析(基础) | name (query), scheme_name (query), sensor_number (query), min_diameter (query), username (query) | - |
| POST | `/api/v1/pressure_sensor_placement_sensitivity/` | 压力传感器放置-灵敏度分析(高级) | data (body) | - |
| GET | `/api/v1/pressuresensorplacementkmeans/` | 压力传感器放置-KMeans聚类分析(基础) | name (query), scheme_name (query), sensor_number (query), min_diameter (query), username (query) | - |
| POST | `/api/v1/pressure_sensor_placement_kmeans/` | 压力传感器放置-KMeans聚类分析(高级) | data (body) | - |
| POST | `/api/v1/sensorplacementscheme/create` | 传感器放置方案创建 | network (query), scheme_name (query), sensor_type (query), method (query), sensor_count (query), user_name (query) | min_diameter (query) |
| POST | `/api/v1/runsimulationmanuallybydate/` | 手动运行日期指定模拟 | data (body) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /runproject/` | 运行标准水力模拟,返回纯文本格式的模拟报告 |
| `GET /runprojectreturndict/` | 运行标准水力模拟,返回JSON字典(含节点/管段结果数据),适合程序处理;结果可达30MB+ |
| `GET /runinp/` | 运行指定INP文件(文件放在inp文件夹中,参数为不含扩展名的文件名)进行水力模拟 |
| `GET /dumpoutput/` | 导出指定绝对路径的模拟输出文件内容 |
| `GET /burstanalysis/` | 基础爆管分析:对指定管道指定时间范围内的爆管事件进行分析,评估对压力/流量的影响 |
| `GET /burst_analysis/` | 高级爆管分析:支持在指定时间点修改泵控制模式和阀门开度,分析干预措施对爆管影响的作用;支持固定泵和变速泵独立控制 |
| `GET /valve_close_analysis/` | 高级阀门关闭分析:支持同时关闭多个阀门,指定持续时间,返回纯文本格式结果 |
| `GET /valve_isolation_analysis/` | 阀门隔离分析:分析突发事件时通过关闭指定阀门进行隔离,确定必须关闭阀门、可选关闭阀门及隔离可行性 |
| `GET /flushing_analysis/` | 高级冲洗分析:支持同时开启多个阀门冲洗,指定排污节点,设置固定冲洗流量,返回纯文本结果 |
| `GET /contaminant_simulation/` | 污染物模拟:评估污染源对管网的影响范围和浓度分布,支持指定污染位置、浓度和扩散模式 |
| `GET /age_analysis/` | 高级水龄分析:在指定时间点分析水体停留时间,支持自定义模拟持续时间,返回纯文本结果 |
| `POST /pressure_regulation/` | 高级压力调节:通过JSON体提供详细控制参数(固定泵/变速泵独立控制、水箱初始水位等)进行压力优化 |
| `POST /project_management/` | 高级项目管理:通过JSON体提供详细参数(泵控制策略、水箱水位、区域需水量控制)进行管网管理 |
| `POST /scheduling_analysis/` | 排程分析:优化泵运行时间和出水流量,平衡水厂出水、水箱进出水,满足用户需求 |
| `POST /daily_scheduling_analysis/` | 日排程分析:优化水库、水厂、水箱和用户需求协调,制定合理的每日排程方案 |
| `POST /pump_failure/` | 泵故障管理:记录故障发生时间和受影响的泵列表,更新泵状态日志 |
| `POST /pressure_sensor_placement_sensitivity/` | 高级传感器放置(灵敏度法):通过JSON体提供详细参数,基于灵敏度矩阵确定最优放置位置 |
| `POST /pressure_sensor_placement_kmeans/` | 高级传感器放置(KMeans法):通过JSON体提供详细参数,基于聚类算法确定最优放置位置 |
| `POST /sensorplacementscheme/create` | 创建传感器放置方案:支持 sensitivity 和 kmeans 两种算法,自动计算最优传感器位置并存储方案 |
| `POST /runsimulationmanuallybydate/` | 按日期手动运行模拟:根据指定日期、开始时间和持续时间查询管网参数并执行水力模拟 |
-17
View File
@@ -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,36 +0,0 @@
---
name: tjwater-action-business-component-config-controls
description: business/component-config 下 controls 操作技能。
version: 3.0.0
---
# controls Action Skill
## 简介
负责 `business/component-config` 场景下 `controls` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/getcontrolproperties/` | 获取控制属性 | network (query) | - |
| GET | `/api/v1/getcontrolschema/` | 获取控制架构 | network (query) | - |
| GET | `/api/v1/getruleproperties/` | 获取规则属性 | network (query) | - |
| GET | `/api/v1/getruleschema/` | 获取规则架构 | network (query) | - |
| POST | `/api/v1/setcontrolproperties/` | 设置控制属性 | network (query) | - |
| POST | `/api/v1/setruleproperties/` | 设置规则属性 | network (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getcontrolschema/` | 返回简单控制(Control)数据模型的字段定义 |
| `GET /getcontrolproperties/` | 获取管网中所有简单控制规则的属性列表 |
| `GET /getruleschema/` | 返回规则控制(Rule)数据模型的字段定义 |
| `GET /getruleproperties/` | 获取管网中所有基于规则的复杂控制条件列表 |
| `POST /setcontrolproperties/` | 设置/更新简单控制规则的属性 |
| `POST /setruleproperties/` | 设置/更新规则控制的属性 |
@@ -1,37 +0,0 @@
---
name: tjwater-action-business-component-config-curves
description: business/component-config 下 curves 操作技能。
version: 3.0.0
---
# curves Action Skill
## 简介
负责 `business/component-config` 场景下 `curves` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/addcurve/` | 添加曲线 | network (query), curve (query) | - |
| POST | `/api/v1/deletecurve/` | 删除曲线 | network (query), curve (query) | - |
| GET | `/api/v1/getcurveproperties/` | 获取曲线属性 | network (query), curve (query) | - |
| GET | `/api/v1/getcurves/` | 获取所有曲线 | network (query) | - |
| GET | `/api/v1/getcurveschema` | 获取曲线架构 | network (query) | - |
| GET | `/api/v1/iscurve/` | 检查曲线存在性 | network (query), curve (query) | - |
| POST | `/api/v1/setcurveproperties/` | 设置曲线属性 | network (query), curve (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getcurveschema` | 返回曲线(Curve)数据模型的字段定义(水泵特性曲线、效率曲线等) |
| `GET /getcurves/` | 获取管网中所有曲线的ID列表 |
| `GET /getcurveproperties/` | 查询指定曲线的详细属性(类型、控制点数据等) |
| `POST /addcurve/` | 向管网添加一条新曲线 |
| `POST /deletecurve/` | 从管网删除指定曲线 |
| `POST /setcurveproperties/` | 设置/更新曲线的属性(控制点坐标等) |
@@ -1,43 +0,0 @@
---
name: tjwater-action-business-component-config-options
description: business/component-config 下 options 操作技能。
version: 3.0.0
---
# options Action Skill
## 简介
负责 `business/component-config` 场景下 `options` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/getenergyproperties/` | 获取能耗选项属性 | network (query) | - |
| GET | `/api/v1/getenergyschema/` | 获取能耗选项架构 | network (query) | - |
| GET | `/api/v1/getoptionproperties/` | 获取选项属性 | network (query) | - |
| GET | `/api/v1/getoptionschema/` | 获取选项架构 | network (query) | - |
| GET | `/api/v1/getpumpenergyproperties/` | 获取泵能耗属性 | - | - |
| GET | `/api/v1/getpumpenergyschema/` | 获取泵能耗选项架构 | network (query) | - |
| GET | `/api/v1/gettimeproperties/` | 获取时间选项属性 | network (query) | - |
| GET | `/api/v1/gettimeschema` | 获取时间选项架构 | network (query) | - |
| POST | `/api/v1/setenergyproperties/` | 设置能耗选项属性 | network (query) | - |
| POST | `/api/v1/setoptionproperties/` | 设置选项属性 | network (query) | - |
| GET | `/api/v1/setpumpenergyproperties/` | 设置泵能耗属性 | - | - |
| POST | `/api/v1/settimeproperties/` | 设置时间选项属性 | network (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getoptionschema/` | 返回模拟选项(Options)数据模型的字段定义 |
| `GET /getoptionproperties/` | 获取管网模拟参数设置(时步、精度、单位系统等) |
| `GET /getenergyschema/` | 返回能耗选项(Energy)数据模型的字段定义 |
| `GET /getenergyproperties/` | 获取全局能耗设置(电价、效率等) |
| `GET /getpumpenergyproperties/` | 获取单台水泵的能耗参数 |
| `POST /setoptionproperties/` | 设置管网模拟参数 |
| `POST /setenergyproperties/` | 设置全局能耗参数 |
@@ -1,37 +0,0 @@
---
name: tjwater-action-business-component-config-patterns
description: business/component-config 下 patterns 操作技能。
version: 3.0.0
---
# patterns Action Skill
## 简介
负责 `business/component-config` 场景下 `patterns` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/addpattern/` | 添加模式 | network (query), pattern (query) | - |
| POST | `/api/v1/deletepattern/` | 删除模式 | network (query), pattern (query) | - |
| GET | `/api/v1/getpatternproperties/` | 获取模式属性 | network (query), pattern (query) | - |
| GET | `/api/v1/getpatterns/` | 获取所有模式 | network (query) | - |
| GET | `/api/v1/getpatternschema` | 获取模式架构 | network (query) | - |
| GET | `/api/v1/ispattern/` | 检查模式存在性 | network (query), pattern (query) | - |
| POST | `/api/v1/setpatternproperties/` | 设置模式属性 | network (query), pattern (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getpatternschema` | 返回模式(Pattern)数据模型的字段定义 |
| `GET /getpatterns/` | 获取管网中所有时间模式的ID列表 |
| `GET /getpatternproperties/` | 查询指定模式的属性(时间序列乘数值等) |
| `POST /addpattern/` | 向管网添加一个新时间模式 |
| `POST /deletepattern/` | 从管网删除指定时间模式 |
| `POST /setpatternproperties/` | 设置/更新时间模式的属性(乘数序列) |
@@ -1,59 +0,0 @@
---
name: tjwater-action-business-component-config-quality
description: business/component-config 下 quality 操作技能。
version: 3.0.0
---
# quality Action Skill
## 简介
负责 `business/component-config` 场景下 `quality` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/addmixing/` | 添加混合 | network (query) | - |
| POST | `/api/v1/addsource/` | 添加水源 | network (query) | - |
| POST | `/api/v1/deletemixing/` | 删除混合 | network (query) | - |
| POST | `/api/v1/deletesource/` | 删除水源 | network (query), node (query) | - |
| GET | `/api/v1/getemitterproperties/` | 获取发射器属性 | network (query), junction (query) | - |
| GET | `/api/v1/getemitterschema` | 获取发射器架构 | network (query) | - |
| GET | `/api/v1/getmixing/` | 获取混合属性 | network (query), tank (query) | - |
| GET | `/api/v1/getmixingschema/` | 获取混合架构 | network (query) | - |
| GET | `/api/v1/getpipereaction/` | 获取管道反应属性 | network (query), pipe (query) | - |
| GET | `/api/v1/getpipereactionschema/` | 获取管道反应架构 | network (query) | - |
| GET | `/api/v1/getqualityproperties/` | 获取水质属性 | network (query), node (query) | - |
| GET | `/api/v1/getqualityschema/` | 获取水质架构 | network (query) | - |
| GET | `/api/v1/getreaction/` | 获取反应属性 | network (query) | - |
| GET | `/api/v1/getreactionschema/` | 获取反应架构 | network (query) | - |
| GET | `/api/v1/getsource/` | 获取水源属性 | network (query), node (query) | - |
| GET | `/api/v1/getsourcechema/` | 获取水源架构 | network (query) | - |
| GET | `/api/v1/gettankreaction/` | 获取水池反应属性 | network (query), tank (query) | - |
| GET | `/api/v1/gettankreactionschema/` | 获取水池反应架构 | network (query) | - |
| POST | `/api/v1/setemitterproperties/` | 设置发射器属性 | network (query), junction (query) | - |
| POST | `/api/v1/setmixing/` | 设置混合属性 | network (query) | - |
| POST | `/api/v1/setpipereaction/` | 设置管道反应属性 | network (query) | - |
| POST | `/api/v1/setqualityproperties/` | 设置水质属性 | network (query) | - |
| POST | `/api/v1/setreaction/` | 设置反应属性 | network (query) | - |
| POST | `/api/v1/setsource/` | 设置水源属性 | network (query) | - |
| POST | `/api/v1/settankreaction/` | 设置水池反应属性 | network (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getemitterproperties/` | 查询节点处发射器(用于模拟管漏)的属性 |
| `GET /getmixingproperties/` | 查询水箱水质混合模型属性 |
| `GET /getsourceproperties/` | 查询水质污染源的属性(位置、类型、浓度等) |
| `GET /getreactionproperties/` | 获取全局水质反应参数(管网反应系数等) |
| `GET /getwaterqualityresult/` | 查询水质模拟结果 |
| `POST /addsource/` | 向管网添加一个水质污染源 |
| `POST /deletesource/` | 删除指定水质污染源 |
| `POST /addmixing/` | 为水箱添加水质混合模型 |
| `POST /deletemixing/` | 删除水箱水质混合模型 |
| `POST /setemitter*/` | 设置发射器属性(流量系数等) |
@@ -1,45 +0,0 @@
---
name: tjwater-action-business-component-config-visuals
description: business/component-config 下 visuals 操作技能。
version: 3.0.0
---
# visuals Action Skill
## 简介
负责 `business/component-config` 场景下 `visuals` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/addlabel/` | 添加标签 | network (query) | - |
| POST | `/api/v1/addvertex/` | 添加图形元素 | network (query) | - |
| POST | `/api/v1/deletelabel/` | 删除标签 | network (query) | - |
| POST | `/api/v1/deletevertex/` | 删除图形元素 | network (query) | - |
| GET | `/api/v1/getallvertexlinks/` | 获取所有图形元素链接 | network (query) | - |
| GET | `/api/v1/getallvertices/` | 获取所有图形元素 | network (query) | - |
| GET | `/api/v1/getbackdropproperties/` | 获取背景属性 | network (query) | - |
| GET | `/api/v1/getbackdropschema/` | 获取背景架构 | network (query) | - |
| GET | `/api/v1/getlabelproperties/` | 获取标签属性 | network (query), x (query), y (query) | - |
| GET | `/api/v1/getlabelschema/` | 获取标签架构 | network (query) | - |
| GET | `/api/v1/getvertexproperties/` | 获取图形元素属性 | network (query), link (query) | - |
| GET | `/api/v1/getvertexschema/` | 获取图形元素架构 | network (query) | - |
| POST | `/api/v1/setbackdropproperties/` | 设置背景属性 | network (query) | - |
| POST | `/api/v1/setlabelproperties/` | 设置标签属性 | network (query) | - |
| POST | `/api/v1/setvertexproperties/` | 设置图形元素属性 | network (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getallvertexlinks/` | 获取所有管线的折点列表(用于地图还原管线真实走向) |
| `GET /getvertexlink/` | 获取单条管线的折点坐标序列 |
| `POST /addvertex/` | 为管线添加一个折点(改变管线显示路径) |
| `POST /deletevertex/` | 删除管线上的指定折点 |
| `POST /addlabel/` | 在地图上添加文字标注 |
| `POST /deletelabel/` | 删除地图文字标注 |
@@ -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,32 +0,0 @@
---
name: tjwater-action-business-identity-access-auth
description: business/identity-access 下 auth 操作技能。
version: 3.0.0
---
# auth Action Skill
## 简介
负责 `business/identity-access` 场景下 `auth` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/auth/login` | 用户登录 | form_data (body) | - |
| POST | `/api/v1/auth/login/simple` | 简化版登录 | username (query), password (query) | - |
| GET | `/api/v1/auth/me` | 获取当前用户信息 | - | - |
| POST | `/api/v1/auth/refresh` | 刷新AccessToken | refresh_token (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `POST /login` | OAuth2标准格式登录,提交form-datausername+password),返回JWT Access Token和Refresh Token |
| `POST /login/simple` | 简化版登录,直接通过query参数传递username和password,保持向后兼容 |
| `GET /me` | 返回当前已登录用户的详细信息(需携带Access Token |
| `POST /refresh` | 使用Refresh Token换取新的Access Token,延续会话 |
@@ -1,36 +0,0 @@
---
name: tjwater-action-business-identity-access-user-management
description: business/identity-access 下 user-management 操作技能。
version: 3.0.0
---
# user-management Action Skill
## 简介
负责 `business/identity-access` 场景下 `user-management` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/users/` | 列出所有用户 | - | skip (query), limit (query) |
| DELETE | `/api/v1/users/{user_id}` | 删除用户 | user_id (path) | - |
| GET | `/api/v1/users/{user_id}` | 获取用户详情 | user_id (path) | - |
| PUT | `/api/v1/users/{user_id}` | 更新用户信息 | user_id (path) | user_update (body) |
| POST | `/api/v1/users/{user_id}/activate` | 激活用户 | user_id (path) | - |
| POST | `/api/v1/users/{user_id}/deactivate` | 停用用户 | user_id (path) | - |
- 覆盖方法:`DELETE, GET, POST, PUT`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /users/` | 列出系统中所有用户(管理员权限),支持分页(skip/limit |
| `GET /users/{user_id}` | 按用户ID查询单个用户的详细信息 |
| `PUT /users/{user_id}` | 更新指定用户的信息(邮箱、角色、密码等),请求体为 user_update 对象 |
| `DELETE /users/{user_id}` | 删除指定用户(软删除或硬删除) |
| `POST /users/{user_id}/activate` | 激活指定用户账号(管理员操作) |
| `POST /users/{user_id}/deactivate` | 停用指定用户账号(禁止登录,管理员操作) |
@@ -1,30 +0,0 @@
---
name: tjwater-action-business-identity-access-users
description: business/identity-access 下 users 操作技能。
version: 3.0.0
---
# users Action Skill
## 简介
负责 `business/identity-access` 场景下 `users` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/getallusers/` | 获取所有用户 | network (query) | - |
| GET | `/api/v1/getuser/` | 获取单个用户 | network (query), user_name (query) | - |
| GET | `/api/v1/getuserschema/` | 获取用户模式 | network (query) | - |
- 覆盖方法:`GET`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getallusers/` | 获取指定管网下的所有用户列表(旧版接口,返回管网级别用户信息) |
| `GET /getuser/` | 按用户名查询指定管网下的单个用户信息 |
| `GET /getuserschema/` | 获取用户数据模型的字段定义(Schema) |
@@ -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,36 +0,0 @@
---
name: tjwater-action-business-network-assets-demands
description: business/network-assets 下 demands 操作技能。
version: 3.0.0
---
# demands Action Skill
## 简介
负责 `business/network-assets` 场景下 `demands` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/calculatedemandtonetwork/` | 计算需水量到整网分配 | network (query), demand (query) | - |
| GET | `/api/v1/calculatedemandtonodes/` | 计算需水量到节点分配 | network (query) | - |
| GET | `/api/v1/calculatedemandtoregion/` | 计算需水量到区域分配 | network (query) | - |
| GET | `/api/v1/getdemandproperties/` | 获取需水量属性 | network (query), junction (query) | - |
| GET | `/api/v1/getdemandschema` | 获取需水量属性架构 | network (query) | - |
| POST | `/api/v1/setdemandproperties/` | 设置需水量属性 | network (query), junction (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getdemandschema` | 返回需水量(Demand)数据模型的字段定义 |
| `GET /getdemandproperties/` | 查询指定节点的需水量属性(基础需水量、模式等) |
| `POST /setdemandproperties/` | 设置节点的需水量属性 |
| `POST /calculatedemandtonodes/` | 将指定总需水量计算分配到各节点 |
| `POST /calculatedemandtoregion/` | 将指定总需水量计算分配到指定区域内的节点 |
| `POST /calculatedemandtonetwork/` | 将指定总需水量按比例分配到整个管网的所有节点 |
@@ -1,62 +0,0 @@
---
name: tjwater-action-business-network-assets-general
description: business/network-assets 下 general 操作技能。
version: 3.0.0
---
# general Action Skill
## 简介
负责 `business/network-assets` 场景下 `general` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/deletelink/` | 删除管线 | network (query), link (query) | - |
| POST | `/api/v1/deletenode/` | 删除节点 | network (query), node (query) | - |
| GET | `/api/v1/getallscadaproperties/` | 获取所有SCADA点属性 | network (query) | - |
| GET | `/api/v1/getelementproperties/` | 获取元素属性 | network (query), element (query) | - |
| GET | `/api/v1/getelementpropertieswithtype/` | 获取指定类型元素属性 | network (query), elementtype (query), element (query) | - |
| GET | `/api/v1/getelementtype/` | 获取元素类型 | network (query), element (query) | - |
| GET | `/api/v1/getelementtypevalue/` | 获取元素类型值 | network (query), element (query) | - |
| GET | `/api/v1/getlinkproperties/` | 获取管线属性 | network (query), link (query) | - |
| GET | `/api/v1/getlinks/` | 获取所有管线 | network (query) | - |
| GET | `/api/v1/getlinktype/` | 获取管线类型 | network (query), link (query) | - |
| GET | `/api/v1/getnodelinks/` | 获取节点的关联管线 | network (query), node (query) | - |
| GET | `/api/v1/getnodeproperties/` | 获取节点属性 | network (query), node (query) | - |
| GET | `/api/v1/getnodes/` | 获取所有节点 | network (query) | - |
| GET | `/api/v1/getnodetype/` | 获取节点类型 | network (query), node (query) | - |
| GET | `/api/v1/getscadaproperties/` | 获取SCADA点属性 | network (query), scada (query) | - |
| GET | `/api/v1/getstatus/` | 获取管线状态 | network (query), link (query) | - |
| GET | `/api/v1/getstatusschema` | 获取状态属性架构 | network (query) | - |
| GET | `/api/v1/gettitle/` | 获取水网标题属性 | network (query) | - |
| GET | `/api/v1/gettitleschema/` | 获取标题属性架构 | network (query) | - |
| GET | `/api/v1/isjunction/` | 检查是否为接点 | network (query), node (query) | - |
| GET | `/api/v1/islink/` | 检查管线有效性 | network (query), link (query) | - |
| GET | `/api/v1/isnode/` | 检查节点有效性 | network (query), node (query) | - |
| GET | `/api/v1/ispipe/` | 检查是否为管道 | network (query), link (query) | - |
| GET | `/api/v1/ispump/` | 检查是否为泵 | network (query), link (query) | - |
| GET | `/api/v1/isreservoir/` | 检查是否为水源 | network (query), node (query) | - |
| GET | `/api/v1/istank/` | 检查是否为蓄水池 | network (query), node (query) | - |
| GET | `/api/v1/isvalve/` | 检查是否为阀门 | network (query), link (query) | - |
| POST | `/api/v1/setstatus/` | 设置管线状态 | network (query), link (query) | - |
| GET | `/api/v1/settitle/` | 设置水网标题属性 | network (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getelementtype/` | 查询指定ID元素的类型(Junction/Pipe/Pump/Tank/Valve/Reservoir |
| `GET /getelementtypevalue/` | 查询指定ID元素的类型编码值 |
| `GET /getelementproperties/` | 查询指定ID元素的所有属性(自动识别类型) |
| `GET /getelementpropertieswithtype/` | 查询指定类型和ID的元素属性 |
| `GET /getlinkproperties/` | 查询管线(Pipe/Pump/Valve)的属性 |
| `GET /getnodeproperties/` | 查询节点(Junction/Tank/Reservoir)的属性 |
| `GET /settitle/` | 设置管网标题属性 |
| `POST /deletelink/` | 删除管线(管道/水泵/阀门) |
| `POST /deletenode/` | 删除节点(节点/水箱/水库) |
@@ -1,36 +0,0 @@
---
name: tjwater-action-business-network-assets-geometry
description: business/network-assets 下 geometry 操作技能。
version: 3.0.0
---
# geometry Action Skill
## 简介
负责 `business/network-assets` 场景下 `geometry` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/getmajornodecoords/` | 获取主要节点坐标 | network (query), diameter (query) | - |
| GET | `/api/v1/getmajorpipenodes/` | 获取主要管道节点 | network (query), diameter (query) | - |
| GET | `/api/v1/getnetworkgeometries/` | 获取完整网络几何信息 | network (query) | - |
| GET | `/api/v1/getnetworkinextent/` | 获取范围内的网络元素 | network (query), x1 (query), y1 (query), x2 (query), y2 (query) | - |
| GET | `/api/v1/getnetworklinknodes/` | 获取网络管线节点 | network (query) | - |
| GET | `/api/v1/getnodecoord/` | 获取节点坐标 | network (query), node (query) | - |
- 覆盖方法:`GET`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getnodecoord/` | 查询单个节点(任意类型)的坐标(x, y) |
| `GET /getmajornodecoords/` | 获取管网主要节点(干管节点)的坐标列表,用于快速渲染 |
| `GET /getmajorpipenodes/` | 获取主要管道的起终节点列表 |
| `GET /getnetworklinknodes/` | 获取管网中所有管线的起终节点信息 |
| `GET /getnetworkgeometries/` | 获取整个管网的完整几何信息(节点坐标 + 管线折点),适合地图绘制 |
| `GET /getnetworkinextent/` | 查询指定地理范围(bbox)内的管网节点和管线 |
@@ -1,52 +0,0 @@
---
name: tjwater-action-business-network-assets-junctions
description: business/network-assets 下 junctions 操作技能。
version: 3.0.0
---
# junctions Action Skill
## 简介
负责 `business/network-assets` 场景下 `junctions` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/addjunction/` | 添加节点 | network (query), junction (query), x (query), y (query), z (query) | - |
| POST | `/api/v1/deletejunction/` | 删除节点 | network (query), junction (query) | - |
| GET | `/api/v1/getalljunctionproperties/` | 获取所有节点属性 | network (query) | - |
| GET | `/api/v1/getjunctioncoord/` | 获取节点坐标 | network (query), junction (query) | - |
| GET | `/api/v1/getjunctiondemand/` | 获取节点需水量 | network (query), junction (query) | - |
| GET | `/api/v1/getjunctionelevation/` | 获取节点标高 | network (query), junction (query) | - |
| GET | `/api/v1/getjunctionpattern/` | 获取节点需水模式 | network (query), junction (query) | - |
| GET | `/api/v1/getjunctionproperties/` | 获取节点属性 | network (query), junction (query) | - |
| GET | `/api/v1/getjunctionschema` | 获取节点架构 | network (query) | - |
| GET | `/api/v1/getjunctionx/` | 获取节点 X 坐标 | network (query), junction (query) | - |
| GET | `/api/v1/getjunctiony/` | 获取节点 Y 坐标 | network (query), junction (query) | - |
| POST | `/api/v1/setjunctioncoord/` | 设置节点坐标 | network (query), junction (query), x (query), y (query) | - |
| POST | `/api/v1/setjunctiondemand/` | 设置节点需水量 | network (query), junction (query), demand (query) | - |
| POST | `/api/v1/setjunctionelevation/` | 设置节点标高 | network (query), junction (query), elevation (query) | - |
| POST | `/api/v1/setjunctionpattern/` | 设置节点需水模式 | network (query), junction (query), pattern (query) | - |
| POST | `/api/v1/setjunctionproperties/` | 批量设置节点属性 | network (query), junction (query) | - |
| POST | `/api/v1/setjunctionx/` | 设置节点 X 坐标 | network (query), junction (query), x (query) | - |
| POST | `/api/v1/setjunctiony/` | 设置节点 Y 坐标 | network (query), junction (query), y (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getjunctionschema` | 返回节点(Junction)数据模型的所有字段定义 |
| `GET /getjunctionproperties/` | 查询单个节点的所有属性(标高、需水量、坐标等) |
| `GET /getalljunctionproperties/` | 批量获取管网中所有节点的属性列表 |
| `GET /getjunctioncoord/` | 查询单个节点的坐标(x, y) |
| `GET /getjunctionelevation/` | 查询节点标高值 |
| `GET /getjunctiondemand/` | 查询节点基础需水量 |
| `GET /getjunctionpattern/` | 查询节点关联的需水时间模式名称 |
| `POST /addjunction/` | 向管网添加一个新节点,需提供ID、坐标和标高 |
| `POST /deletejunction/` | 从管网删除指定节点 |
| `POST /setjunction*/` | 设置节点某个具体属性(坐标、标高、需水量等) |
@@ -1,54 +0,0 @@
---
name: tjwater-action-business-network-assets-pipes
description: business/network-assets 下 pipes 操作技能。
version: 3.0.0
---
# pipes Action Skill
## 简介
负责 `business/network-assets` 场景下 `pipes` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/addpipe/` | 添加管道 | network (query), pipe (query), node1 (query), node2 (query) | length (query), diameter (query), roughness (query), minor_loss (query), status (query) |
| POST | `/api/v1/deletepipe/` | 删除管道 | network (query), pipe (query) | - |
| GET | `/api/v1/getallpipeproperties/` | 获取所有管道属性 | network (query) | - |
| GET | `/api/v1/getpipediameter/` | 获取管道管径 | network (query), pipe (query) | - |
| GET | `/api/v1/getpipelength/` | 获取管道长度 | network (query), pipe (query) | - |
| GET | `/api/v1/getpipeminorloss/` | 获取管道局部阻力系数 | network (query), pipe (query) | - |
| GET | `/api/v1/getpipenode1/` | 获取管道起始节点 | network (query), pipe (query) | - |
| GET | `/api/v1/getpipenode2/` | 获取管道终止节点 | network (query), pipe (query) | - |
| GET | `/api/v1/getpipeproperties/` | 获取管道属性 | network (query), pipe (query) | - |
| GET | `/api/v1/getpiperoughness/` | 获取管道粗糙度 | network (query), pipe (query) | - |
| GET | `/api/v1/getpipeschema` | 获取管道模式 | network (query) | - |
| GET | `/api/v1/getpipestatus/` | 获取管道状态 | network (query), pipe (query) | - |
| POST | `/api/v1/setpipediameter/` | 设置管道管径 | network (query), pipe (query), diameter (query) | - |
| POST | `/api/v1/setpipelength/` | 设置管道长度 | network (query), pipe (query), length (query) | - |
| POST | `/api/v1/setpipeminorloss/` | 设置管道局部阻力系数 | network (query), pipe (query), minor_loss (query) | - |
| POST | `/api/v1/setpipenode1/` | 设置管道起始节点 | network (query), pipe (query), node1 (query) | - |
| POST | `/api/v1/setpipenode2/` | 设置管道终止节点 | network (query), pipe (query), node2 (query) | - |
| POST | `/api/v1/setpipeproperties/` | 设置管道属性 | network (query), pipe (query) | - |
| POST | `/api/v1/setpiperoughness/` | 设置管道粗糙度 | network (query), pipe (query), roughness (query) | - |
| POST | `/api/v1/setpipestatus/` | 设置管道状态 | network (query), pipe (query), status (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getpipeschema` | 返回管道(Pipe)数据模型的所有字段定义 |
| `GET /getpipeproperties/` | 查询单条管道的所有属性(管径、长度、起终节点等) |
| `GET /getallpipeproperties/` | 批量获取管网中所有管道的属性列表 |
| `GET /getpipelength/` | 查询管道长度 |
| `GET /getpipediameter/` | 查询管道管径 |
| `GET /getpipestatus/` | 查询管道当前状态(开/关/CV) |
| `GET /getpiperoughness/` | 查询管道粗糙系数 |
| `POST /addpipe/` | 向管网添加一条新管道,需提供ID、起终节点、长度和管径 |
| `POST /deletepipe/` | 从管网删除指定管道 |
| `POST /setpipe*/` | 设置管道某个具体属性(管径、长度、状态等) |
@@ -1,42 +0,0 @@
---
name: tjwater-action-business-network-assets-pumps
description: business/network-assets 下 pumps 操作技能。
version: 3.0.0
---
# pumps Action Skill
## 简介
负责 `business/network-assets` 场景下 `pumps` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/addpump/` | 添加水泵 | network (query), pump (query), node1 (query), node2 (query) | power (query) |
| POST | `/api/v1/deletepump/` | 删除水泵 | network (query), pump (query) | - |
| GET | `/api/v1/getallpumpproperties/` | 获取所有水泵属性 | network (query) | - |
| GET | `/api/v1/getpumpnode1/` | 获取水泵起始节点 | network (query), pump (query) | - |
| GET | `/api/v1/getpumpnode2/` | 获取水泵终止节点 | network (query), pump (query) | - |
| GET | `/api/v1/getpumpproperties/` | 获取水泵属性 | network (query), pump (query) | - |
| GET | `/api/v1/getpumpschema` | 获取水泵模式 | network (query) | - |
| POST | `/api/v1/setpumpnode1/` | 设置水泵起始节点 | network (query), pump (query), node1 (query) | - |
| POST | `/api/v1/setpumpnode2/` | 设置水泵终止节点 | network (query), pump (query), node2 (query) | - |
| POST | `/api/v1/setpumpproperties/` | 设置水泵属性 | network (query), pump (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getpumpschema` | 返回水泵(Pump)数据模型的所有字段定义 |
| `GET /getpumpproperties/` | 查询单台水泵的所有属性(曲线名称、起终节点等) |
| `GET /getallpumpproperties/` | 批量获取管网中所有水泵的属性列表 |
| `POST /addpump/` | 向管网添加一台新水泵,需提供ID和起终节点 |
| `POST /deletepump/` | 从管网删除指定水泵 |
| `POST /setpumpproperties/` | 批量设置水泵属性(曲线、初始状态、效率等) |
| `POST /setpumpnode1/` | 设置水泵起始节点 |
| `POST /setpumpnode2/` | 设置水泵终止节点 |
@@ -1,69 +0,0 @@
---
name: tjwater-action-business-network-assets-regions
description: business/network-assets 下 regions 操作技能。
version: 3.0.1
---
# regions Action Skill
## 简介
负责 `business/network-assets` 场景下 `regions` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/adddistrictmeteringarea/` | 添加新DMA | network (query) | - |
| POST | `/api/v1/addregion/` | 添加新区域 | network (query) | - |
| POST | `/api/v1/addservicearea/` | 添加新服务区 | network (query) | - |
| POST | `/api/v1/addvirtualdistrict/` | 添加新虚拟分区 | network (query) | - |
| GET | `/api/v1/calculatedistrictmeteringareafornetwork/` | 计算整网DMA分区 | network (query) | - |
| GET | `/api/v1/calculatedistrictmeteringareafornodes/` | 计算节点DMA分区 | network (query) | - |
| GET | `/api/v1/calculatedistrictmeteringareaforregion/` | 计算区域内DMA分区 | network (query) | - |
| GET | `/api/v1/calculateservicearea/` | 计算服务区(返回全部时间步) | network (query) | - |
| GET | `/api/v1/calculatevirtualdistrict/` | 计算虚拟分区 | network (query), centers (query) | - |
| POST | `/api/v1/deletedistrictmeteringarea/` | 删除DMA | network (query) | - |
| POST | `/api/v1/deleteregion/` | 删除区域 | network (query) | - |
| POST | `/api/v1/deleteservicearea/` | 删除服务区 | network (query) | - |
| POST | `/api/v1/deletevirtualdistrict/` | 删除虚拟分区 | network (query) | - |
| POST | `/api/v1/generatedistrictmeteringarea/` | 生成DMA分区 | network (query), part_count (query), part_type (query), inflate_delta (query) | - |
| POST | `/api/v1/generateservicearea/` | 生成服务区分区 | network (query), inflate_delta (query) | - |
| POST | `/api/v1/generatesubdistrictmeteringarea/` | 生成DMA子分区 | network (query), dma (query), part_count (query), part_type (query), inflate_delta (query) | - |
| POST | `/api/v1/generatevirtualdistrict/` | 生成虚拟分区 | network (query), inflate_delta (query) | - |
| GET | `/api/v1/getalldistrictmeteringareaids/` | 获取所有DMA ID | network (query) | - |
| GET | `/api/v1/getalldistrictmeteringareas/` | 获取所有DMA | network (query) | - |
| GET | `/api/v1/getallserviceareas/` | 获取所有服务区 | network (query) | - |
| GET | `/api/v1/getallvirtualdistrict/` | 获取所有虚拟分区 | network (query) | - |
| GET | `/api/v1/getdistrictmeteringarea/` | 获取DMA信息 | network (query), id (query) | - |
| GET | `/api/v1/getdistrictmeteringareaschema/` | 获取DMA属性架构 | network (query) | - |
| GET | `/api/v1/getregion/` | 获取区域信息 | network (query), id (query) | - |
| GET | `/api/v1/getregionschema/` | 获取区域属性架构 | network (query) | - |
| GET | `/api/v1/getservicearea/` | 获取服务区信息 | network (query), id (query) | - |
| GET | `/api/v1/getserviceareaschema/` | 获取服务区属性架构 | network (query) | - |
| GET | `/api/v1/getvirtualdistrict/` | 获取虚拟分区信息 | network (query), id (query) | - |
| GET | `/api/v1/getvirtualdistrictschema/` | 获取虚拟分区属性架构 | network (query) | - |
| POST | `/api/v1/setdistrictmeteringarea/` | 设置DMA属性 | network (query) | - |
| POST | `/api/v1/setregion/` | 设置区域属性 | network (query) | - |
| POST | `/api/v1/setservicearea/` | 设置服务区属性 | network (query) | - |
| POST | `/api/v1/setvirtualdistrict/` | 设置虚拟分区属性 | network (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getregionschema` | 返回区域(Region)数据模型的字段定义 |
| `GET /getregion/` | 查询单个区域的属性 |
| `GET /getalldistrictmeteringareas/` | 获取所有 DMA(独立计量区)列表 |
| `GET /getallserviceareas/` | 获取所有服务区列表 |
| `POST /addregion/` | 新增区域(需提供名称和节点/管道列表) |
| `POST /adddistrictmeteringarea/` | 新增 DMA 分区 |
| `POST /addvirtualdistrict/` | 新增虚拟分区 |
| `POST /addservicearea/` | 新增服务区 |
| `GET /calculatedistrictmeteringareafornodes/` | 为指定节点集合计算其所属 DMA |
| `GET /calculatedistrictmeteringareaforregion/` | 为指定区域内的所有节点计算 DMA 归属 |
| `GET /calculatedistrictmeteringareafornetwork/` | 为整个管网的所有节点计算 DMA 归属 |
| `GET /calculateservicearea/` | 计算服务区,返回全部时间步结果 |
@@ -1,48 +0,0 @@
---
name: tjwater-action-business-network-assets-reservoirs
description: business/network-assets 下 reservoirs 操作技能。
version: 3.0.0
---
# reservoirs Action Skill
## 简介
负责 `business/network-assets` 场景下 `reservoirs` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/addreservoir/` | 添加水库 | network (query), reservoir (query), x (query), y (query), head (query) | - |
| POST | `/api/v1/deletereservoir/` | 删除水库 | network (query), reservoir (query) | - |
| GET | `/api/v1/getallreservoirproperties/` | 获取所有水库属性 | network (query) | - |
| GET | `/api/v1/getreservoircoord/` | 获取水库坐标 | network (query), reservoir (query) | - |
| GET | `/api/v1/getreservoirhead/` | 获取水库水头 | network (query), reservoir (query) | - |
| GET | `/api/v1/getreservoirpattern/` | 获取水库模式 | network (query), reservoir (query) | - |
| GET | `/api/v1/getreservoirproperties/` | 获取水库属性 | network (query), reservoir (query) | - |
| GET | `/api/v1/getreservoirschema` | 获取水库模式 | network (query) | - |
| GET | `/api/v1/getreservoirx/` | 获取水库X坐标 | network (query), reservoir (query) | - |
| GET | `/api/v1/getreservoiry/` | 获取水库Y坐标 | network (query), reservoir (query) | - |
| POST | `/api/v1/setreservoircoord/` | 设置水库坐标 | network (query), reservoir (query), x (query), y (query) | - |
| POST | `/api/v1/setreservoirhead/` | 设置水库水头 | network (query), reservoir (query), head (query) | - |
| POST | `/api/v1/setreservoirpattern/` | 设置水库模式 | network (query), reservoir (query), pattern (query) | - |
| POST | `/api/v1/setreservoirproperties/` | 设置水库属性 | network (query), reservoir (query) | - |
| POST | `/api/v1/setreservoirx/` | 设置水库X坐标 | network (query), reservoir (query), x (query) | - |
| POST | `/api/v1/setreservoiry/` | 设置水库Y坐标 | network (query), reservoir (query), y (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getreservoirschema` | 返回水库(Reservoir)数据模型的所有字段定义 |
| `GET /getreservoirproperties/` | 查询单个水库的所有属性(水头、模式、坐标等) |
| `GET /getallreservoirproperties/` | 批量获取管网中所有水库的属性列表 |
| `GET /getreservoirhead/` | 查询水库水头(即水库水位高度) |
| `GET /getreservoirpattern/` | 查询水库关联的时间模式名称 |
| `POST /addreservoir/` | 向管网添加一个新水库,需提供ID、坐标和水头 |
| `POST /deletereservoir/` | 从管网删除指定水库 |
| `POST /setreservoir*/` | 设置水库某个具体属性(水头、模式、坐标等) |
@@ -1,32 +0,0 @@
---
name: tjwater-action-business-network-assets-tags
description: business/network-assets 下 tags 操作技能。
version: 3.0.0
---
# tags Action Skill
## 简介
负责 `business/network-assets` 场景下 `tags` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/gettag/` | 获取标签信息 | network (query), t_type (query), id (query) | - |
| GET | `/api/v1/gettags/` | 获取所有标签 | network (query) | - |
| GET | `/api/v1/gettagschema/` | 获取标签属性架构 | network (query) | - |
| POST | `/api/v1/settag/` | 设置标签 | network (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /gettagschema/` | 返回标签(Tag)数据模型的字段定义 |
| `GET /gettag/` | 查询单个元素绑定的标签信息 |
| `GET /gettags/` | 获取管网中所有标签列表 |
| `POST /settag/` | 为管网元素设置/更新标签(支持自定义键值对属性) |
@@ -1,61 +0,0 @@
---
name: tjwater-action-business-network-assets-tanks
description: business/network-assets 下 tanks 操作技能。
version: 3.0.0
---
# tanks Action Skill
## 简介
负责 `business/network-assets` 场景下 `tanks` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/addtank/` | 新增水箱 | network (query), tank (query), x (query), y (query), elevation (query) | init_level (query), min_level (query), max_level (query), diameter (query), min_vol (query) |
| POST | `/api/v1/deletetank/` | 删除水箱 | network (query), tank (query) | - |
| GET | `/api/v1/getalltankproperties/` | 获取所有水箱属性 | network (query) | - |
| GET | `/api/v1/gettankcoord/` | 获取水箱坐标 | network (query), tank (query) | - |
| GET | `/api/v1/gettankdiameter/` | 获取水箱直径 | network (query), tank (query) | - |
| GET | `/api/v1/gettankelevation/` | 获取水箱标高 | network (query), tank (query) | - |
| GET | `/api/v1/gettankinitlevel/` | 获取水箱初始水位 | network (query), tank (query) | - |
| GET | `/api/v1/gettankmaxlevel/` | 获取水箱最大水位 | network (query), tank (query) | - |
| GET | `/api/v1/gettankminlevel/` | 获取水箱最小水位 | network (query), tank (query) | - |
| GET | `/api/v1/gettankminvol/` | 获取水箱最小体积 | network (query), tank (query) | - |
| GET | `/api/v1/gettankoverflow/` | 获取水箱溢流口 | network (query), tank (query) | - |
| GET | `/api/v1/gettankproperties/` | 获取水箱属性 | network (query), tank (query) | - |
| GET | `/api/v1/gettankschema` | 获取水箱模式 | network (query) | - |
| GET | `/api/v1/gettankvolcurve/` | 获取水箱容积曲线 | network (query), tank (query) | - |
| GET | `/api/v1/gettankx/` | 获取水箱X坐标 | network (query), tank (query) | - |
| GET | `/api/v1/gettanky/` | 获取水箱Y坐标 | network (query), tank (query) | - |
| POST | `/api/v1/settankcoord/` | 设置水箱坐标 | network (query), tank (query), x (query), y (query) | - |
| POST | `/api/v1/settankdiameter/` | 设置水箱直径 | network (query), tank (query), diameter (query) | - |
| POST | `/api/v1/settankelevation/` | 设置水箱标高 | network (query), tank (query), elevation (query) | - |
| POST | `/api/v1/settankinitlevel/` | 设置水箱初始水位 | network (query), tank (query), init_level (query) | - |
| POST | `/api/v1/settankmaxlevel/` | 设置水箱最大水位 | network (query), tank (query), max_level (query) | - |
| POST | `/api/v1/settankminlevel/` | 设置水箱最小水位 | network (query), tank (query), min_level (query) | - |
| POST | `/api/v1/settankminvol/` | 设置水箱最小体积 | network (query), tank (query), min_vol (query) | - |
| POST | `/api/v1/settankoverflow/` | 设置水箱溢流口 | network (query), tank (query), overflow (query) | - |
| POST | `/api/v1/settankproperties/` | 设置水箱属性 | network (query), tank (query) | - |
| POST | `/api/v1/settankvolcurve/` | 设置水箱容积曲线 | network (query), tank (query), vol_curve (query) | - |
| POST | `/api/v1/settankx/` | 设置水箱X坐标 | network (query), tank (query), x (query) | - |
| POST | `/api/v1/settanky/` | 设置水箱Y坐标 | network (query), tank (query), y (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /gettankschema` | 返回水箱(Tank)数据模型的所有字段定义 |
| `GET /gettankproperties/` | 查询单个水箱的所有属性(标高、直径、初始/最大/最小水位等) |
| `GET /getalltankproperties/` | 批量获取管网中所有水箱的属性列表 |
| `GET /gettankelevation/` | 查询水箱底部标高 |
| `GET /gettankdiameter/` | 查询水箱直径 |
| `GET /gettankinitlevel/` | 查询水箱初始水位 |
| `POST /addtank/` | 向管网添加一个新水箱,需提供ID、坐标、标高和水位参数 |
| `POST /deletetank/` | 从管网删除指定水箱 |
| `POST /settank*/` | 设置水箱某个具体属性(坐标、标高、直径、水位等) |
@@ -1,50 +0,0 @@
---
name: tjwater-action-business-network-assets-valves
description: business/network-assets 下 valves 操作技能。
version: 3.0.0
---
# valves Action Skill
## 简介
负责 `business/network-assets` 场景下 `valves` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/addvalve/` | 添加阀门 | network (query), valve (query), node1 (query), node2 (query) | diameter (query), v_type (query), setting (query), minor_loss (query) |
| POST | `/api/v1/deletevalve/` | 删除阀门 | network (query), valve (query) | - |
| GET | `/api/v1/getallvalveproperties/` | 获取所有阀门属性 | network (query) | - |
| GET | `/api/v1/getvalvediameter/` | 获取阀门直径 | network (query), valve (query) | - |
| GET | `/api/v1/getvalveminorloss/` | 获取阀门损失系数 | network (query), valve (query) | - |
| GET | `/api/v1/getvalvenode1/` | 获取阀门起点节点 | network (query), valve (query) | - |
| GET | `/api/v1/getvalvenode2/` | 获取阀门终点节点 | network (query), valve (query) | - |
| GET | `/api/v1/getvalveproperties/` | 获取阀门所有属性 | network (query), valve (query) | - |
| GET | `/api/v1/getvalveschema` | 获取阀门架构 | network (query) | - |
| GET | `/api/v1/getvalvesetting/` | 获取阀门开度 | network (query), valve (query) | - |
| GET | `/api/v1/getvalvetype/` | 获取阀门类型 | network (query), valve (query) | - |
| POST | `/api/v1/setvalvenode1/` | 设置阀门起点节点 | network (query), valve (query), node1 (query) | - |
| POST | `/api/v1/setvalvenode2/` | 设置阀门终点节点 | network (query), valve (query), node2 (query) | - |
| POST | `/api/v1/setvalvenodediameter/` | 设置阀门直径 | network (query), valve (query), diameter (query) | - |
| POST | `/api/v1/setvalveproperties/` | 批量设置阀门属性 | network (query), valve (query) | - |
| POST | `/api/v1/setvalvesetting/` | 设置阀门开度 | network (query), valve (query), setting (query) | - |
| POST | `/api/v1/setvalvetype/` | 设置阀门类型 | network (query), valve (query), type (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getvalveschema` | 返回阀门(Valve)数据模型的所有字段定义 |
| `GET /getvalveproperties/` | 查询单个阀门的所有属性(管径、类型、设定值等) |
| `GET /getallvalveproperties/` | 批量获取管网中所有阀门的属性列表 |
| `GET /getvalvediameter/` | 查询阀门管径 |
| `GET /getvalvetype/` | 查询阀门类型(PRV/PSV/TCV/FCV/PBV/GPV |
| `GET /getvalvesetting/` | 查询阀门设定值(压力设定或流量设定) |
| `POST /addvalve/` | 向管网添加一个新阀门,需提供ID、起终节点、管径和类型 |
| `POST /deletevalve/` | 从管网删除指定阀门 |
| `POST /setvalve*/` | 设置阀门某个具体属性(类型、设定值、管径等) |
@@ -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,32 +0,0 @@
---
name: tjwater-action-business-project-workspace-extension
description: business/project-workspace 下 extension 操作技能。
version: 3.0.0
---
# extension Action Skill
## 简介
负责 `business/project-workspace` 场景下 `extension` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/getallextensiondata/` | 获取所有扩展数据 | network (query) | - |
| GET | `/api/v1/getallextensiondatakeys/` | 获取所有扩展数据键 | network (query) | - |
| GET | `/api/v1/getextensiondata/` | 获取指定扩展数据 | network (query), key (query) | - |
| POST | `/api/v1/setextensiondata/` | 设置扩展数据 | network (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getallextensiondatakeys/` | 获取当前管网中所有已存储的自定义扩展数据的键名列表 |
| `GET /getallextensiondata/` | 获取当前管网所有自定义扩展数据(键值对集合) |
| `GET /getextensiondata/` | 按 key 查询指定的自定义扩展数据值 |
| `POST /setextensiondata/` | 设置或更新一个自定义扩展数据键值对(可用于存储任意业务自定义信息) |
@@ -1,36 +0,0 @@
---
name: tjwater-action-business-project-workspace-misc
description: business/project-workspace 下 misc 操作技能。
version: 3.0.0
---
# misc Action Skill
## 简介
负责 `business/project-workspace` 场景下 `misc` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/getallburstlocateresults/` | 获取所有爆管定位结果 | network (query) | - |
| GET | `/api/v1/getallsensorplacements/` | 获取所有传感器位置 | network (query) | - |
| GET | `/api/v1/getjson/` | 获取JSON示例 | - | - |
| GET | `/api/v1/getrealtimedata/` | 获取实时数据 | - | - |
| GET | `/api/v1/getsimulationresult/` | 获取模拟结果 | - | - |
| POST | `/api/v1/test_dict/` | 测试字典处理 | data (body) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getallburstlocateresults/` | 获取当前管网所有历史爆管定位分析结果(旧版接口) |
| `GET /getallsensorplacements/` | 获取当前管网所有传感器布置方案的结果列表 |
| `GET /getsimulationresult/` | 获取最近一次水力模拟结果(旧版接口) |
| `GET /getrealtimedata/` | 获取管网实时监测数据(旧版接口) |
| `GET /getjson/` | 返回示例 JSON 数据结构,用于开发调试 |
| `POST /test_dict/` | 测试字典类型请求体的接口,用于开发调试 |
@@ -1,32 +0,0 @@
---
name: tjwater-action-business-project-workspace-project-data
description: business/project-workspace 下 project_data 操作技能。
version: 3.0.0
---
# project_data Action Skill
## 简介
负责 `business/project-workspace` 场景下 `project_data` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/burst-locate-result` | 获取爆管定位结果 | - | - |
| GET | `/api/v1/burst-locate-result/{burst_incident}` | 按事件查询爆管定位结果 | burst_incident (path) | - |
| GET | `/api/v1/scada-info` | 获取SCADA信息 | - | - |
| GET | `/api/v1/scheme-list` | 获取方案列表 | - | - |
- 覆盖方法:`GET`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /burst-locate-result` | 获取所有爆管定位事件的历史结果列表(新版REST接口) |
| `GET /burst-locate-result/{burst_incident}` | 查询指定爆管事件(burst_incident ID)的详细定位结果 |
| `GET /scada-info` | 获取当前项目关联的 SCADA 设备和监测点信息汇总 |
| `GET /scheme-list` | 获取当前项目中所有可用的水力计算方案列表 |
@@ -1,55 +0,0 @@
---
name: tjwater-action-business-project-workspace-project
description: business/project-workspace 下 project 操作技能。
version: 3.0.0
---
# project Action Skill
## 简介
负责 `business/project-workspace` 场景下 `project` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/listprojects/` | 获取项目列表 | - | - |
| GET | `/api/v1/project_info/` | 获取项目信息 | network (query) | - |
| GET | `/api/v1/haveproject/` | 检查项目是否存在 | network (query) | - |
| POST | `/api/v1/createproject/` | 创建新项目 | network (query) | - |
| POST | `/api/v1/deleteproject/` | 删除项目 | network (query) | - |
| POST | `/api/v1/copyproject/` | 复制项目 | source (query), target (query) | - |
| GET | `/api/v1/isprojectopen/` | 检查项目是否已打开 | network (query) | - |
| POST | `/api/v1/openproject/` | 打开项目 | network (query) | - |
| POST | `/api/v1/closeproject/` | 关闭项目 | network (query) | - |
| GET | `/api/v1/isprojectlocked/` | 检查项目是否被锁定 | network (query) | - |
| GET | `/api/v1/isprojectlockedbyme/` | 检查项目是否被当前用户锁定 | network (query) | - |
| POST | `/api/v1/lockproject/` | 锁定项目 | network (query) | - |
| POST | `/api/v1/unlockproject/` | 解锁项目 | network (query) | - |
| POST | `/api/v1/importinp/` | 导入 INP 文件内容 | network (query) | - |
| GET | `/api/v1/exportinp/` | 导出项目为 ChangeSet | network (query), version (query) | - |
| POST | `/api/v1/readinp/` | 读取 INP 文件到项目 | network (query), inp (query) | - |
| GET | `/api/v1/dumpinp/` | 导出项目到 INP 文件 | network (query), inp (query) | - |
| POST | `/api/v1/uploadinp/` | 上传 INP 文件 | afile (body), name (query) | - |
| GET | `/api/v1/downloadinp/` | 下载 INP 文件 | name (query) | - |
| GET | `/api/v1/convertv3tov2/` | 转换 INP V3 为 V2 | - | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /listprojects/` | 获取服务器上所有可用的供水管网项目名称列表 |
| `GET /project_info/` | 从数据库获取项目的详细信息,包括地图范围等配置 |
| `POST /createproject/` | 创建一个新的供水管网项目;若已存在可能覆盖或报错 |
| `POST /deleteproject/` | 永久删除指定项目,此操作不可恢复 |
| `POST /openproject/` | 将指定项目加载到内存并初始化数据库连接池 |
| `POST /closeproject/` | 将指定项目从内存中卸载,释放相关资源 |
| `POST /lockproject/` | 锁定项目以防止并发修改 |
| `POST /unlockproject/` | 释放对项目的锁定 |
| `POST /importinp/` | 将 INP 格式文本内容导入到指定项目 |
| `GET /exportinp/` | 导出项目变更集(ChangeSet),含顶点、SCADA元素、DMA、SA、VD等 |
| `GET /convertv3tov2/` | 将 EPANET 3.0 格式的 INP 内容转换为 2.x 格式 |
@@ -1,30 +0,0 @@
---
name: tjwater-action-business-project-workspace-schemes
description: business/project-workspace 下 schemes 操作技能。
version: 3.0.0
---
# schemes Action Skill
## 简介
负责 `business/project-workspace` 场景下 `schemes` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/getallschemes/` | 获取所有方案 | network (query) | - |
| GET | `/api/v1/getscheme/` | 获取单个方案 | network (query), schema_name (query) | - |
| GET | `/api/v1/getschemeschema/` | 获取方案模式 | network (query) | - |
- 覆盖方法:`GET`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getschemeschema/` | 返回方案(Scheme)数据模型的字段定义 |
| `GET /getallschemes/` | 获取当前管网下所有已保存方案的列表 |
| `GET /getscheme/` | 查询指定方案名称(schema_name)的详细属性和配置 |
@@ -1,54 +0,0 @@
---
name: tjwater-action-business-project-workspace-snapshots
description: business/project-workspace 下 snapshots 操作技能。
version: 3.0.0
---
# snapshots Action Skill
## 简介
负责 `business/project-workspace` 场景下 `snapshots` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/batch/` | 执行批量命令 | network (query) | - |
| POST | `/api/v1/compressedbatch/` | 执行压缩批量命令 | network (query) | - |
| GET | `/api/v1/getcurrentoperationid/` | 获取当前操作ID | network (query) | - |
| GET | `/api/v1/getrestoreoperation/` | 获取恢复操作ID | network (query) | - |
| GET | `/api/v1/getsnapshots/` | 获取快照列表 | network (query) | - |
| GET | `/api/v1/havesnapshot/` | 检查快照是否存在 | network (query), tag (query) | - |
| GET | `/api/v1/havesnapshotforcurrentoperation/` | 检查当前操作快照是否存在 | network (query) | - |
| GET | `/api/v1/havesnapshotforoperation/` | 检查操作快照是否存在 | network (query), operation (query) | - |
| POST | `/api/v1/pickoperation/` | 选择操作 | network (query), operation (query) | discard (query) |
| POST | `/api/v1/picksnapshot/` | 选择快照 | network (query), tag (query) | discard (query) |
| POST | `/api/v1/redo/` | 重做操作 | network (query) | - |
| POST | `/api/v1/setrestoreoperation/` | 设置恢复操作ID | network (query), operation (query) | - |
| GET | `/api/v1/syncwithserver/` | 与服务器同步 | network (query), operation (query) | - |
| POST | `/api/v1/takenapshotforcurrentoperation` | 为当前操作创建快照(兼容模式) | network (query), tag (query) | - |
| POST | `/api/v1/takesnapshot/` | 创建快照 | network (query), tag (query) | - |
| POST | `/api/v1/takesnapshotforcurrentoperation` | 为当前操作创建快照 | network (query), tag (query) | - |
| POST | `/api/v1/takesnapshotforoperation/` | 为操作创建快照 | network (query), operation (query), tag (query) | - |
| POST | `/api/v1/undo/` | 撤销操作 | network (query) | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /getsnapshots/` | 列出当前管网所有已保存的快照标签(tag)列表 |
| `GET /havesnapshot/` | 检查指定 tag 的快照是否存在 |
| `POST /takesnapshot/` | 保存当前管网状态为一个快照,tag 为快照名称 |
| `POST /picksnapshot/` | 将管网状态回滚到指定快照,discard=true 时丢弃当前未保存修改 |
| `GET /getcurrentoperationid/` | 获取当前管网的操作ID(用于追踪操作历史) |
| `POST /undo/` | 撤销对管网的最近一次操作 |
| `POST /redo/` | 重做上一次被撤销的操作 |
| `POST /batch/` | 批量执行多个管网操作命令(原子事务) |
| `POST /compressedbatch/` | 执行压缩格式的批量命令(减少网络传输量) |
| `GET /syncwithserver/` | 将客户端的操作与服务端管网状态同步 |
| `POST /pickoperation/` | 切换到指定 operation ID 的历史操作状态 |
| `POST /takesnapshotforcurrentoperation` | 为当前 operation 创建快照(保存当前操作节点状态) |
-14
View File
@@ -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,34 +0,0 @@
---
name: tjwater-action-data-timeseries-access-composite
description: data/timeseries-access 下 composite 操作技能。
version: 3.0.0
---
# composite Action Skill
## 简介
负责 `data/timeseries-access` 场景下 `composite` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/composite/clean-scada` | 清洗SCADA监测数据 | device_ids (query), start_time (query), end_time (query) | - |
| GET | `/api/v1/composite/element-scada` | 获取管网元素关联的SCADA监测数据 | element_id (query), start_time (query), end_time (query) | use_cleaned (query) |
| GET | `/api/v1/composite/element-simulation` | 获取管网元素的模拟数据 | start_time (query), end_time (query), feature_infos (query) | scheme_type (query), scheme_name (query) |
| GET | `/api/v1/composite/pipeline-health-prediction` | 预测管道健康状况 | query_time (query), network_name (query) | - |
| GET | `/api/v1/composite/scada-simulation` | 获取SCADA关联的模拟数据 | start_time (query), end_time (query), device_ids (query) | scheme_type (query), scheme_name (query) |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /composite/scada-simulation` | 将 SCADA 设备历史监测数据与水力模拟结果对齐返回,便于压差分析 |
| `GET /composite/element-simulation` | 按管网元素ID和属性类型查询该元素在某方案下的模拟时序数据 |
| `GET /composite/element-scada` | 查询某管网元素关联的 SCADA 设备的历史监测时序数据,支持使用清洗后数据(use_cleaned |
| `POST /composite/clean-scada` | 对指定设备在指定时间段的 SCADA 数据进行清洗处理(去噪、异常值替换) |
| `GET /composite/pipeline-health-prediction` | 在指定时刻对整个管网的管道健康状态进行预测,返回各管道健康评分 |
@@ -1,44 +0,0 @@
---
name: tjwater-action-data-timeseries-access-realtime
description: data/timeseries-access 下 realtime 操作技能。
version: 3.0.0
---
# realtime Action Skill
## 简介
负责 `data/timeseries-access` 场景下 `realtime` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| DELETE | `/api/v1/realtime/links` | 删除实时管道数据 | start_time (query), end_time (query) | - |
| GET | `/api/v1/realtime/links` | 查询实时管道数据 | start_time (query), end_time (query) | - |
| POST | `/api/v1/realtime/links/batch` | 批量插入实时管道数据 | data (body) | - |
| PATCH | `/api/v1/realtime/links/{link_id}/field` | 更新实时管道字段 | link_id (path), time (query), field (query), value (query) | - |
| DELETE | `/api/v1/realtime/nodes` | 删除实时节点数据 | start_time (query), end_time (query) | - |
| GET | `/api/v1/realtime/nodes` | 查询实时节点数据 | start_time (query), end_time (query) | - |
| POST | `/api/v1/realtime/nodes/batch` | 批量插入实时节点数据 | data (body) | - |
| GET | `/api/v1/realtime/query/by-id-time` | 按ID和时间查询实时模拟数据 | id (query), type (query), query_time (query) | - |
| GET | `/api/v1/realtime/query/by-time-property` | 按时间和属性查询实时数据 | query_time (query), type (query), property (query) | - |
| POST | `/api/v1/realtime/simulation/store` | 存储实时模拟结果 | node_result_list (body), link_result_list (body), result_start_time (query) | - |
- 覆盖方法:`DELETE, GET, PATCH, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `POST /realtime/links/batch` | 批量将管道(link)模拟结果写入实时数据表(TimescaleDB |
| `GET /realtime/links` | 查询指定时间范围内的实时管道模拟数据 |
| `DELETE /realtime/links` | 删除指定时间范围内的实时管道数据 |
| `PATCH /realtime/links/{link_id}/field` | 修改某条管道在特定时刻的某个字段值 |
| `POST /realtime/nodes/batch` | 批量将节点模拟结果写入实时数据表 |
| `GET /realtime/nodes` | 查询指定时间范围内的实时节点模拟数据 |
| `DELETE /realtime/nodes` | 删除指定时间范围内的实时节点数据 |
| `POST /realtime/simulation/store` | 一次性存储一次完整模拟运行的节点和管道结果(含起始时间) |
| `GET /realtime/query/by-time-property` | 按查询时间点和属性名(如 pressure/flow)查询全网实时模拟值 |
| `GET /realtime/query/by-id-time` | 按单个元素ID、类型和时间点查询其实时模拟值 |
@@ -1,48 +0,0 @@
---
name: tjwater-action-data-timeseries-access-scheme
description: data/timeseries-access 下 scheme 操作技能。
version: 3.0.0
---
# scheme Action Skill
## 简介
负责 `data/timeseries-access` 场景下 `scheme` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| DELETE | `/api/v1/scheme/links` | 删除方案管道数据 | scheme_type (query), scheme_name (query), start_time (query), end_time (query) | - |
| GET | `/api/v1/scheme/links` | 查询方案管道数据 | scheme_type (query), scheme_name (query), start_time (query), end_time (query) | - |
| POST | `/api/v1/scheme/links/batch` | 批量插入方案管道数据 | data (body) | - |
| GET | `/api/v1/scheme/links/{link_id}/field` | 查询方案管道字段数据 | link_id (path), scheme_type (query), scheme_name (query), start_time (query), end_time (query), field (query) | - |
| PATCH | `/api/v1/scheme/links/{link_id}/field` | 更新方案管道字段 | link_id (path), scheme_type (query), scheme_name (query), time (query), field (query), value (query) | - |
| DELETE | `/api/v1/scheme/nodes` | 删除方案节点数据 | scheme_type (query), scheme_name (query), start_time (query), end_time (query) | - |
| POST | `/api/v1/scheme/nodes/batch` | 批量插入方案节点数据 | data (body) | - |
| GET | `/api/v1/scheme/nodes/{node_id}/field` | 查询方案节点字段数据 | node_id (path), scheme_type (query), scheme_name (query), start_time (query), end_time (query), field (query) | - |
| PATCH | `/api/v1/scheme/nodes/{node_id}/field` | 更新方案节点字段 | node_id (path), scheme_type (query), scheme_name (query), time (query), field (query), value (query) | - |
| GET | `/api/v1/scheme/query/by-id-time` | 按ID和时间查询方案模拟数据 | scheme_type (query), scheme_name (query), id (query), type (query), query_time (query) | - |
| GET | `/api/v1/scheme/query/by-scheme-time-property` | 按方案、时间和属性查询数据 | scheme_type (query), scheme_name (query), query_time (query), type (query), property (query) | - |
| POST | `/api/v1/scheme/simulation/store` | 存储方案模拟结果 | scheme_type (query), scheme_name (query), node_result_list (body), link_result_list (body), result_start_time (query) | - |
- 覆盖方法:`DELETE, GET, PATCH, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `POST /scheme/links/batch` | 批量将管道模拟结果写入指定方案(scheme_type + scheme_name)的数据表 |
| `GET /scheme/links` | 查询指定方案和时间范围内的所有管道模拟数据 |
| `DELETE /scheme/links` | 删除指定方案和时间范围内的管道模拟数据 |
| `GET /scheme/links/{link_id}/field` | 查询特定管道在指定方案和时间范围内某字段的时序数据 |
| `PATCH /scheme/links/{link_id}/field` | 修改特定管道在指定方案某时刻的某个字段值 |
| `POST /scheme/nodes/batch` | 批量将节点模拟结果写入指定方案的数据表 |
| `GET /scheme/nodes/{node_id}/field` | 查询特定节点在指定方案和时间范围内某字段的时序数据 |
| `PATCH /scheme/nodes/{node_id}/field` | 修改特定节点在指定方案某时刻的某个字段值 |
| `DELETE /scheme/nodes` | 删除指定方案和时间范围内的节点模拟数据 |
| `POST /scheme/simulation/store` | 一次性存储完整方案模拟结果(节点 + 管道),需提供 scheme_type 和 scheme_name |
| `GET /scheme/query/by-id-time` | 按元素ID、类型和时间点查询该元素在指定方案下的模拟值 |
| `GET /scheme/query/by-scheme-time-property` | 按方案、时间点和属性名查询全网在指定方案下的模拟值 |
-191
View File
@@ -1,191 +0,0 @@
# 示例(基于 opencode Agent chat/stream 工具调用链)
## 示例 1:前端发起对话,opencode agent 触发工具调用
用户意图:查询设备 `170490` 在时间范围内的 `monitored_value`
前端调用 `POST /api/v1/agent/chat/stream`
```json
{
"message": "请查询设备170490在最近24小时的monitored_value历史数据",
"session_id": "agent-demo-001"
}
```
请求头至少包含(由前端传入):
- `Authorization: Bearer <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"
}
}
```
## 示例 2opencode agent 多步规划 + 多次工具调用
用户消息:
- “先查这个设备历史数据,再给我异常点摘要。”
典型链路:
- 第一步工具调用:查询历史数据接口。
- 第二步(可选)工具调用:查询补充数据接口。
- opencode agent 汇总工具结果,持续通过 SSE 输出 `progress``token`,最终返回 `done`
`progress` 示例:
```json
{
"session_id": "agent-demo-001",
"id": "tool-dynamic-http",
"phase": "tool",
"status": "running",
"title": "正在调用后端数据查询"
}
```
## 示例 3:前端工具 — 定位要素
用户消息:
- "帮我找到管道 P-001 和 P-002"
opencode agent 调用工具 `locate_features`
```json
{
"ids": ["P-001", "P-002"],
"feature_type": "pipe"
}
```
前端收到 SSE 事件后缩放地图并高亮管道。opencode agent 回复文字:"已在地图上定位到管道 P-001 和 P-002。"
## 示例 4:前端工具 — 对话内图表
用户消息:
- "展示节点 J-001 最近一天的压力变化曲线"
典型链路:
1. opencode agent 先调用 `dynamic_http_call` 查询数据
2. 拿到数据后,调用 `show_chart` 将处理好的数据传给前端渲染
第一步 — opencode agent 调用 `dynamic_http_call`
```json
{
"path": "/api/v1/composite/element-simulation",
"method": "GET",
"arguments": {
"feature_infos": "[\"J-001\", \"node\"]",
"start_time": "2026-03-29T00:00:00Z",
"end_time": "2026-03-30T00:00:00Z"
}
}
```
第二步 — opencode agent 处理数据后调用 `show_chart`
```json
{
"title": "节点 J-001 压力变化",
"chart_type": "line",
"x_data": ["03-29 00:00", "03-29 01:00", "03-29 02:00", "..."],
"series": [
{
"name": "J-001 压力",
"data": [32.5, 31.8, 30.2, "..."]
}
],
"y_axis_name": "压力 (m)"
}
```
前端直接用 AI 提供的数据渲染 ECharts 图表,不再请求后端。
## 示例 5:前端工具 — 查看 SCADA 监测面板
用户消息:
- "我想看看 J-001 的监测数据"
opencode agent 调用工具 `view_scada`
```json
{
"device_ids": ["J-001"],
"start_time": "2026-03-29T00:00:00Z",
"end_time": "2026-03-30T00:00:00Z"
}
```
前端打开 SCADA 监测面板,展示该节点的历史监测曲线。
## 示例 6:记住用户长期偏好
用户消息:
- "以后回答尽量简洁,先给结论再解释。"
opencode agent 调用工具 `memory_manager`
```json
{
"action": "add",
"reason": "用户明确给出了长期有效的回答风格偏好,后续会话也应遵守。",
"scope": "user",
"content": "用户偏好先给结论、再补必要解释,整体风格尽量简洁。"
}
```
## 示例 7:检索历史案例而不是误写入 memory
用户消息:
- "我们之前是不是分析过类似的爆管定位问题?"
opencode agent 调用工具 `session_search`
```json
{
"reason": "用户在询问过往会话中的类似案例,应先检索历史 transcript 而不是写入新的 memory。",
"query": "爆管定位 类似案例",
"max_results": 5
}
```
## 示例 8:沉淀可复用 workflow 模式
用户消息:
- "这套瓶颈分析流程之后可以复用。"
opencode agent 调用工具 `skill_manager`
```json
{
"action": "append_pattern",
"reason": "本轮已验证一套稳定可复用的瓶颈分析 workflow,适合沉淀到已有 skill。",
"skill_path": "workflow/bottleneck-analysis",
"pattern": "当瓶颈分析依赖大体量属性数据和模拟结果时,先用 dynamic_http_call 获取 preview,再用 fetch_result_ref 回读完整数据后再做合并与排序。"
}
```
## 示例 9:给单个 workflow skill 写入可复用脚本
当某个 workflow 的本地 Python 处理逻辑已经稳定、未来同类任务会重复使用时,可写入该 skill 自己的 `scripts/*.py`
```json
{
"action": "write_script",
"reason": "本轮已验证瓶颈分析中的合并与排序脚本,后续同类 workflow 可直接复用。",
"skill_path": "workflow/bottleneck-analysis",
"file_path": "scripts/merge_and_rank.py",
"content": "import json\n\n\ndef rank_links(rows):\n return sorted(rows, key=lambda row: row['composite_score'], reverse=True)\n"
}
```
脚本应只归属当前 `skill_path`,不要写到 `data/` 或其他 skill 目录。
-14
View File
@@ -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,30 +0,0 @@
---
name: tjwater-action-platform-governance-observability-audit
description: platform/governance-observability 下 audit 操作技能。
version: 3.0.0
---
# audit Action Skill
## 简介
负责 `platform/governance-observability` 场景下 `audit` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/audit/logs` | 查询审计日志 | - | user_id (query), project_id (query), action (query), resource_type (query), start_time (query), end_time (query), skip (query), limit (query) |
| GET | `/api/v1/audit/logs/count` | 获取审计日志总数 | - | user_id (query), project_id (query), action (query), resource_type (query), start_time (query), end_time (query) |
| GET | `/api/v1/audit/logs/my` | 查询我的审计日志 | - | action (query), start_time (query), end_time (query), skip (query), limit (query) |
- 覆盖方法:`GET`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /audit/logs` | 查询系统审计日志(仅管理员),支持按用户ID、项目ID、操作类型、资源类型、时间范围过滤;支持分页(skip/limit) |
| `GET /audit/logs/count` | 获取满足过滤条件的审计日志总条数,用于分页显示 |
| `GET /audit/logs/my` | 查询当前登录用户自己的操作日志,支持按操作类型和时间范围过滤 |
@@ -1,32 +0,0 @@
---
name: tjwater-action-platform-governance-observability-cache
description: platform/governance-observability 下 cache 操作技能。
version: 3.0.0
---
# cache Action Skill
## 简介
负责 `platform/governance-observability` 场景下 `cache` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| POST | `/api/v1/clearallredis/` | 清除所有缓存 | - | - |
| POST | `/api/v1/clearrediskey/` | 清除单个缓存键 | key (query) | - |
| POST | `/api/v1/clearrediskeys/` | 清除匹配的缓存键 | keys (query) | - |
| GET | `/api/v1/queryredis/` | 查询缓存键列表 | - | - |
- 覆盖方法:`GET, POST`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /queryredis/` | 查询当前 Redis 中所有缓存键列表,用于检查缓存状态 |
| `POST /clearrediskey/` | 删除指定单个键的缓存(精确匹配) |
| `POST /clearrediskeys/` | 批量删除多个匹配的缓存键(支持模式匹配) |
| `POST /clearallredis/` | 清除 Redis 中所有缓存数据(慎用,会影响所有会话) |
@@ -1,30 +0,0 @@
---
name: tjwater-action-platform-governance-observability-meta
description: platform/governance-observability 下 meta 操作技能。
version: 3.0.0
---
# meta Action Skill
## 简介
负责 `platform/governance-observability` 场景下 `meta` 的具体接口调用。
## 子模块索引 (渐进式引导)
- 当前为叶子节点,直接使用下方接口目录。
## 接口目录
| Method | Path | Summary | Required Params | Optional Params |
|---|---|---|---|---|
| GET | `/api/v1/meta/db/health` | 检查数据库健康状态 | - | - |
| GET | `/api/v1/meta/project` | 获取项目元数据 | - | - |
| GET | `/api/v1/meta/projects` | 列出用户项目 | - | - |
- 覆盖方法:`GET`
## 接口说明
| 接口 | 说明 |
|---|---|
| `GET /meta/db/health` | 检查数据库(PostgreSQL/TimescaleDB)连接健康状态,返回 ok/error |
| `GET /meta/project` | 获取当前用户当前项目的元数据(名称、创建时间、所有者等) |
| `GET /meta/projects` | 列出当前登录用户有权限访问的所有项目信息 |
-125
View File
@@ -1,125 +0,0 @@
# API Skills 使用 Runbook(工具调用链)
## 1) 总体原则
- Skills 负责“告诉模型可做什么”。
- `chat/stream` 内部启动 opencode 会话,并注册工具 `dynamic_http_call`
- opencode agent 通过工具调用后端能力,不直接发 HTTP。
- TJWaterAgent 执行器负责“代表当前用户调真实后端 API”(动态路径,无白名单)。
- 会话完成后,运行时会基于 transcript 做后台 learning review;这一步用于判断是否需要更新 memory 或 skill,而不是替代主任务回答。
## 1.1) 自我学习闭环
- **memory_manager**:保存用户长期偏好 / 约束,以及稳定 workspace 事实
- **skill_manager**:保存经过验证、可复用的 workflow / 方法 / pitfall
- **session_search**:检索当前用户 + 当前项目范围内的历史会话 transcript,用于回忆旧案例,避免把一次性案例写入 memory
推荐分流:
- 需要长期遵守的偏好 / 稳定事实 → `memory_manager`
- 可复用的方法、步骤、坑点 → `skill_manager`
- 某次分析过程、历史案例、临时结论 → `session_search`
## 2) 请求入口(前端)
- `POST /api/v1/agent/chat/stream`(唯一前端入口)
不提供 `/execute` 对外调用路径,统一通过 `chat/stream` + 工具调用链执行。
请求体:
```json
{
"message": "帮我分析当前管网中的水力瓶颈管道,并给出改造建议",
"session_id": "agent-demo-001"
}
```
SSE 事件:
| event | 用途 | 关键字段 |
| --- | --- | --- |
| `progress` | 展示 Agent 处理过程、规划和工具进度 | `session_id`, `id`, `phase`, `status`, `title`, `detail` |
| `token` | 渲染面向用户的最终回答文本 | `session_id`, `content` |
| `tool_call` | 驱动前端地图/面板/图表动作 | `session_id`, `tool`, `params` |
| `done` | 当前轮对话结束 | `session_id` |
| `error` | 当前轮失败 | `session_id`, `message`, `detail` |
`progress.status` 取值为 `running``completed``error`;前端应按相同 `id` 覆盖更新同一条进度,而不是重复追加。
## 3) 工具参数约定(opencode agent 调用工具时)
```json
{
"path": "/api/v1/scada/by-ids-field-time-range",
"method": "GET",
"arguments": {
"device_ids": "170490",
"field": "monitored_value",
"start_time": "2026-03-29T07:57:47.338Z",
"end_time": "2026-03-30T07:57:47.338Z"
}
}
```
说明(工具 `dynamic_http_call`):
- `path` 必须以 `/` 开头。
- `method` 支持:`GET/POST/PUT/PATCH/DELETE`
- `arguments` 会编码为 query 参数(列表会转为逗号拼接)。
## 3.1) 学习工具约定
- 所有学习类工具都必须带 `reason`
- `memory_manager` 支持:`add / list / replace / remove`
- `skill_manager` 支持:`list / append_pattern / remove_pattern / write_reference / remove_reference / write_script / remove_script`
- `session_search` 只搜索当前用户 + 当前项目作用域,不接受跨项目检索
- `skill_manager` 的结构化写入优先落到:
1. `## Learned Patterns`
2. `references/*.md`
3. `scripts/*.py`
不应直接重写 skill frontmatter 或任意正文段落
- `scripts/*.py` 仅表示当前 `skill_path` 私有的可复用脚本资产;不要把运行时临时脚本写进 `data/`
## 4) 用户上下文注入(后端执行阶段)
- `Authorization`Bearer Token
- `x-project-id`
执行器会附带 `x-trace-id` 用于链路排查(可透传或自动生成)。
## 5) 排障要点
- `400`:检查工具参数 `path/method/arguments` 格式。
- `401/403`:检查 token 与项目权限。
- `404`:检查 `path` 是否正确。
- `422`:检查 `arguments` 字段名与类型。
- `5xx`:记录 `trace_id`,结合后端日志排查。
## 6) 前端工具调用链
前端工具(`locate_features`, `view_history`, `view_scada`, `show_chart`)的调用链与 `dynamic_http_call` 不同:
```
用户消息 → opencode agent → tool calling → 调用前端工具 (如 locate_features)
tool handler:
1) 推送 SSE tool_call 事件到前端
2) 返回简短确认给 opencode agent"已定位到管道"
前端同时收到:
- SSE event: progress → 展示规划/工具执行/完成状态
- SSE event: tool_call → 前端执行操作(定位地图/打开面板/渲染图表)
- SSE event: token → 渲染 opencode agent 文字回复
```
关键区别:
- `dynamic_http_call`TJWaterAgent 代理 HTTP 请求,结果返回给 opencode agent 做后续分析。
- 前端工具:TJWaterAgent 仅推送 SSE 事件,前端直接执行,结果不返回 opencode agent。
- `show_chart`opencode agent 先通过 `dynamic_http_call` 查询数据,处理为 x_data + series 格式后调用 `show_chart`,前端直接渲染图表,不再请求后端。
## 7) 复盘与沉淀建议
- 复杂多工具任务完成后,优先判断是否产生了稳定 workflow,可写入 `skill_manager`
- 用户明确纠正表达风格、输出格式或步骤时,优先判断是否需要写入 `memory_manager`
- 如果你只是想确认“以前是不是处理过类似问题”,先用 `session_search`
- 如果结果仍然只是 preview,不要基于 preview 做 learned pattern,总是先 `fetch_result_ref`
+186
View File
@@ -0,0 +1,186 @@
---
name: tjwater-cli
description: tjwater-cli 命令行工具使用说明,涵盖命令发现、输出格式、命令族、错误处理及最佳实践。
---
# tjwater-cli 使用说明
## 概述
`tjwater-cli` 是 TJWater 供水管网系统的命令行工具,用于与后端服务交互,支持数据查询、分析和工程操作。所有输出统一为 JSON 格式。
## 工具调用
通过 `tjwater_cli` 工具执行 CLI 命令:
```json
{
"reason": "说明调用原因",
"command": "project list",
"timeout": 120,
"store_result": false
}
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `reason` | string | 是 | 调用原因 |
| `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`):向服务端发起计算请求,返回任务状态/ID,**不直接返回分析结果**。
- **数据获取**`data timeseries`):所有计算结果(仿真压力、分析指标等)的唯一数据出口,需在触发动作完成后调用。
```
simulation/analysis → 触发计算 → 返回状态/任务ID
data timeseries → 获取计算结果
```
通过 `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` 确认其存在及参数签名,禁止凭经验拼写。
### 已知命令族
| 命令族 | 典型子命令 | 用途 |
|------|-----------|------|
| `project` | `list`, `db-health` | 项目管理、数据库健康检查 |
| `data` | `timeseries realtime links / nodes`, `timeseries scada query` | **时序数据查询**(实时/SCADA),所有分析结果的唯一获取渠道 |
| `simulation` | 通过 `help simulation` 发现 | **触发水力仿真计算**(执行成功返回状态,实际结果需走 `data timeseries` 获取) |
| `analysis` | 通过 `help analysis` 发现 | **触发分析计算**(执行成功返回状态,实际结果需走 `data timeseries` 获取) |
| `net` | `list-pipes` | 管网拓扑查询 |
| `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. **reason 必填** — 每次调用必须说明具体理由
3. **触发后取数据**`simulation`/`analysis` 仅触发计算,结果必须从 `data timeseries` 获取,勿将触发返回的状态信息当作分析结果
4. **文件分析** — workflow 脚本需要文件时使用 `store_result=true`,不得从 Bash 直接联网调用 CLI
5. **结果验证** — 始终检查 `ok` 字段,失败时先处理错误码再重试
6. **大结果集** — 优先过滤/采样,不要一次性拉取全部数据
7. **模拟时长控制** — 模拟(`simulation`)或方案模拟的 `--duration` 不宜过长,建议每次仿真时间跨度控制在一小时以内,避免计算耗时过长或结果数据量过大
## 示例
### 查询所有实时节点数据
```json
{
"reason": "获取最近1小时内全部节点的实时数据",
"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
{
"reason": "查询节点 J-001 最近1小时的压力数据",
"command": "data timeseries scheme node-field --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
{
"reason": "查询 SCADA 设备 170490 在指定时间范围的 monitored_value",
"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: 先尝试获取仿真结果
{
"reason": "尝试获取节点 J-001 09:00 时刻的仿真压力",
"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 为空),触发仿真
{
"reason": "无已有仿真结果,触发1小时水力仿真",
"command": "simulation run --start-time 2026-06-03T08:00:00+08:00 --duration 60"
}
// step 3: 仿真完成后,再次获取结果(同 step 1)
{
"reason": "获取仿真结果中节点 J-001 09:00 时刻的压力",
"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`(整数分钟,必填)。
-19
View File
@@ -1,19 +0,0 @@
---
name: tjwater-workflow
description: 负责分析类工作流能力。
version: 1.0.0
---
# Workflow Domain Skill
## 简介
负责分析场景下的工作流组织与调用入口能力。
## 使用策略
- 当用户问题明显属于“多接口 + 本地分析 + 综合结论”的分析任务时,优先从本目录查找固定 workflow。
- 如果找到合适 workflow,应先按 workflow 执行主路径,再补充缺少的原子 skill。
- 如果没有匹配 workflow,或现有 workflow 缺少关键步骤、接口或输出约束,再回到其他 domain/scenario/action skills 组合能力。
## 子模块索引 (渐进式引导)
- **bottleneck-analysis**: 见 `./bottleneck-analysis/SKILL.md`
@@ -1,121 +0,0 @@
---
name: tjwater-workflow-bottleneck-analysis
description: workflow 下 bottleneck-analysis(水力瓶颈分析)工作流技能。
version: 1.1.0
---
# bottleneck-analysis Workflow Skill
## 简介
负责 `analytics/simulation-analysis` 场景下的水力瓶颈综合分析,通过结合管道属性与水力模拟结果,识别管网中超负荷、高流速、高水头损失的瓶颈管道,并给出分级改造建议。
## 前置依赖
本工作流依赖以下两个数据源,需按顺序并行或串行获取:
### 依赖 1:管道属性数据
- 接口:`GET /api/v1/getallpipeproperties/`
- 参数:`network`query,如 `tjwater`
- 用途:获取全部管道的 id、管径(diameter)、长度(length)、粗糙度(roughness)、起端(node1)、终端(node2) 等属性
- 注意:结果可能很大(数万条),需使用 `fetch_result_ref` 分批或全量获取
### 依赖 2:水力模拟结果
- 接口:`GET /api/v1/runprojectreturndict/`
- 参数:`network`query,如 `tjwater`
- 用途:运行管网水力模拟,返回各管段的 flow(LPS)、velocity(m/s)、headloss(m)、status,以及各节点的 demand、head、pressure(KPA)
- 注意:结果可达 30MB+,需用 Python 脚本批量处理或使用 `fetch_result_ref` 回读
## 工作流步骤
### 第 1 步:并行获取管道属性和运行水力模拟
同时调用 `getallpipeproperties``runprojectreturndict`,network 参数使用项目名称(如 `tjwater`)。
### 第 2 步:合并数据
用 Python 脚本将管道属性的 pipe_id 与模拟结果的 link_id 进行关联,构建含以下字段的合并数据集:
| 字段 | 来源 | 说明 |
|------|------|------|
| id | 两者关联键 | 管道/链路 ID |
| flow | 模拟 link_results | 流量 (LPS) |
| velocity | 模拟 link_results | 流速 (m/s) |
| headloss | 模拟 link_results | 水头损失 (m) |
| diameter | 管道属性 | 管径 (mm) |
| length | 管道属性 | 长度 (m) |
| roughness | 管道属性 | 粗糙度系数 |
| node1 / node2 | 管道属性 | 起端/终端节点 ID |
| unit_headloss | 计算 | headloss / length (m/m) |
| capacity_ratio | 计算 | |flow| / (π×(d/2000)²×1000),即实际流量与 1m/s 设计流量的比值 |
同时从模拟 node_results 提取各节点 pressure,关联到管段两端。
### 第 3 步:多维度瓶颈识别
按以下 5 个维度分别排序筛选,交叉印证:
| 维度 | 筛选条件 | 指示含义 |
|------|---------|---------|
| 高流速 | velocity > 1.2 m/s | 管径不足 |
| 主干管高流量 | diameter ≥ 300mm 且 velocity > 0.5 m/s | 传输瓶颈 |
| 高水头损失 | headloss > 5m 且 0.3 < velocity < 1.5 m/s | 能耗瓶颈/粗糙度问题 |
| 高单位水头损失 | unit_headloss > 1.0 m/m | 严重局部瓶颈 |
| 超负荷 | capacity_ratio > 1.0 | 实际流量超过设计能力 |
排除极短管道(length < 0.5m)以减少噪声。
### 第 4 步:综合评分
对有效管道计算综合瓶颈分数:
```
composite_score = (velocity / max_velocity) × 0.4
+ (headloss / max_headloss) × 0.3
+ (capacity_ratio / max_capacity_ratio) × 0.3
```
取 TOP 10~20 作为最严重瓶颈管道。
### 第 5 步:前端可视化
- 使用 `show_chart` 展示流速分布柱状图
- 使用 `locate_features` 在地图上定位 TOP 瓶颈管道(feature_type=pipe
- 可选:使用 `view_history` 查看瓶颈管道的历史运行数据
- 前端工具仅用于展示,分析结论必须来自 `dynamic_http_call` / `fetch_result_ref` 获得的数据
### 第 6 步:给出分级改造建议
按严重程度分为三级:
- **🚨 紧急**:综合评分 > 0.3,立即安排管径升级
- **⚡ 重点**:综合评分 0.15~0.3,纳入近期改造计划
- **📋 关注**:综合评分 0.05~0.15 或单维度超标,持续监测
每条建议含:当前管径 → 建议管径(基于目标流速 1.0~1.5 m/s 反推),并附改造理由。
## 改造管径计算公式
```
建议管径(mm) = 2 × 1000 × sqrt(|flow| / (π × target_velocity × 1000))
```
目标流速:DN<300 取 1.0 m/sDN≥300 取 1.2 m/s。
## 证据约束
- 如果关键数据仍处于 preview 状态,不得直接输出最终瓶颈结论
- 如果模拟结果不完整或接口失败,应明确说明当前仅能做初步筛查
- 改造建议必须区分“数据直接支持的结论”和“工程经验推断”
## 推荐输出结构
1. 分析范围与数据来源
2. 主要瓶颈管段 Top N
3. 分级建议(紧急 / 重点 / 关注)
4. 假设与局限
5. 是否建议地图定位或图表展示
## 参考
- 管道属性操作:`../business/network-assets/pipes/SKILL.md`
- 模拟操作:`./simulation/SKILL.md`
- 节点属性操作:`../business/network-assets/junctions/SKILL.md`
## Learned Patterns
- 先按“属性数据获取 → 模拟结果获取 → 本地关联 → 多指标筛选 → 分级建议”拆解工作流,再组织展示步骤,避免把一次分析过程写成会话流水账。
- 结果集较大时,优先使用 `fetch_result_ref` 或本地脚本批处理;只要数据仍是 preview、截断或未完整回读,就不能直接输出 Top N 瓶颈结论。
- 关联前先统一关键字段和单位:`pipe_id/link_id``diameter(mm)``length(m)``flow(LPS)``pressure(KPA)`;字段未对齐时,后续 ranking 和建议都会失真。
- `unit_headloss``capacity_ratio` 等衍生指标应在过滤异常数据(如 `length < 0.5m` 的短管)后再计算,否则容易被极端值放大。
- 阈值和评分权重应视为可调启发式,而不是唯一真理;输出时要区分“数据直接支持的结论”和“工程经验推断的建议”。
- 地图定位、图表展示属于证据呈现层,不能替代分析层;瓶颈判定必须基于后端原始结果或完整回读数据。
- 常见坑点:短管导致单位水头损失虚高、节点或链路映射缺失导致误判、模拟结果不完整时误把局部结果当全量结论。
@@ -0,0 +1,150 @@
---
name: hydraulic-bottleneck-analysis
description: 基于实时水力数据的管网水力瓶颈识别与改造建议。复合评分法(流速×水头损失)定位瓶颈管段,输出分级改造方案。
---
# 水力瓶颈分析工作流
## 概述
本工作流通过复合评分法(流速分级 × 水头损失百分位)从全管网管道中识别水力瓶颈管段,并结合节点压力、管径、粗糙系数给出分级改造建议。
适用场景:管网运行评估、管网改造优先级排序、泵站阀站运行诊断。
## 评分方法论
### 双维度复合评分
| 维度 | 判定标准 | 分值 |
|------|----------|------|
| **流速** | >3.0 m/s = 极危 | 3 |
| | 2.03.0 m/s = 严重 | 2 |
| | 1.52.0 m/s = 偏高 | 1 |
| | <1.5 m/s = 正常 | 0 |
| **水头损失** | >P90 = 严重 | 2 |
| | P80P90 = 中度 | 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,需确保执行环境有足够内存。
@@ -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`
@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""
水力瓶颈管道综合分析
数据源管道属性 + 实时水力 + 节点压力 复合评分 改造建议
realtime links setting 字段为无效值已移除所有基于 setting 的判定
"""
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['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['id']; prop = pipe_map.get(pid)
if prop:
merged.append({**prop, **r, '_prop_id': prop['id'], '_rt_id': r['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,146 @@
---
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. 预期覆盖 **8590%** 节点
### 第三步:无向拓扑修正
有向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` 阈值过滤极低流量管段,若管网有长期小流量管段可能漏判方向
@@ -0,0 +1,205 @@
#!/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 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_links = [l for l in ldata if l["time"] == args.target_time]
flow_direction = {}
pipe_flow = {}
for l in target_links:
lid = l["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 n["time"] == args.target_time]
node_pressure = {}
node_demand = {}
for n in target_nodes:
nid = n["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()
+113
View File
@@ -0,0 +1,113 @@
import { tool } from "@opencode-ai/plugin";
export default tool({
description:
"在前端地图上对节点或管道图层应用样式,或重置为默认样式。样式参数应尽量与前端样式编辑器字段保持一致。",
args: {
reason: tool.schema
.string()
.describe(
"Why this style action is needed for the current user request.",
),
layer_id: tool.schema
.enum(["junctions", "pipes"])
.describe("Target layer id. Must be exactly 'junctions' or 'pipes'."),
reset_to_default: tool.schema
.boolean()
.optional()
.describe("Whether to reset the target layer to its default style."),
style_config: tool.schema
.object({
property: tool.schema
.string()
.optional()
.describe("Data property to render."),
classification_method: tool.schema
.enum(["pretty_breaks", "custom_breaks"])
.optional()
.describe("Classification method."),
segments: tool.schema
.number()
.int()
.min(2)
.max(10)
.optional()
.describe("Number of rendered intervals, from 2 to 10."),
min_size: tool.schema
.number()
.optional()
.describe("Minimum point radius."),
max_size: tool.schema
.number()
.optional()
.describe("Maximum point radius."),
min_stroke_width: tool.schema
.number()
.optional()
.describe("Minimum line width."),
max_stroke_width: tool.schema
.number()
.optional()
.describe("Maximum line width."),
fixed_stroke_width: tool.schema
.number()
.optional()
.describe("Fixed line width when width is not data-driven."),
color_type: tool.schema
.enum(["single", "gradient", "rainbow", "custom"])
.optional()
.describe("Color strategy."),
single_palette_index: tool.schema.number().int().min(0).max(6).optional(),
gradient_palette_index: tool.schema.number().int().min(0).max(2).optional(),
rainbow_palette_index: tool.schema.number().int().min(0).max(1).optional(),
show_labels: tool.schema
.boolean()
.optional()
.describe("Whether to show labels."),
show_id: tool.schema
.boolean()
.optional()
.describe("Whether to show ids."),
opacity: tool.schema.number().min(0).max(1).optional().describe("Opacity in [0, 1]."),
adjust_width_by_property: tool.schema
.boolean()
.optional()
.describe("Whether line width is driven by the rendered property."),
custom_breaks: tool.schema
.array(tool.schema.number())
.optional()
.describe("Strictly increasing boundaries. Length must equal segments + 1."),
custom_colors: tool.schema
.array(tool.schema.string())
.optional()
.describe("Custom CSS colors. Length must equal segments."),
})
.optional()
.describe(
"Optional style config overrides. Omit when reset_to_default is true.",
),
},
async execute(args) {
const layerLabel = args.layer_id === "junctions" ? "节点" : "管道";
if (args.reset_to_default) {
return `已提交${layerLabel}图层默认样式重置请求。`;
}
const style = args.style_config;
if (style?.custom_breaks) {
if (
style.custom_breaks.some(
(value, index, values) => index > 0 && value <= values[index - 1],
)
) {
throw new Error("custom_breaks must be strictly increasing");
}
if (style.segments && style.custom_breaks.length !== style.segments + 1) {
throw new Error("custom_breaks length must equal segments + 1");
}
}
if (style?.segments && style.custom_colors && style.custom_colors.length !== style.segments) {
throw new Error("custom_colors length must equal segments");
}
return `已提交${layerLabel}图层样式请求。`;
},
});
+53
View File
@@ -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;
},
});
-54
View File
@@ -1,54 +0,0 @@
import { tool } from "@opencode-ai/plugin";
import { ToolSessionContextStore } from "../../src/session/toolContextStore.js";
const internalBaseUrl = process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
const toolContextStore = new ToolSessionContextStore();
const initializePromise = toolContextStore.initialize();
export default tool({
description:
"通过本地 Agent 桥接调用 TJWater 后端 API。需提供 API 路径、可选的请求方法以及查询参数。",
args: {
reason: tool.schema
.string()
.describe("Why this tool call is required for the current user request."),
path: tool.schema.string().describe("Target backend API path, starting with '/'."),
method: tool.schema
.string()
.optional()
.describe("HTTP method. Defaults to GET."),
arguments: tool.schema
.record(tool.schema.string(), tool.schema.unknown())
.optional()
.describe("Query arguments object."),
},
async execute(args, context) {
await initializePromise;
const sessionContext = await toolContextStore.read(context.sessionID);
if (!sessionContext) {
throw new Error(`session context not found for ${context.sessionID}`);
}
// 工具本身不直接持有用户 token;通过 sessionID 回调 Agent 服务,由服务侧补齐用户上下文。
const response = await fetch(`${internalBaseUrl}/internal/tools/dynamic-http-call`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-agent-internal-token": internalToken,
},
body: JSON.stringify({
sessionScopeKey: sessionContext.sessionScopeKey,
reason: args.reason,
path: args.path,
method: args.method,
arguments: args.arguments,
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(text);
}
return text;
},
});
-49
View File
@@ -1,49 +0,0 @@
import { tool } from "@opencode-ai/plugin";
import { ToolSessionContextStore } from "../../src/session/toolContextStore.js";
const internalBaseUrl = process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
const toolContextStore = new ToolSessionContextStore();
const initializePromise = toolContextStore.initialize();
export default tool({
description:
"回读由 dynamic_http_call 生成的持久化 result_ref。适用于大结果只返回 preview 时,再按需读取完整或截断后的数据。",
args: {
reason: tool.schema
.string()
.describe("Why the stored result needs to be read for the current user request."),
result_ref: tool.schema.string().describe("The result_ref returned by dynamic_http_call."),
max_items: tool.schema
.number()
.int()
.positive()
.optional()
.describe("Optional maximum number of top-level items or fields to return."),
},
async execute(args, context) {
await initializePromise;
const sessionContext = await toolContextStore.read(context.sessionID);
if (!sessionContext) {
throw new Error(`session context not found for ${context.sessionID}`);
}
const response = await fetch(`${internalBaseUrl}/internal/tools/fetch-result-ref`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-agent-internal-token": internalToken,
},
body: JSON.stringify({
sessionScopeKey: sessionContext.sessionScopeKey,
result_ref: args.result_ref,
max_items: args.max_items,
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(text);
}
return text;
},
});
+37
View File
@@ -0,0 +1,37 @@
import { tool } from "@opencode-ai/plugin";
const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
export default tool({
description:
"调用 TJWater 后端的天地图地理编码服务,将中国境内结构化地址或地点名称转换为经纬度。若需缩放地图,把返回的 location.lon/location.lat 传给 zoom_to_map,并设置 source_crs='EPSG:4326'。",
args: {
reason: tool.schema
.string()
.describe("Why geocoding is required for the current user request."),
keyword: tool.schema
.string()
.describe("Address or place name to geocode, such as 北京市人民政府."),
},
async execute(args, context) {
const response = await fetch(`${internalBaseUrl}/internal/tools/geocode`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-agent-internal-token": internalToken,
},
body: JSON.stringify({
session_id: context.sessionID,
keyword: args.keyword,
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(text);
}
return text;
},
});
+6 -2
View File
@@ -5,8 +5,12 @@ export default tool({
args: { args: {
reason: tool.schema reason: tool.schema
.string() .string()
.describe("Why this map positioning action is needed for the user request."), .describe(
ids: tool.schema.array(tool.schema.string()).describe("Feature ids to locate."), "Why this map positioning action is needed for the user request.",
),
ids: tool.schema
.array(tool.schema.string())
.describe("Feature ids to locate."),
feature_type: tool.schema feature_type: tool.schema
.enum(["junction", "pipe", "valve", "reservoir", "pump", "tank"]) .enum(["junction", "pipe", "valve", "reservoir", "pump", "tank"])
.describe("Type of feature to locate."), .describe("Type of feature to locate."),
+23 -101
View File
@@ -1,17 +1,12 @@
import { tool } from "@opencode-ai/plugin"; import { tool } from "@opencode-ai/plugin";
import { MemoryStore } from "../../src/memory/store.js";
import { ToolSessionContextStore } from "../../src/session/toolContextStore.js";
const memoryStore = new MemoryStore(); const internalBaseUrl =
const toolContextStore = new ToolSessionContextStore(); process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const initializePromise = Promise.all([ const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
memoryStore.initialize(),
toolContextStore.initialize(),
]);
export default tool({ export default tool({
description: description:
"管理长期有效的用户偏好或项目事实。支持 add/list/replace/remove。禁止写入 token、password、secret、system prompt 或一次性上下文。scope 仅允许 'user' 或 'workspace'。", "管理长期有效的用户偏好或项目事实。支持 add/list/replace/remove。add 前必须先对同 scope 执行 list 并阅读现有记忆,再决定 add、replace 或 remove;不要跳过读取直接新增。禁止写入 token、password、secret、system prompt 或一次性上下文。scope 仅允许 'user' 或 'workspace'。",
args: { args: {
action: tool.schema action: tool.schema
.enum(["add", "list", "replace", "remove"]) .enum(["add", "list", "replace", "remove"])
@@ -27,104 +22,31 @@ export default tool({
content: tool.schema content: tool.schema
.string() .string()
.optional() .optional()
.describe( .describe("The durable fact or preference to remember, written as one concise sentence."),
"The durable fact or preference to remember, written as one concise sentence.",
),
target_id: tool.schema target_id: tool.schema
.string() .string()
.optional() .optional()
.describe("Stable memory entry id used by replace/remove."), .describe("Stable memory entry id used by replace/remove."),
}, },
async execute(args, context) { async execute(args, context) {
await initializePromise; const response = await fetch(
const sessionContext = await toolContextStore.read(context.sessionID); `${internalBaseUrl}/internal/tools/memory-manager`,
if (!sessionContext) { {
throw new Error(`session context not found for ${context.sessionID}`); method: "POST",
headers: {
"Content-Type": "application/json",
"x-agent-internal-token": internalToken,
},
body: JSON.stringify({
...args,
session_id: context.sessionID,
}),
},
);
const text = await response.text();
if (!response.ok) {
throw new Error(text);
} }
const scope = return text;
args.scope === "user"
? "user"
: args.scope === "workspace"
? "workspace"
: null;
if (!scope) {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: `unsupported scope: ${args.scope}; use exact keyword 'user' or 'workspace'`,
});
}
if (sessionContext.allowLearningWrite === false && args.action !== "list") {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: "memory writes are disabled for this session",
});
}
const scopeKey =
scope === "user" ? sessionContext.actorKey : sessionContext.projectKey;
if (args.action === "list") {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "accepted",
detail: "memory listed",
items: await memoryStore.list(scope, scopeKey),
target: scope,
});
}
if (args.action === "add") {
const result = await memoryStore.upsert(scope, scopeKey, {
content: args.content ?? "",
sessionId: sessionContext.clientSessionId,
source: "tool",
traceId: sessionContext.traceId,
});
if (!result.entry) {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: "content rejected by persistence policy",
});
}
return JSON.stringify({
ok: true,
kind: "memory",
decision: result.changed ? "accepted" : "deduped",
detail: result.changed ? "memory stored" : "memory already existed",
entry: result.entry,
target: scope,
});
}
if (args.action === "replace") {
const result = await memoryStore.replace(scope, scopeKey, args.target_id ?? "", {
content: args.content ?? "",
sessionId: sessionContext.clientSessionId,
source: "tool",
traceId: sessionContext.traceId,
});
return JSON.stringify({
ok: true,
kind: "memory",
decision: result.changed ? "accepted" : "rejected",
detail: result.detail,
target: scope,
});
}
const result = await memoryStore.remove(scope, scopeKey, args.target_id ?? "");
return JSON.stringify({
ok: true,
kind: "memory",
decision: result.changed ? "accepted" : "rejected",
detail: result.detail,
target: scope,
});
}, },
}); });
+5 -3
View File
@@ -2,15 +2,17 @@ import { tool } from "@opencode-ai/plugin";
export default tool({ export default tool({
description: description:
"在前端地图上对 junctions 图层应用分区渲染。优先直接传入 render_ref(指向已持久化的渲染结果引用,格式应为 res-...),也不要先把 ref 内容完整读出再重组;前端会自行根据 render_ref 拉取完整 payload 并渲染,这样可以避免 LLM 读取大型 node_area_map。若当前只有本地 JSON 文件,请先调用 store_render_ref 把它迁移为受控 render_ref。供 render_ref 引用的 JSON 结构必须为 { node_area_map: Record<string, string>, area_ids?: string[], area_colors?: Record<string, string> },其中 node_area_map 的 key 是 junction/node idvalue 是 area id。", "在前端地图上对 junctions 图层应用分区渲染。先把包装格式 { metadata, location: { file_path }, data: { node_area_map, area_ids?, area_colors? } } 写入 RESULT_REF_IMPORT_DIRlocation.file_path 必须等于文件绝对路径;再调用 store_render_ref 获得 res-... 引用,最后把引用传入本工具。不要读取并转传完整 ref 内容,也不要直接传本地文件路径。",
args: { args: {
reason: tool.schema reason: tool.schema
.string() .string()
.describe("Why this junction rendering action is needed for the user request."), .describe(
"Why this junction rendering action is needed for the user request.",
),
render_ref: tool.schema render_ref: tool.schema
.string() .string()
.describe( .describe(
"渲染引用 ID。必须是持久化结果引用res-...)。前端会按该引用取完整 payload.data 并渲染,不需要先用 fetch_result_ref 提取完整数据。render_ref 对应的数据结构必须是 { node_area_map: { [junctionId]: areaId }, area_ids?: string[], area_colors?: { [areaId]: color } }node_area_map 必填,area_ids / area_colors 可选。", "上一步通过 store_render_ref 获得的渲染引用 ID(res-...)。前端会按该引用取完整 payload 并渲染。不可直接传入本地文件路径或完整 JSON 数据。",
), ),
}, },
async execute() { async execute() {
+14 -19
View File
@@ -1,11 +1,8 @@
import { tool } from "@opencode-ai/plugin"; import { tool } from "@opencode-ai/plugin";
import { ToolSessionContextStore } from "../../src/session/toolContextStore.js";
const internalBaseUrl = const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
const toolContextStore = new ToolSessionContextStore();
const initializePromise = toolContextStore.initialize();
export default tool({ export default tool({
description: description:
@@ -25,23 +22,21 @@ export default tool({
.describe("Optional maximum number of hits to return."), .describe("Optional maximum number of hits to return."),
}, },
async execute(args, context) { async execute(args, context) {
await initializePromise; const response = await fetch(
const sessionContext = await toolContextStore.read(context.sessionID); `${internalBaseUrl}/internal/tools/session-search`,
if (!sessionContext) { {
throw new Error(`session context not found for ${context.sessionID}`); method: "POST",
} headers: {
const response = await fetch(`${internalBaseUrl}/internal/tools/session-search`, { "Content-Type": "application/json",
method: "POST", "x-agent-internal-token": internalToken,
headers: { },
"Content-Type": "application/json", body: JSON.stringify({
"x-agent-internal-token": internalToken, max_results: args.max_results,
query: args.query,
session_id: context.sessionID,
}),
}, },
body: JSON.stringify({ );
max_results: args.max_results,
query: args.query,
sessionScopeKey: sessionContext.sessionScopeKey,
}),
});
const text = await response.text(); const text = await response.text();
if (!response.ok) { if (!response.ok) {
throw new Error(text); throw new Error(text);
+16 -5
View File
@@ -1,7 +1,8 @@
import { tool } from "@opencode-ai/plugin"; import { tool } from "@opencode-ai/plugin";
export default tool({ export default tool({
description: "在前端对话界面中渲染图表。", description:
"在前端对话界面中渲染图表。折线图/柱状图必须使用 x_data 作为横轴标签,series[].data 作为同长度的一维数值数组,不要把折线数据写成 ECharts 的 [x, y] 二维点数组。",
args: { args: {
reason: tool.schema reason: tool.schema
.string() .string()
@@ -11,18 +12,28 @@ export default tool({
.enum(["line", "bar", "pie"]) .enum(["line", "bar", "pie"])
.optional() .optional()
.describe("Chart type."), .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 series: tool.schema
.array( .array(
tool.schema.object({ tool.schema.object({
name: tool.schema.string(), 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(), type: tool.schema.enum(["line", "bar"]).optional(),
}), }),
) )
.describe("Series data."), .describe("Series data."),
x_axis_name: tool.schema.string().optional().describe("X-axis display name."), x_axis_name: tool.schema
y_axis_name: tool.schema.string().optional().describe("Y-axis display name."), .string()
.optional()
.describe("X-axis display name."),
y_axis_name: tool.schema
.string()
.optional()
.describe("Y-axis display name."),
}, },
async execute() { async execute() {
// 图表数据已经在工具参数里,前端收到 tool_call 后直接渲染,不再二次请求后端。 // 图表数据已经在工具参数里,前端收到 tool_call 后直接渲染,不再二次请求后端。
+28 -76
View File
@@ -1,19 +1,18 @@
import { tool } from "@opencode-ai/plugin"; import { tool } from "@opencode-ai/plugin";
import { SkillStore } from "../../src/skills/store.js"; const internalBaseUrl =
import { ToolSessionContextStore } from "../../src/session/toolContextStore.js"; process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
const toolContextStore = new ToolSessionContextStore();
const initializePromise = toolContextStore.initialize();
const skillStore = new SkillStore();
export default tool({ export default tool({
description: description:
"维护已验证、可复用、非敏感的 workflow 或方法模式。支持 list、append_pattern、remove_pattern、write_reference、remove_reference、write_script、remove_script。", "维护已验证、可复用、非敏感的 workflow 或方法模式。支持 list、write_skill、remove_skill、append_pattern、remove_pattern、write_reference、remove_reference、write_script、remove_script。",
args: { args: {
action: tool.schema action: tool.schema
.enum([ .enum([
"list", "list",
"write_skill",
"remove_skill",
"append_pattern", "append_pattern",
"remove_pattern", "remove_pattern",
"write_reference", "write_reference",
@@ -24,18 +23,13 @@ export default tool({
.describe("Skill maintenance operation."), .describe("Skill maintenance operation."),
reason: tool.schema reason: tool.schema
.string() .string()
.describe( .describe("Why this skill maintenance action is justified for future reuse."),
"Why this skill maintenance action is justified for future reuse.",
),
skill_path: tool.schema skill_path: tool.schema
.string() .string()
.describe( .describe(
"Target skill directory path relative to .opencode/skills, for example analytics/simulation-analysis/leakage or platform/governance-observability/meta.", "Target skill directory path relative to .opencode/skills. Use 'workflow' for the workflow index, or '__root__' for the root skills index.",
), ),
pattern: tool.schema pattern: tool.schema.string().optional().describe("Pattern text used by append_pattern."),
.string()
.optional()
.describe("Pattern text used by append_pattern."),
target_id: tool.schema target_id: tool.schema
.string() .string()
.optional() .optional()
@@ -47,69 +41,27 @@ export default tool({
content: tool.schema content: tool.schema
.string() .string()
.optional() .optional()
.describe("Asset content used by write_reference or write_script."), .describe("Content used by write_skill, write_reference, or write_script."),
}, },
async execute(args, context) { async execute(args, context) {
await initializePromise; const response = await fetch(
const sessionContext = await toolContextStore.read(context.sessionID); `${internalBaseUrl}/internal/tools/skill-manager`,
if (!sessionContext) { {
throw new Error(`session context not found for ${context.sessionID}`); method: "POST",
headers: {
"Content-Type": "application/json",
"x-agent-internal-token": internalToken,
},
body: JSON.stringify({
...args,
session_id: context.sessionID,
}),
},
);
const text = await response.text();
if (!response.ok) {
throw new Error(text);
} }
if (sessionContext.allowLearningWrite === false && args.action !== "list") { return text;
return JSON.stringify({
ok: true,
kind: "skill",
decision: "rejected",
detail: "skill writes are disabled for this session",
});
}
if (args.action === "list") {
const result = await skillStore.list(args.skill_path);
if (!result) {
return JSON.stringify({
ok: true,
kind: "skill",
decision: "rejected",
detail:
"invalid skill_path; expected a relative path under .opencode/skills",
});
}
return JSON.stringify({
ok: true,
kind: "skill",
decision: "accepted",
detail: "skill listed",
...result,
});
}
const result =
args.action === "append_pattern"
? await skillStore.appendPattern(args.skill_path, args.pattern ?? "")
: args.action === "remove_pattern"
? await skillStore.removePattern(args.skill_path, args.target_id ?? "")
: args.action === "write_reference"
? await skillStore.writeReference(
args.skill_path,
args.file_path ?? "",
args.content ?? "",
)
: args.action === "remove_reference"
? await skillStore.removeReference(args.skill_path, args.file_path ?? "")
: args.action === "write_script"
? await skillStore.writeScript(
args.skill_path,
args.file_path ?? "",
args.content ?? "",
)
: await skillStore.removeScript(args.skill_path, args.file_path ?? "");
return JSON.stringify({
ok: true,
kind: "skill",
decision: result.changed ? "accepted" : "rejected",
detail: result.detail,
target: result.target,
});
}, },
}); });
+41 -22
View File
@@ -1,41 +1,60 @@
import { tool } from "@opencode-ai/plugin"; import { tool } from "@opencode-ai/plugin";
import { ToolSessionContextStore } from "../../src/session/toolContextStore.js";
const internalBaseUrl = process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
const toolContextStore = new ToolSessionContextStore();
const initializePromise = toolContextStore.initialize(); 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({ export default tool({
description: description:
"把本地 JSON 渲染文件迁移成受控的 render_ref。仅适用于需要通过链接引用传递的大型 junction render payload。", "导入当前对话工作目录下的受控 JSON 包装文件并返回 render_ref。文件必须是 { metadata: object, location: { file_path: string }, data: { node_area_map, area_ids?, area_colors? } }location.file_path 必须与传入的绝对路径完全一致。只接受当前对话工作目录内的真实文件,不接受其他对话目录、目录外路径或指向目录外的符号链接。",
args: { args: {
reason: tool.schema reason: tool.schema
.string() .string()
.describe("Why this local render payload should be persisted as a render_ref."), .describe(
"为何需要将此本地渲染数据持久化为 render_ref,以便后续通过 render_junctions 渲染到前端。",
),
file_path: tool.schema file_path: tool.schema
.string() .string()
.optional()
.describe( .describe(
"Absolute path to a local JSON file containing the raw render payload, or a wrapper object with data, metadata, and location. If wrapper metadata/location is missing or stale, the resolver will normalize and write it back before storing the render_ref.", "位于当前对话工作目录内的包装 JSON 文件绝对路径。必须包含 metadatalocation.file_path 和 datadata 才是 render_junctions 使用的 { node_area_map, area_ids?, area_colors? }。",
), ),
filePath: tool.schema
.string()
.optional()
.describe("兼容旧调用的参数名;新调用应优先使用 file_path。"),
}, },
async execute(args, context) { async execute(args, context) {
await initializePromise; const filePath = resolveStoreRenderFilePath(args);
const sessionContext = await toolContextStore.read(context.sessionID); const response = await fetch(
if (!sessionContext) { `${internalBaseUrl}/internal/tools/store-render-ref`,
throw new Error(`session context not found for ${context.sessionID}`); {
} method: "POST",
const response = await fetch(`${internalBaseUrl}/internal/tools/store-render-ref`, { headers: {
method: "POST", "Content-Type": "application/json",
headers: { "x-agent-internal-token": internalToken,
"Content-Type": "application/json", },
"x-agent-internal-token": internalToken, body: JSON.stringify({
session_id: context.sessionID,
file_path: filePath,
}),
}, },
body: JSON.stringify({ );
sessionScopeKey: sessionContext.sessionScopeKey,
file_path: args.file_path,
}),
});
const text = await response.text(); const text = await response.text();
if (!response.ok) { if (!response.ok) {
+55
View File
@@ -0,0 +1,55 @@
import { tool } from "@opencode-ai/plugin";
const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
export default tool({
description:
"通过本地 Agent 桥接调用 tjwater-cli 命令访问 TJWater 后端服务。提供 CLI 子命令和参数。",
args: {
reason: tool.schema
.string()
.describe("Why this tool call is required for the current user request."),
command: tool.schema
.string()
.describe(
"tjwater-cli 子命令,不含二进制路径。示例:'data scheme list'、'data timeseries realtime links --start-time 2025-01-01T00:00:00+08:00 --end-time 2025-01-01T01:00:00+08:00'",
),
timeout: tool.schema
.number()
.optional()
.describe("超时秒数,默认 120。大结果集建议设 300+。"),
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,
reason: args.reason,
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;
},
});
+11 -3
View File
@@ -5,15 +5,23 @@ export default tool({
args: { args: {
reason: tool.schema reason: tool.schema
.string() .string()
.describe("Why this history panel should be opened for the current task."), .describe(
"Why this history panel should be opened for the current task.",
),
feature_infos: tool.schema feature_infos: tool.schema
.array(tool.schema.tuple([tool.schema.string(), tool.schema.string()])) .array(tool.schema.tuple([tool.schema.string(), tool.schema.string()]))
.describe("List of [id, type] pairs."), .describe("List of [id, type] pairs."),
data_type: tool.schema data_type: tool.schema
.enum(["realtime", "scheme", "none"]) .enum(["realtime", "scheme", "none"])
.describe("History data source type."), .describe("History data source type."),
start_time: tool.schema.string().optional().describe("Optional ISO8601 start time."), start_time: tool.schema
end_time: tool.schema.string().optional().describe("Optional ISO8601 end time."), .string()
.optional()
.describe("Optional ISO8601 start time."),
end_time: tool.schema
.string()
.optional()
.describe("Optional ISO8601 end time."),
}, },
async execute() { async execute() {
// 返回短确认即可;面板打开动作由前端根据 tool_call 参数完成。 // 返回短确认即可;面板打开动作由前端根据 tool_call 参数完成。
+11 -6
View File
@@ -10,13 +10,18 @@ export default tool({
.array(tool.schema.string()) .array(tool.schema.string())
.optional() .optional()
.describe("Preferred SCADA device ids."), .describe("Preferred SCADA device ids."),
device_id: tool.schema.string().optional().describe("Single SCADA device id."), device_id: tool.schema
feature_infos: tool.schema .string()
.array(tool.schema.tuple([tool.schema.string(), tool.schema.string()]))
.optional() .optional()
.describe("Legacy [id, type] pairs."), .describe("Single SCADA device id."),
start_time: tool.schema.string().optional().describe("Optional ISO8601 start time."), start_time: tool.schema
end_time: tool.schema.string().optional().describe("Optional ISO8601 end time."), .string()
.optional()
.describe("Optional ISO8601 start time."),
end_time: tool.schema
.string()
.optional()
.describe("Optional ISO8601 end time."),
}, },
async execute() { async execute() {
// SCADA 面板仍在浏览器侧执行,工具结果不承载实际监测数据。 // SCADA 面板仍在浏览器侧执行,工具结果不承载实际监测数据。
+62
View File
@@ -0,0 +1,62 @@
import { tool } from "@opencode-ai/plugin";
const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
export default tool({
description:
"调用 TJWater 后端的实时网页搜索服务。适合查询新闻、政策、规范、产品资料、公开网页事实等可能变化的信息。",
args: {
reason: tool.schema
.string()
.describe("Why web search is required for the current user request."),
query: tool.schema.string().describe("Search query text."),
freshness: tool.schema
.enum(["no_limit", "one_day", "one_week", "one_month", "one_year"])
.optional()
.describe("Optional freshness filter. Defaults to no_limit."),
summary: tool.schema
.boolean()
.optional()
.describe("Whether the backend should include page summaries."),
count: tool.schema
.number()
.int()
.positive()
.optional()
.describe("Optional result count, backend accepts 1 to 50."),
include: tool.schema
.array(tool.schema.string())
.optional()
.describe("Optional domains to include."),
exclude: tool.schema
.array(tool.schema.string())
.optional()
.describe("Optional domains to exclude."),
},
async execute(args, context) {
const response = await fetch(`${internalBaseUrl}/internal/tools/web-search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-agent-internal-token": internalToken,
},
body: JSON.stringify({
session_id: context.sessionID,
query: args.query,
freshness: args.freshness,
summary: args.summary,
count: args.count,
include: args.include,
exclude: args.exclude,
}),
});
const text = await response.text();
if (!response.ok) {
throw new Error(text);
}
return text;
},
});
+32
View File
@@ -0,0 +1,32 @@
import { tool } from "@opencode-ai/plugin";
export default tool({
description:
"在前端地图上缩放定位到坐标。默认坐标为 EPSG:3857;如果来自天地图 geocode 的 lon/lat,传 source_crs='EPSG:4326',前端会转换为 EPSG:3857 后缩放。",
args: {
reason: tool.schema
.string()
.describe("Why this map zoom action is needed for the current request."),
x: tool.schema
.number()
.describe("X coordinate. For EPSG:4326 this is longitude; for EPSG:3857 this is meters."),
y: tool.schema
.number()
.describe("Y coordinate. For EPSG:4326 this is latitude; for EPSG:3857 this is meters."),
source_crs: tool.schema
.enum(["EPSG:3857", "EPSG:4326"])
.optional()
.describe("Input coordinate CRS. Defaults to EPSG:3857."),
zoom: tool.schema
.number()
.optional()
.describe("Optional OpenLayers zoom level. Defaults to 18."),
duration_ms: tool.schema
.number()
.optional()
.describe("Optional animation duration in milliseconds. Defaults to 1000."),
},
async execute() {
return "已缩放到指定地图坐标。";
},
});
+41
View File
@@ -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.
+36 -19
View File
@@ -1,8 +1,6 @@
FROM oven/bun:canary-slim AS bun-bin FROM smanx/opencode:1.18.13@sha256:b976acda21efffacd44abd7847dac7d646910dbaa477d1877e2881b39cf22a91 AS base
FROM smanx/opencode:latest AS base
USER root USER root
ARG UBUNTU_APT_MIRROR=mirrors.aliyun.com ARG UBUNTU_APT_MIRROR=
ARG PYPI_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple ARG PYPI_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
ARG PYPI_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn ARG PYPI_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
ENV VIRTUAL_ENV=/opt/venv ENV VIRTUAL_ENV=/opt/venv
@@ -11,13 +9,23 @@ ENV PIP_INDEX_URL=${PYPI_INDEX_URL}
ENV PIP_TRUSTED_HOST=${PYPI_TRUSTED_HOST} ENV PIP_TRUSTED_HOST=${PYPI_TRUSTED_HOST}
ENV UV_INDEX_URL=${PYPI_INDEX_URL} ENV UV_INDEX_URL=${PYPI_INDEX_URL}
RUN sed -i "s|http://archive.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g; s|http://security.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g" /etc/apt/sources.list 2>/dev/null || true && \ COPY vendor/bun-linux-x64.zip /tmp/bun.zip
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 && \
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 \ apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
jq \
libseccomp2 \
unzip \ unzip \
python3 \ python3 \
python3-venv && \ 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 && \ curl -LsSf https://astral.sh/uv/install.sh | sh && \
ln -s /root/.local/bin/uv /usr/local/bin/uv && \ ln -s /root/.local/bin/uv /usr/local/bin/uv && \
ln -sf /usr/bin/python3 /usr/local/bin/python && \ ln -sf /usr/bin/python3 /usr/local/bin/python && \
@@ -36,10 +44,10 @@ RUN sed -i "s|http://archive.ubuntu.com|https://${UBUNTU_APT_MIRROR}|g; s|http:/
rich \ rich \
ipython \ ipython \
pytest && \ 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/* rm -rf /var/lib/apt/lists/*
COPY --from=bun-bin /usr/local/bin/bun /usr/local/bin/bun
FROM base AS deps FROM base AS deps
WORKDIR /app WORKDIR /app
@@ -53,30 +61,39 @@ WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/.opencode/node_modules ./.opencode/node_modules COPY --from=deps /app/.opencode/node_modules ./.opencode/node_modules
COPY package.json bun.lock tsconfig.json opencode.json README.md ./ COPY package.json bun.lock ./
COPY tsconfig.json opencode.json README.md .gitignore Dockerfile ./
COPY src ./src COPY src ./src
COPY cli ./cli
COPY scripts ./scripts
COPY .opencode ./.opencode COPY .opencode ./.opencode
RUN bun run check RUN bun run check
FROM base AS runner FROM build AS test
RUN apt-get update && apt-get install -y --no-install-recommends nodejs && \
rm -rf /var/lib/apt/lists/*
COPY contracts ./contracts
COPY node-tests ./node-tests
COPY scripts ./scripts
COPY tests ./tests
RUN bun run test:ci
FROM build AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV PORT=8787 ENV PORT=8787
ENV OPENCODE_HOST=127.0.0.1
COPY --from=deps /app/node_modules ./node_modules ENV OPENCODE_HOSTNAME=127.0.0.1
COPY --from=deps /app/.opencode/node_modules ./.opencode/node_modules ENV TJWATER_CLI_PATH=./cli/tjwater-cli
COPY package.json bun.lock ./
COPY tsconfig.json opencode.json ./
COPY src ./src
COPY .opencode ./.opencode
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh ./cli/tjwater-cli
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
COPY .opencode ./.opencode
EXPOSE 8787 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"] CMD ["bun", "src/server.ts"]
+83 -278
View File
@@ -1,312 +1,117 @@
# 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 ```text
TJWaterAgent/ src/ 服务端 TypeScript 源码
package.json src/routes/ HTTP 路由
tsconfig.json src/chat/ 聊天流和 SSE 事件适配
src/ src/runtime/ OpenCode 运行时管理
opencode.json src/session/ 会话映射和运行上下文
.opencode/ src/mcp/ MCP 服务与工具桥接
agents/ cli/ Agent 使用的 TypeScript 后端 API CLI
tools/ .opencode/agents/ Agent prompt 和模型行为配置
skills/ .opencode/tools/ OpenCode 自定义工具
package.json .opencode/skills/ 可复用分析工作流
tsconfig.json node-tests/ Node CLI 测试
data/ 本地运行时数据,禁止提交
logs/ 本地日志,禁止提交
``` ```
| 位置 | 主要作用 | 典型内容 | 仓库跟踪 `.opencode/skills/` 中经过评审的默认工作流基线;部署环境仍可通过持久化卷保留 `skill_manager` 在运行中沉淀的增量内容。默认基线不得包含真实客户数据、认证信息或本地执行产物。
| --- | --- | --- |
| `TJWaterAgent/` | 服务宿主、API 层和编排层 | Express 服务、SSE 接口、会话管理、鉴权上下文、后端 API 代理、opencode SDK 启动逻辑 |
| `TJWaterAgent/.opencode/` | opencode 项目资产目录 | agent prompt、自定义 tools、skills 树、plugins 相关依赖 |
## `TJWaterAgent/` 根目录的职责 ## 本地开发
根目录是 Node/TypeScript 服务本体,主要负责 项目使用 Bun
1. 启动 HTTP 服务。
2. 通过 `@opencode-ai/sdk` 启动内嵌 opencode server,或连接外部 opencode server。
3. 管理前端 `session_id -> opencode sessionId` 的映射。
4. 保存并传递用户 `Authorization``x-user-id``x-project-id``x-trace-id`
5. 把 opencode 输出适配成前端需要的 SSE 事件。
6. 为 `.opencode/tools/dynamic_http_call.ts` 提供内部回调接口。
7. 代理调用真实 TJWater 后端 API。
当前 Agent API 的主入口:
```text
POST /api/v1/agent/chat/stream
```
该接口返回 SSE,事件包括:
| event | 用途 |
| --- | --- |
| `progress` | 前端过程可视化,展示规划、工具调用和完成状态 |
| `token` | 最终回答文本流 |
| `tool_call` | 前端地图/面板/图表动作 |
| `done` | 当前轮完成 |
| `error` | 当前轮失败 |
主要目录和文件:
```text
src/
server.ts
config.ts
runtime/
session/
chat/
routes/
tools/
```
其中 `src/` 是业务服务层,不直接放 opencode skill 或 agent prompt。
## `.opencode/` 的职责
`.opencode/` 是给 opencode 运行时读取的项目资产目录,不是对外 HTTP 服务的主代码目录。
### agents
```text
.opencode/agents/agent.md
```
这里定义默认 agent 的角色、行为规则、模型配置和工具使用策略。
当前项目已将 always-loaded instructions 收敛到 `agent.md``opencode.json` 不再额外配置 `instructions` 数组。
### tools
```text
.opencode/tools/
dynamic_http_call.ts
locate_features.ts
view_history.ts
view_scada.ts
show_chart.ts
```
这些是 opencode 可以调用的自定义工具。
`dynamic_http_call.ts` 不直接保存用户 token,也不直接访问后端。它会回调 `TJWaterAgent` 的内部接口,由上级服务层根据当前 session 补上用户 token、项目 ID 和 trace ID,再调用 TJWater 后端。
前端类工具如 `locate_features``view_history``view_scada``show_chart` 主要用于触发 UI 动作或可视化,不应被当作数据查询工具。
### skills
```text
.opencode/skills/tjwater-skills-root-index/
SKILL.md
ai/
analytics/
business/
data/
platform/
```
这里保存 TJWater 技能树,并保持树结构,符合渐进式披露设计。
agent 需要某个领域知识时再按需加载对应 skill,不把整棵技能树作为 always-loaded prompt 一次性注入。
## 依赖边界
根目录和 `.opencode/` 使用两组 npm 依赖,职责不同。
### 根目录依赖
```text
TJWaterAgent/package.json
```
用于服务本体,例如:
```text
@opencode-ai/sdk
express
zod
pino
```
### `.opencode` 依赖
```text
TJWaterAgent/.opencode/package.json
```
用于 opencode 自定义 tools/plugins,例如:
```text
@opencode-ai/plugin
typescript
@types/node
```
这两组依赖不要混在一起:根目录负责服务运行,`.opencode` 负责 opencode 扩展资产的类型检查和运行依赖。
## 启动与部署
支持两种 opencode 接入方式:
1. Embedded 模式:服务通过 `@opencode-ai/sdk` 调用 `createOpencode`,启动本地 `opencode` CLI 子进程并自动创建 client。
2. Client 模式:服务通过 `createOpencodeClient` 直接连接一个已经存在的 opencode server。
因此,只有 Embedded 模式要求运行环境已安装 `opencode` CLIClient 模式不依赖本地 CLI。
根目录的 Bun scripts 已经封装 `.opencode` 依赖安装和类型检查,日常只需要在 `TJWaterAgent/` 根目录操作。
### 本地开发
```bash ```bash
cd TJWaterAgent
bun install bun install
bun run dev 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/** ```bash
opencode.json bun run check
.local.env bun run contract:generate
bun run test:api
bun run test:cli
bun run start
bun run start:prod
docker build -t tjwater-agent:local .
``` ```
因此修改 agent prompt、tools、skills、模型配置或本地环境变量后,不需要手动重启 `bun run dev` - `bun run check`:检查主项目和 `.opencode` 的 TypeScript 类型
- `bun run contract:generate`:生成 `contracts/agent-v1.openapi.json`
- `bun run test:api`:验证公开 REST 契约和聊天路由。
- `bun run test:cli`:运行 `node-tests/cli/*.node.mjs`
- `bun run start`:直接启动服务。
- `bun run start:prod`:先类型检查,再启动服务。
本地开发可以在项目根目录的 `.local.env` 中配置环境变量。 ## 运行模式
Embedded 模式示例 当前运行时使用 OpenCode 稳定版 1.x CLI,并通过稳定版 SDK 的 `@opencode-ai/sdk/v2` HTTP 客户端访问运行时;这与 `opencode2``@opencode-ai/client` 的 2.0 beta 运行时不同。Embedded 模式由服务进程拉起本机 OpenCode
```bash ```bash
OPENCODE_MODE=embedded OPENCODE_MODE=embedded
DEEPSEEK_API_KEY=sk-xxx
TJWATER_API_BASE_URL=http://127.0.0.1:8000 TJWATER_API_BASE_URL=http://127.0.0.1:8000
``` ```
Client 模式示例: 当前仅支持 Embedded 模式,不支持连接外部 OpenCode server。
## 认证续期与学习工具
后端工具调用遇到即将过期的 access token 或首次 `401` 时,Agent 会通过当前 SSE 流发送 `credential_refresh_required`。前端使用服务端保存的 Keycloak refresh token 强制换取新 access token,再调用 `POST /api/v1/agent/sessions/{session_id}/credential-refreshes` 唤醒原工具调用。等待上限为 30 秒,同一会话的并发请求合并为一次续期,原调用最多重试一次;`403` 不触发续期。
`memory_manager``skill_manager` 在 OpenCode 侧只保留内部 HTTP 桥,读取会话上下文和持久化数据的逻辑统一在 Agent 主进程中执行。长期记忆、自动学习和显式工具写入因此共享同一组 `MemoryStore``SkillStore` 和运行时会话上下文。
本地可使用 `.local.env` 保存开发配置;系统环境变量优先级更高。
服务会在 HTTP 端口开始监听前完成 OpenCode 健康检查、临时会话创建和工具目录加载。`GET /health` 返回 `ready: true``warmed_up: true` 时,表示冷启动预热已经完成。开发环境会输出各预热阶段的耗时。
`opencode.json` 已启用 `experimental.continue_loop_on_deny`。用户拒绝权限请求后,OpenCode V1 会把拒绝结果交还给 Agent,让其尝试无需该权限的替代方案,而不是直接结束本轮执行。
前端提供三种整体权限模式:“请求批准”只执行 OpenCode 明确允许的白名单,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 ```bash
OPENCODE_MODE=client
OPENCODE_CLIENT_BASE_URL=http://127.0.0.1:4096
DEEPSEEK_API_KEY=sk-xxx
TJWATER_API_BASE_URL=http://127.0.0.1:8000
```
服务启动时会自动读取 `.local.env`,但系统环境变量优先级更高,适合在本机保存开发用 key。
### 生产启动
```bash
cd TJWaterAgent
bun install
bun run check bun run check
bun run start
``` ```
也可以使用一条命令完成构建并启动 如修改 CLI 或工具调用逻辑,同时运行
```bash ```bash
cd TJWaterAgent bun run test:cli
bun install
bun run start:prod
``` ```
### Docker Compose 启动
项目根目录已提供 `Dockerfile``docker-compose.yml`,可直接使用:
```bash
cd TJWaterAgent
docker compose up -d --build
```
查看日志:
```bash
docker compose logs -f tjwater-agent
```
停止并清理容器:
```bash
docker compose down
```
### 常用脚本
| 命令 | 作用 |
| --- | --- |
| `bun run dev` | 类型检查 `.opencode` tools 后,以 watch 模式直接运行 `src/server.ts` |
| `bun run check` | 执行完整类型检查(服务与 `.opencode` tools |
| `bun run start` | 直接运行 `src/server.ts` |
| `bun run start:prod` | 先类型检查再启动 |
| `bun run install:opencode` | 手动安装 `.opencode` 依赖 |
| `bun run pipeline:trigger` | 通过重建并强推 annotated `latest` tag 触发 Gitea CI/CD,只发布/覆盖 `latest` 镜像 |
### 模型与 API 配置
默认 Agent 模型为:
```text
deepseek/deepseek-v4-pro
```
涉及位置:
```text
opencode.json
.opencode/agents/tjwater-assistant.md
src/config.ts 的 OPENCODE_MODEL 默认值
```
如果需要临时覆盖模型,可以在启动时设置:
```bash
OPENCODE_MODEL=deepseek/deepseek-v4-pro bun run start
```
DeepSeek API key 不写入代码,部署时通过环境变量设置:
```bash
DEEPSEEK_API_KEY=sk-xxx bun run start
```
`opencode.json` 已配置从环境变量读取:
```json
{
"provider": {
"deepseek": {
"options": {
"apiKey": "{env:DEEPSEEK_API_KEY}"
}
}
}
}
```
如果需要自定义 DeepSeek 兼容 API 地址,可以通过 opencode 的 provider 配置增加 `baseURL`,例如在部署环境使用 `OPENCODE_CONFIG_CONTENT` 覆盖:
```bash
OPENCODE_CONFIG_CONTENT='{"provider":{"deepseek":{"options":{"baseURL":"https://your-api.example.com/v1"}}}}' \
DEEPSEEK_API_KEY=sk-xxx \
bun run start
```
也可以使用 opencode 的 `/connect` 命令写入用户级凭据,但服务部署更推荐使用环境变量。
如果需要连接外部独立运行的 opencode server,可以配置:
```bash
OPENCODE_MODE=client
OPENCODE_CLIENT_BASE_URL=http://127.0.0.1:4096
```
配置后,`TJWaterAgent` 会连接该外部 opencode server,而不是自行启动 embedded opencode server。
+9 -2
View File
@@ -5,7 +5,7 @@
"": { "": {
"name": "tjwater-agent", "name": "tjwater-agent",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "^1.14.29", "@opencode-ai/sdk": "1.18.13",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^4.21.2", "express": "^4.21.2",
@@ -14,6 +14,7 @@
"zod": "^3.25.76", "zod": "^3.25.76",
}, },
"devDependencies": { "devDependencies": {
"@asteasolutions/zod-to-openapi": "7.3.4",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@types/node": "^24.7.2", "@types/node": "^24.7.2",
@@ -23,7 +24,9 @@
}, },
}, },
"packages": { "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=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
@@ -175,6 +178,8 @@
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "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=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -259,6 +264,8 @@
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "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=="], "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=="], "body-parser/qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
+189
View File
@@ -0,0 +1,189 @@
import { CliError } from "../core/errors.js";
import { emitApi, requestJson } from "../core/http.js";
import { assignDatasetKeys, parseBurstFile, parseValveSettingFile } from "../core/files.js";
import { optionalNumber, optionalString, optionalStringArray, parseOptions, requiredNumber, requiredString, validateChoice } from "../core/options.js";
import { resolveScheme } from "../core/runtime.js";
import { parseTime } from "../core/time.js";
import { success } from "../core/output.js";
import type { HandlerMap, RuntimeContext } from "../core/types.js";
function analysisBurst(ctx: RuntimeContext, argv: string[]): Promise<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 data scheme get --name ${schemeName}`, "tjwater-cli data scheme list"]);
}
function analysisValve(ctx: RuntimeContext, argv: string[]): Promise<void> {
const { values } = parseOptions(argv, { valve: "repeat", element: "repeat", "disabled-valve": "repeat", duration: "integer" });
const mode = validateChoice(requiredString(values, "mode"), ["close", "isolation"] as const, "--mode");
if (mode === "close") {
const valves = optionalStringArray(values, "valve");
const startTime = optionalString(values, "start-time");
if (!startTime || !valves) throw new CliError("CLI 参数错误", "INVALID_VALVE_CLOSE_ARGS", "close mode requires --start-time and at least one --valve", 2);
return emitApi(ctx, "阀门关闭分析执行成功", {
method: "POST",
path: "/valve-isolation-analyses",
params: {
start_time: parseTime(startTime, "--start-time"),
valves,
duration: optionalNumber(values, "duration") || 900,
scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true),
},
requireProject: true,
});
}
const elements = optionalStringArray(values, "element");
if (!elements) throw new CliError("CLI 参数错误", "INVALID_VALVE_ISOLATION_ARGS", "isolation mode requires at least one --element", 2);
return emitApi(ctx, "阀门隔离分析执行成功", {
method: "POST",
path: "/valve-isolation-analyses",
params: { accident_element: elements, disabled_valves: optionalStringArray(values, "disabled-valve") },
requireProject: true,
});
}
function analysisFlushing(ctx: RuntimeContext, argv: string[]): Promise<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 sensorKmeans(ctx: RuntimeContext, argv: string[]): Promise<void> {
const { values } = parseOptions(argv, { count: "integer", "min-diameter": "integer" });
return emitApi(ctx, "传感器选址执行成功", {
method: "POST",
path: "/pressure-sensor-placement-kmeans",
body: {
scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true),
sensor_number: requiredNumber(values, "count"),
min_diameter: optionalNumber(values, "min-diameter") || 0,
},
requireProject: true,
});
}
function schemeAnalysis(ctx: RuntimeContext, argv: string[], summary: string, path: string, startKey: string, endKey: string): Promise<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 schemeList(ctx: RuntimeContext, summary: string, schemeType: string): Promise<void> {
return emitApi(ctx, summary, { method: "GET", path: "/schemes", params: { scheme_type: schemeType }, requireProject: true });
}
function schemeGet(ctx: RuntimeContext, argv: string[], summary: string, schemeType: string): Promise<void> {
const { positionals } = parseOptions(argv);
if (!positionals[0]) throw new CliError("CLI 参数错误", "MISSING_ARGUMENT", "Missing argument 'SCHEME_NAME'", 2);
return emitApi(ctx, summary, { method: "GET", path: `/schemes/${positionals[0]}`, params: { scheme_type: schemeType }, requireProject: true });
}
function burstLocation(ctx: RuntimeContext, argv: string[]): Promise<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 });
}
function riskPipe(ctx: RuntimeContext, argv: string[], summary: string, path: string): Promise<void> {
const { values } = parseOptions(argv);
return emitApi(ctx, summary, { method: "GET", path, params: { pipe_id: requiredString(values, "pipe") }, requireProject: true });
}
async function riskNetwork(ctx: RuntimeContext): Promise<void> {
const [probabilities, a] = await requestJson(ctx, { method: "GET", path: "/network-pipe-risk-probability-nows", requireProject: true });
const [geometries, b] = await requestJson(ctx, { method: "GET", path: "/pipes/risk-probability-geometries", requireProject: true });
success("读取全网风险成功", { probabilities, geometries }, ctx, a + b);
}
export const analysisHandlers: HandlerMap = {
"analysis burst": analysisBurst,
"analysis valve": analysisValve,
"analysis flushing": analysisFlushing,
"analysis age": analysisAge,
"analysis contaminant": analysisContaminant,
"analysis sensor-placement kmeans": sensorKmeans,
"analysis leakage identify": (ctx, argv) => schemeAnalysis(ctx, argv, "漏损识别执行成功", "/leakage-identifications", "scada_start", "scada_end"),
"analysis leakage schemes list": (ctx) => schemeList(ctx, "读取漏损方案列表成功", "dma_leak_identification"),
"analysis leakage schemes get": (ctx, argv) => schemeGet(ctx, argv, "读取漏损方案详情成功", "dma_leak_identification"),
"analysis burst-detection detect": (ctx, argv) => schemeAnalysis(ctx, argv, "爆管检测执行成功", "/burst-detections", "scada_start", "scada_end"),
"analysis burst-detection schemes list": (ctx) => schemeList(ctx, "读取爆管检测方案列表成功", "burst_detection"),
"analysis burst-detection schemes get": (ctx, argv) => schemeGet(ctx, argv, "读取爆管检测方案详情成功", "burst_detection"),
"analysis burst-location locate": burstLocation,
"analysis burst-location schemes list": (ctx) => schemeList(ctx, "读取爆管定位方案列表成功", "burst_location"),
"analysis burst-location schemes get": (ctx, argv) => schemeGet(ctx, argv, "读取爆管定位方案详情成功", "burst_location"),
"analysis risk pipe-now": (ctx, argv) => riskPipe(ctx, argv, "读取当前管道风险成功", "/pipes/risk-probability-now"),
"analysis risk pipe-history": (ctx, argv) => riskPipe(ctx, argv, "读取历史管道风险成功", "/pipes/risk-probability"),
"analysis risk network": riskNetwork,
};
+33
View File
@@ -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),
};
+176
View File
@@ -0,0 +1,176 @@
import { SCADA_FIELDS, type ElementType } from "../core/constants.js";
import { CliError } from "../core/errors.js";
import { emitApi } from "../core/http.js";
import { fieldsFor, optionalString, parseOptions, requiredString, requiredStringArray, validateChoice } from "../core/options.js";
import { resolveScheme } from "../core/runtime.js";
import { parseTime } from "../core/time.js";
import type { HandlerMap, RuntimeContext } from "../core/types.js";
function backendElementType(type: ElementType): "link" | "node" {
return type === "pipe" ? "link" : "node";
}
function rangeGet(ctx: RuntimeContext, argv: string[], summary: string, path: string): Promise<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 schemeLinks(ctx: RuntimeContext, argv: string[]): Promise<void> {
const { values } = parseOptions(argv);
return emitApi(ctx, "读取方案管道数据成功", {
method: "GET",
path: "/timeseries/schemes/links",
params: {
scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true),
scheme_type: optionalString(values, "scheme-type") || "simulation",
start_time: parseTime(requiredString(values, "start-time"), "--start-time"),
end_time: parseTime(requiredString(values, "end-time"), "--end-time"),
},
requireProject: true,
});
}
function schemeNodeField(ctx: RuntimeContext, argv: string[]): Promise<void> {
const { values } = parseOptions(argv);
return emitApi(ctx, "读取方案节点字段成功", {
method: "GET",
path: `/timeseries/schemes/nodes/${requiredString(values, "node")}/field`,
params: {
field: validateChoice(requiredString(values, "field"), fieldsFor("junction"), "--field"),
scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true),
scheme_type: optionalString(values, "scheme-type") || "simulation",
start_time: parseTime(requiredString(values, "start-time"), "--start-time"),
end_time: parseTime(requiredString(values, "end-time"), "--end-time"),
},
requireProject: true,
});
}
function schemeSimulation(ctx: RuntimeContext, argv: string[]): Promise<void> {
const { values } = parseOptions(argv);
const query = validateChoice(requiredString(values, "query"), ["by-id-time", "by-scheme-time-property"] as const, "--query");
const type = validateChoice(optionalString(values, "type") || "pipe", ["pipe", "junction"] as const, "--type") as ElementType;
const params: Record<string, unknown> = {
scheme_name: resolveScheme(ctx, optionalString(values, "scheme"), true),
scheme_type: optionalString(values, "scheme-type") || "simulation",
query_time: parseTime(requiredString(values, "time"), "--time"),
type: backendElementType(type),
};
if (query === "by-id-time") {
params.id = requiredString(values, "id");
return emitApi(ctx, "读取方案单点模拟数据成功", { method: "GET", path: "/timeseries/schemes/simulation-results", params, requireProject: true });
}
params.property = validateChoice(requiredString(values, "property"), fieldsFor(type), "--property");
return emitApi(ctx, "读取方案属性聚合数据成功", { method: "GET", path: "/timeseries/schemes/records", params, requireProject: true });
}
function scadaQuery(ctx: RuntimeContext, argv: string[]): Promise<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 schemeName = resolveScheme(ctx, optionalString(values, "scheme"));
if (schemeName) Object.assign(params, { scheme_name: schemeName, scheme_type: optionalString(values, "scheme-type") || "simulation" });
if (kind === "scada-simulation") params.device_ids = requiredStringArray(values, "feature").join(",");
else if (kind === "element-simulation") params.feature_infos = requiredStringArray(values, "feature").join(",");
else {
const feature = requiredStringArray(values, "feature");
if (feature.length !== 1) throw new CliError("CLI 参数错误", "FEATURE_REQUIRED", "element-scada requires exactly one --feature as element_id", 2);
params.element_id = feature[0];
params.use_cleaned = Boolean(values["use-cleaned"]);
}
const paths = {
"scada-simulation": "/timeseries/views/scada-simulations",
"element-simulation": "/timeseries/views/element-simulations",
"element-scada": "/timeseries/views/element-scada-readings",
} as const;
return emitApi(ctx, kind === "scada-simulation" ? "读取复合 SCADA-模拟数据成功" : kind === "element-simulation" ? "读取复合元素模拟数据成功" : "读取元素关联 SCADA 数据成功", { method: "GET", path: paths[kind], params, requireProject: true });
}
function pipelineHealth(ctx: RuntimeContext, argv: string[]): Promise<void> {
const { values } = parseOptions(argv);
requiredString(values, "pipe");
requiredString(values, "start-time");
return emitApi(ctx, "读取管道健康预测成功", {
method: "GET",
path: "/pipeline-health-predictions",
params: { query_time: parseTime(requiredString(values, "end-time"), "--end-time") },
requireProject: true,
});
}
function dataScadaGet(ctx: RuntimeContext, argv: string[]): Promise<void> {
const { values } = parseOptions(argv);
validateChoice(requiredString(values, "kind"), ["info"] as const, "--kind");
return emitApi(ctx, "读取 SCADA 数据成功", { method: "GET", path: "/scada-info/detail", params: { id: requiredString(values, "id") }, requireProject: true });
}
function dataScadaList(ctx: RuntimeContext, argv: string[]): Promise<void> {
const { values } = parseOptions(argv);
validateChoice(requiredString(values, "kind"), ["info"] as const, "--kind");
return emitApi(ctx, "读取 SCADA 列表成功", { method: "GET", path: "/scada-info", requireProject: true });
}
function dataSchemeGet(ctx: RuntimeContext, argv: string[]): Promise<void> {
const { values } = parseOptions(argv);
return emitApi(ctx, "读取方案成功", { method: "GET", path: "/schemes/detail", params: { schema_name: requiredString(values, "name") }, requireProject: true });
}
export const dataHandlers: HandlerMap = {
"data timeseries realtime links": (ctx, argv) => rangeGet(ctx, argv, "读取实时管道数据成功", "/timeseries/realtime/links"),
"data timeseries realtime nodes": (ctx, argv) => rangeGet(ctx, argv, "读取实时节点数据成功", "/timeseries/realtime/nodes"),
"data timeseries realtime simulation-by-id-time": realtimeByIdTime,
"data timeseries realtime simulation-by-time-property": realtimeByTimeProperty,
"data timeseries scheme links": schemeLinks,
"data timeseries scheme node-field": schemeNodeField,
"data timeseries scheme simulation": schemeSimulation,
"data timeseries scada query": scadaQuery,
"data timeseries composite": composite,
"data timeseries composite pipeline-health": pipelineHealth,
"data scada get": dataScadaGet,
"data scada list": dataScadaList,
"data scheme schema": (ctx) => emitApi(ctx, "读取方案 schema 成功", { method: "GET", path: "/network-schemas/scheme", requireProject: true }),
"data scheme get": dataSchemeGet,
"data scheme list": (ctx) => emitApi(ctx, "读取方案列表成功", { method: "GET", path: "/schemes", requireProject: true }),
};
+35
View File
@@ -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"),
};
+24
View File
@@ -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,
};
+13
View File
@@ -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];
+31
View File
@@ -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);
}
+68
View File
@@ -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];
}
}
}
+185
View File
@@ -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);
}
+112
View File
@@ -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);
}
+61
View File
@@ -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,
});
}
+84
View File
@@ -0,0 +1,84 @@
import { randomUUID } from "node:crypto";
import { DEFAULT_SERVER, DEFAULT_TIMEOUT } from "./constants.js";
import { CliError } from "./errors.js";
import { requireValue, parseIntStrict } from "./options.js";
import type { AuthContext, GlobalArgs, ParsedGlobalArgs, RuntimeContext } from "./types.js";
function pick(source: Record<string, unknown>, ...keys: string[]): string | null {
for (const key of keys) {
const value = source[key];
if (value !== undefined && value !== null && value !== "") return String(value);
}
return null;
}
function readStdin(): Promise<string> {
return new Promise((resolve, reject) => {
let body = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk: string) => {
body += chunk;
});
process.stdin.on("end", () => resolve(body));
process.stdin.on("error", reject);
});
}
export async function loadAuthContext(authStdin: boolean): Promise<AuthContext> {
const raw = authStdin
? (JSON.parse(await readStdin()) as Record<string, unknown>)
: {
server: process.env.TJWATER_SERVER,
access_token: process.env.TJWATER_ACCESS_TOKEN,
project_id: process.env.TJWATER_PROJECT_ID,
headers: process.env.TJWATER_EXTRA_HEADERS ? JSON.parse(process.env.TJWATER_EXTRA_HEADERS) : {},
};
const headers = (raw.headers ?? {}) as unknown;
if (!headers || Array.isArray(headers) || typeof headers !== "object") {
throw new CliError("认证失败", "AUTH_CONTEXT_INVALID", "auth context headers must be a JSON object", 3);
}
return {
server: pick(raw, "server", "base_url"),
accessToken: pick(raw, "access_token", "token", "accessToken"),
projectId: pick(raw, "project_id", "projectId", "x_project_id"),
headers: Object.fromEntries(Object.entries(headers as Record<string, unknown>).map(([key, value]) => [String(key), String(value)])),
};
}
export function parseGlobalArgs(argv: string[]): ParsedGlobalArgs {
const globals: GlobalArgs = {
server: null,
authStdin: false,
scheme: null,
timeout: DEFAULT_TIMEOUT,
requestId: null,
};
const rest: string[] = [];
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i]!;
if (arg === "--auth-stdin") globals.authStdin = true;
else if (arg === "--server") globals.server = requireValue(argv, ++i, "--server");
else if (arg === "--scheme") globals.scheme = requireValue(argv, ++i, "--scheme");
else if (arg === "--timeout") globals.timeout = parseIntStrict(requireValue(argv, ++i, "--timeout"), "--timeout");
else if (arg === "--request-id") globals.requestId = requireValue(argv, ++i, "--request-id");
else rest.push(arg);
}
return { globals, rest };
}
export async function buildRuntime(globals: GlobalArgs): Promise<RuntimeContext> {
const auth = await loadAuthContext(globals.authStdin);
return {
server: globals.server || auth.server || DEFAULT_SERVER,
auth,
scheme: globals.scheme,
timeout: globals.timeout,
requestId: globals.requestId || randomUUID(),
};
}
export function resolveScheme(ctx: RuntimeContext, explicit: string | undefined, must = false): string | null {
const scheme = explicit || ctx.scheme;
if (must && !scheme) throw new CliError("CLI 参数错误", "SCHEME_REQUIRED", "missing scheme; use --scheme", 2);
return scheme ?? null;
}

Some files were not shown because too many files have changed in this diff Show More