拆分代码;约束cli命令

This commit is contained in:
2026-06-02 14:54:08 +08:00
parent 9b8a517092
commit 40e699e173
17 changed files with 395 additions and 267 deletions
+3
View File
@@ -0,0 +1,3 @@
dist/
build/
__pycache__/
+31 -20
View File
@@ -1,57 +1,68 @@
# TJWater CLI
独立于服务端主代码的 Python CLI 文件夹,放在 `TJWaterServerBinary/cli/` 下,供 agent 服务器**直接调用并通过 stdout/stderr 参与管道**
独立于服务端主代码的 Python CLI 文件夹,放在 `TJWaterServerBinary/cli/` 下,供 agent 服务器使用**编译后的可执行文件**直接调用并通过 stdout/stderr 参与管道。
## 直接使用
## 构建可执行产物
```bash
cd TJWaterServerBinary/cli
./tjwater help --json
python -m pip install -r requirements.txt
python -m pip install -r requirements-build.txt
chmod +x build.sh
./build.sh
```
这个入口文件可以直接参与管道
构建完成后,直接使用编译产物
```bash
./tjwater help --json | jq
./dist/tjwater-cli/tjwater-cli help
```
它会优先使用
1. `cli/.venv/bin/python`
2. 环境变量 `PYTHON`
3. 当前环境里的 `python`
4. 最后回退到 `python3`
如果需要,也可以显式走 Python:
这个可执行文件可以直接参与管道
```bash
python -m tjwater_agent_cli help --json
./dist/tjwater-cli/tjwater-cli help | jq
```
当前采用 `PyInstaller onedir` 方式输出到 `dist/tjwater-cli/`,避免 onefile 在部分 agent/server 环境下依赖临时目录解包执行的问题。
如果需要在开发时直接走源码入口,也可以显式使用 Python:
```bash
python -m tjwater_cli help
```
## 部署到 agent 服务器
最简单的方式是把整个 `TJWaterServerBinary/cli/` 文件夹同步到 agent 服务器,然后直接执行:
最简单的方式是把 `dist/tjwater-cli/` 整个目录同步到 agent 服务器,然后直接执行:
```bash
./tjwater-cli/tjwater-cli help
```
如果希望打包传输:
```bash
cd TJWaterServerBinary/cli
./tjwater help --json
tar -C dist -czf tjwater-cli-linux-amd64.tar.gz tjwater-cli
```
如果希望放到 PATH 中:
```bash
chmod +x tjwater
ln -s /path/to/TJWaterServerBinary/cli/tjwater /usr/local/bin/tjwater
tjwater help --json
ln -s /path/to/TJWaterServerBinary/cli/dist/tjwater-cli/tjwater-cli /usr/local/bin/tjwater-cli
tjwater-cli help | jq
```
## Python 依赖
## 运行与构建依赖
```bash
cd TJWaterServerBinary/cli
python -m pip install -r requirements.txt
python -m pip install -r requirements-build.txt
```
只保留运行 CLI 必需依赖,不再包含安装包构建相关内容
`requirements.txt` 仅包含运行 CLI 的依赖;`requirements-build.txt` 仅包含生成可执行文件所需的构建依赖
## 认证上下文
Executable
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -n "${PYTHON:-}" ]; then
PYTHON_BIN="$PYTHON"
elif command -v python >/dev/null 2>&1; then
PYTHON_BIN="python"
else
PYTHON_BIN="python3"
fi
cd "$ROOT"
"$PYTHON_BIN" -m PyInstaller --noconfirm --clean tjwater.spec
BIN_PATH="$ROOT/dist/"
if [ ! -x "$BIN_PATH" ]; then
echo "build succeeded but executable was not created: $BIN_PATH" >&2
exit 1
fi
"$BIN_PATH" help >/dev/null
echo "built executable: $BIN_PATH"
+5
View File
@@ -0,0 +1,5 @@
from tjwater_cli.main import console_entry
if __name__ == "__main__":
console_entry()
+1
View File
@@ -0,0 +1 @@
pyinstaller>=6.11,<7
+75 -50
View File
@@ -1,3 +1,4 @@
import json
from pathlib import Path
from typer.testing import CliRunner
@@ -64,61 +65,60 @@ def test_build_runtime_context_uses_default_server(monkeypatch):
assert runtime.server == core.DEFAULT_SERVER
def test_help_json_lists_commands():
def test_help_outputs_json_lists_commands():
result = runner.invoke(app, ["help"])
payload = json.loads(result.stdout)
assert result.exit_code == 0
assert payload["schema_version"] == "tjwater-cli/v1"
assert any(command["command"] == "project" for command in payload["commands"])
assert any(command["command"] == "analysis" for command in payload["commands"])
assert payload["menu_level"] == 1
assert all(command["command"] != "project list" for command in payload["commands"])
def test_help_option_json_is_removed():
result = runner.invoke(app, ["help", "--json"])
assert result.exit_code == 0
assert '"schema_version": "tjwater-cli/v1"' in result.stdout
assert '"command": "project"' in result.stdout
assert '"command": "analysis"' in result.stdout
assert '"menu_level": 1' in result.stdout
assert '"command": "project list"' not in result.stdout
def test_help_defaults_to_text():
result = runner.invoke(app, ["help"])
assert result.exit_code == 0
assert "Commands:" in result.stdout
assert "project: 项目与项目级元数据相关命令。" in result.stdout
assert "analysis: 分析计算与诊断相关命令。" in result.stdout
assert "Use `tjwater <menu> help` to see subcommands." in result.stdout
assert "usage: tjwater project help" not in result.stdout
assert "example: tjwater project help" not in result.stdout
assert "project list: 列出当前用户可访问项目" not in result.stdout
assert '"schema_version": "tjwater-cli/v1"' not in result.stdout
assert result.exit_code == 2
assert "No such option: --json" in result.output
def test_simulation_help_lists_subcommands():
result = runner.invoke(app, ["simulation", "help"])
payload = json.loads(result.stdout)
assert result.exit_code == 0
assert "模拟运行与调度相关命令。" in result.stdout
assert "simulation run: 触发指定绝对时间的模拟运行" in result.stdout
assert "usage: tjwater simulation run --start-time <START_TIME> --duration <DURATION>" in result.stdout
assert "example: tjwater --auth-context auth.json simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30" in result.stdout
assert payload["summary"] == "模拟运行与调度相关命令。"
commands = {command["command"]: command for command in payload["commands"]}
assert commands["simulation run"]["summary"] == "触发指定绝对时间的模拟运行"
assert commands["simulation run"]["usage"] == "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>"
assert commands["simulation run"]["example"] == "tjwater-cli --auth-context auth.json simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30"
def test_nested_group_help_lists_examples():
result = runner.invoke(app, ["analysis", "leakage", "help"])
payload = json.loads(result.stdout)
assert result.exit_code == 0
assert "漏损分析相关命令。" in result.stdout
assert "analysis leakage identify: 执行漏损识别" in result.stdout
assert "example: tjwater --auth-context auth.json analysis leakage identify" in result.stdout
assert payload["summary"] == "漏损分析相关命令。"
commands = {command["command"]: command for command in payload["commands"]}
assert commands["analysis leakage identify"]["summary"] == "执行漏损识别"
assert commands["analysis leakage identify"]["example"] == "tjwater-cli --auth-context auth.json analysis leakage identify --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T04:04:05+08:00"
def test_analysis_help_uses_group_summaries_for_nested_groups():
result = runner.invoke(app, ["analysis", "help"])
payload = json.loads(result.stdout)
commands = {command["command"]: command for command in payload["commands"]}
assert result.exit_code == 0
assert "analysis leakage: 漏损分析相关命令。" in result.stdout
assert "analysis burst-detection: 爆管检测相关命令。" in result.stdout
assert "analysis burst-location" not in result.stdout
assert "analysis risk" not in result.stdout
assert "analysis leakage: 执行漏损识别" not in result.stdout
assert "example: tjwater --auth-context auth.json analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 30 --burst-file ./burst.json --scheme burst_case_01" in result.stdout
assert "example: tjwater --auth-context auth.json analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900" in result.stdout
assert commands["analysis leakage"]["summary"] == "漏损分析相关命令。"
assert commands["analysis burst-detection"]["summary"] == "爆管检测相关命令。"
assert "analysis burst-location" not in commands
assert "analysis risk" not in commands
assert commands["analysis burst"]["example"] == "tjwater-cli --auth-context auth.json analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 30 --burst-file ./burst.json --scheme burst_case_01"
assert commands["analysis valve"]["example"] == "tjwater-cli --auth-context auth.json analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900"
def test_bare_analysis_uses_typer_help_with_descriptions():
@@ -133,23 +133,48 @@ def test_bare_analysis_uses_typer_help_with_descriptions():
assert "risk" not in result.stdout
def test_leaf_help_shows_usage_and_example():
def test_leaf_help_outputs_json():
result = runner.invoke(app, ["help", "simulation", "run"])
payload = json.loads(result.stdout)
assert result.exit_code == 0
assert "Command: simulation run" in result.stdout
assert "结果需后续通过 data timeseries 在对应时间段查询" in result.stdout
assert "Usage: tjwater simulation run --start-time <START_TIME> --duration <DURATION>" in result.stdout
assert "Examples:" in result.stdout
assert "tjwater --auth-context auth.json simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30" in result.stdout
assert payload["command"] == "simulation run"
assert payload["output"] == "模拟触发结果;实时数据需通过 data timeseries 命令按时间段查询"
assert payload["usage"] == "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>"
assert payload["examples"] == ["tjwater-cli --auth-context auth.json simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30"]
def test_project_help_uses_legal_kind_example():
result = runner.invoke(app, ["project", "help"])
payload = json.loads(result.stdout)
commands = {command["command"]: command for command in payload["commands"]}
assert result.exit_code == 0
assert "example: tjwater --auth-context auth.json project data --kind scada-info" in result.stdout
assert "--kind time" not in result.stdout
assert commands["project data"]["example"] == "tjwater-cli --auth-context auth.json project data --kind scada-info"
assert "--kind time" not in commands["project data"]["example"]
def test_root_help_flag_uses_typer_style_with_examples():
result = runner.invoke(app, ["--help"], prog_name="tjwater-cli")
assert result.exit_code == 0
assert "Usage: tjwater-cli" in result.stdout
assert "Examples:" in result.stdout
assert "tjwater-cli help simulation run" in result.stdout
def test_leaf_help_flag_includes_usage_and_example():
result = runner.invoke(app, ["simulation", "run", "--help"], prog_name="tjwater-cli")
assert result.exit_code == 0
assert "Usage: tjwater-cli simulation run [OPTIONS]" in result.stdout
assert "Usage example:" in result.stdout
assert "--start-time <START_TIME>" in result.stdout
assert "--duration" in result.stdout
assert "Examples:" in result.stdout
assert "tjwater-cli simulation run" in result.stdout
assert "START_TIME" in result.stdout
assert "DURATION" in result.stdout
def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path: Path):
@@ -186,8 +211,8 @@ def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path:
assert result.exit_code == 0
assert '"summary": "爆管分析执行成功"' in result.stdout
assert '"tjwater --auth-context auth.json data scheme get --name burst_case_01"' in result.stdout
assert '"tjwater --auth-context auth.json data scheme list"' in result.stdout
assert '"tjwater-cli --auth-context auth.json data scheme get --name burst_case_01"' in result.stdout
assert '"tjwater-cli --auth-context auth.json data scheme list"' in result.stdout
def test_main_missing_option_error_includes_usage_and_next_step(capsys):
@@ -197,8 +222,8 @@ def test_main_missing_option_error_includes_usage_and_next_step(capsys):
assert exit_code == 2
assert '"summary": "缺少参数"' in stdout
assert '"code": "MISSING_PARAMETER"' in stdout
assert '"usage": "tjwater simulation run --start-time <START_TIME> --duration <DURATION>"' in stdout
assert '"tjwater help simulation run"' in stdout
assert '"usage": "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>"' in stdout
assert '"tjwater-cli help simulation run"' in stdout
def test_main_bare_analysis_returns_typer_help_without_json_error(capsys):
@@ -206,7 +231,7 @@ def test_main_bare_analysis_returns_typer_help_without_json_error(capsys):
stdout = capsys.readouterr().out
assert exit_code == 0
assert "Usage: tjwater analysis" in stdout
assert "Usage: tjwater-cli analysis" in stdout
assert "分析计算与诊断相关命令。" in stdout
assert '"ok": false' not in stdout
@@ -268,8 +293,8 @@ def test_simulation_run_translates_rfc3339(monkeypatch, tmp_path: Path):
"start_time": "03:04:05+08:00",
"duration": 30,
}
assert '"tjwater --auth-context auth.json data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00"' in result.stdout
assert '"tjwater --auth-context auth.json data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00"' in result.stdout
assert '"tjwater-cli --auth-context auth.json data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00"' in result.stdout
assert '"tjwater-cli --auth-context auth.json data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00"' in result.stdout
def test_project_export_inp_downloads_file(monkeypatch, tmp_path: Path):
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -x "$ROOT/.venv/bin/python" ]; then
PYTHON_BIN="$ROOT/.venv/bin/python"
elif [ -n "${PYTHON:-}" ]; then
PYTHON_BIN="$PYTHON"
elif command -v python >/dev/null 2>&1; then
PYTHON_BIN="python"
else
PYTHON_BIN="python3"
fi
export PYTHONPATH="$ROOT${PYTHONPATH:+:$PYTHONPATH}"
exec "$PYTHON_BIN" -m tjwater_agent_cli "$@"
+45
View File
@@ -0,0 +1,45 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_data_files
datas = collect_data_files("certifi")
a = Analysis(
["entrypoint.py"],
pathex=["."],
binaries=[],
datas=datas,
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="tjwater-cli",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name="tjwater-cli",
)
+27 -25
View File
@@ -2,31 +2,33 @@ from __future__ import annotations
import typer
app = typer.Typer(help="TJWater agent CLI", add_completion=False, no_args_is_help=True)
project_app = typer.Typer(no_args_is_help=True)
network_app = typer.Typer(no_args_is_help=True)
component_app = typer.Typer(no_args_is_help=True)
component_option_app = typer.Typer(no_args_is_help=True)
simulation_app = typer.Typer(no_args_is_help=True)
analysis_app = typer.Typer(no_args_is_help=True)
analysis_leakage_app = typer.Typer(no_args_is_help=True)
analysis_leakage_schemes_app = typer.Typer(no_args_is_help=True)
analysis_burst_detection_app = typer.Typer(no_args_is_help=True)
analysis_burst_detection_schemes_app = typer.Typer(no_args_is_help=True)
analysis_burst_location_app = typer.Typer(no_args_is_help=True)
analysis_burst_location_schemes_app = typer.Typer(no_args_is_help=True)
analysis_risk_app = typer.Typer(no_args_is_help=True)
analysis_sensor_placement_app = typer.Typer(no_args_is_help=True)
data_app = typer.Typer(no_args_is_help=True)
data_timeseries_app = typer.Typer(no_args_is_help=True)
data_timeseries_realtime_app = typer.Typer(no_args_is_help=True)
data_timeseries_scheme_app = typer.Typer(no_args_is_help=True)
data_timeseries_scada_app = typer.Typer(no_args_is_help=True)
data_timeseries_composite_app = typer.Typer(no_args_is_help=True)
data_scada_app = typer.Typer(no_args_is_help=True)
data_scheme_app = typer.Typer(no_args_is_help=True)
data_extension_app = typer.Typer(no_args_is_help=True)
data_misc_app = typer.Typer(no_args_is_help=True)
from .formatters import TJWaterGroup
app = typer.Typer(help="TJWater agent CLI", add_completion=False, no_args_is_help=True, cls=TJWaterGroup)
project_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
network_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
component_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
component_option_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
simulation_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
analysis_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
analysis_leakage_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
analysis_leakage_schemes_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
analysis_burst_detection_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
analysis_burst_detection_schemes_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
analysis_burst_location_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
analysis_burst_location_schemes_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
analysis_risk_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
analysis_sensor_placement_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
data_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
data_timeseries_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
data_timeseries_realtime_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
data_timeseries_scheme_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
data_timeseries_scada_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
data_timeseries_composite_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
data_scada_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
data_scheme_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
data_extension_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
data_misc_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
app.add_typer(project_app, name="project")
app.add_typer(network_app, name="network")
+4 -4
View File
@@ -58,8 +58,8 @@ def simulation_run(
require_auth=True,
require_network_ctx=True,
next_commands=[
f"tjwater --auth-context auth.json data timeseries realtime links --start-time {parsed.isoformat()} --end-time {end_time}",
f"tjwater --auth-context auth.json data timeseries realtime nodes --start-time {parsed.isoformat()} --end-time {end_time}",
f"tjwater-cli --auth-context auth.json data timeseries realtime links --start-time {parsed.isoformat()} --end-time {end_time}",
f"tjwater-cli --auth-context auth.json data timeseries realtime nodes --start-time {parsed.isoformat()} --end-time {end_time}",
],
)
@@ -92,8 +92,8 @@ def analysis_burst(
require_auth=True,
require_network_ctx=True,
next_commands=[
f"tjwater --auth-context auth.json data scheme get --name {scheme_name}",
"tjwater --auth-context auth.json data scheme list",
f"tjwater-cli --auth-context auth.json data scheme get --name {scheme_name}",
"tjwater-cli --auth-context auth.json data scheme list",
],
)
+1 -1
View File
@@ -98,7 +98,7 @@ def project_export_inp(
data={"output": str(output), "bytes": len(content)},
ctx=runtime,
duration_ms=duration_dump + duration_download,
next_commands=["tjwater project info"],
next_commands=["tjwater-cli project info"],
)
+2 -1
View File
@@ -13,6 +13,7 @@ import requests
import typer
SCHEMA_VERSION = "tjwater-cli/v1"
CLI_NAME = "tjwater-cli"
DEFAULT_TIMEOUT = 60
DEFAULT_SERVER = "http://192.168.1.114:8000"
@@ -180,7 +181,7 @@ def require_access_token(ctx: RuntimeContext) -> str:
code="UNAUTHENTICATED",
message="missing access token for agent context",
exit_code=3,
next_commands=["tjwater <command> --auth-context /path/to/auth-context.json"],
next_commands=["tjwater-cli <command> --auth-context /path/to/auth-context.json"],
)
+15
View File
@@ -0,0 +1,15 @@
from __future__ import annotations
import click
import typer.core
class TJWaterGroup(typer.core.TyperGroup):
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
super().format_help(ctx, formatter)
from .helping import build_group_help_appendix
appendix = build_group_help_appendix(ctx)
if appendix:
formatter.write_paragraph()
formatter.write_text(appendix)
+78 -64
View File
@@ -37,7 +37,7 @@ def context_command_path(click_ctx: click.Context | None) -> tuple[str, ...]:
def _build_click_context(path: tuple[str, ...]) -> click.Context | None:
root = _click_root_command()
ctx: click.Context = click.Context(root, info_name="tjwater")
ctx: click.Context = click.Context(root, info_name="tjwater-cli")
command: click.Command = root
for token in path:
if not isinstance(command, click.Group):
@@ -54,7 +54,7 @@ def build_usage(path: tuple[str, ...]) -> str | None:
ctx = _build_click_context(path)
if ctx is None:
return None
parts = ["tjwater", *path]
parts = ["tjwater-cli", *path]
for parameter in ctx.command.params:
if not isinstance(parameter, click.Option):
continue
@@ -165,7 +165,7 @@ def _build_example(path: tuple[str, ...], *, existing_examples: list[str] | None
has_required_options = all(f"--{option_name}" in example for option_name in required_option_names)
if has_auth and has_required_options:
return example
parts = ["tjwater", "--auth-context", "auth.json", *path]
parts = ["tjwater-cli", "--auth-context", "auth.json", *path]
if ctx is None:
return " ".join(parts)
for parameter in ctx.command.params:
@@ -198,8 +198,8 @@ def _enrich_index_payload(payload: dict[str, Any]) -> dict[str, Any]:
path = tuple(command_item["command"].split())
doc = get_command_doc(path)
if doc is None and has_subcommands(path):
command_item["usage"] = f"tjwater {' '.join(path)} help"
command_item["example"] = f"tjwater {' '.join(path)} help"
command_item["usage"] = f"tjwater-cli {' '.join(path)} help"
command_item["example"] = f"tjwater-cli {' '.join(path)} help"
else:
existing_examples = [] if doc is None else list(doc.get("examples", []))
command_item["usage"] = build_usage(path) or command_item.get("usage")
@@ -220,11 +220,8 @@ def resolve_help_payload(path: tuple[str, ...]) -> tuple[dict[str, Any] | None,
return None, False
def emit_help_payload(payload: dict[str, Any], *, json_output: bool, is_index: bool) -> None:
if json_output:
typer.echo(json.dumps(payload, ensure_ascii=False))
else:
typer.echo(render_help_text(payload, is_index=is_index))
def emit_help_payload(payload: dict[str, Any]) -> None:
typer.echo(json.dumps(payload, ensure_ascii=False))
def merge_next_commands(*groups: list[str] | None) -> list[str]:
@@ -259,12 +256,12 @@ def build_error_guidance(click_ctx: click.Context | None) -> tuple[Any, list[str
return (
{
"command_group": " ".join(group_path),
"usage": f"tjwater {' '.join(group_path)} help",
"examples": [f"tjwater {' '.join(group_path)} help", f"tjwater help {' '.join(group_path)}"],
"usage": f"tjwater-cli {' '.join(group_path)} help",
"examples": [f"tjwater-cli {' '.join(group_path)} help", f"tjwater-cli help {' '.join(group_path)}"],
},
merge_next_commands(
[f"tjwater {' '.join(group_path)} help", f"tjwater help {' '.join(group_path)}"],
["tjwater help"],
[f"tjwater-cli {' '.join(group_path)} help", f"tjwater-cli help {' '.join(group_path)}"],
["tjwater-cli help"],
),
)
payload, is_index = resolve_help_payload(command_path)
@@ -275,21 +272,21 @@ def build_error_guidance(click_ctx: click.Context | None) -> tuple[Any, list[str
"usage": payload.get("usage") or usage,
"examples": payload.get("examples", []),
},
merge_next_commands([f"tjwater help {' '.join(command_path)}"], ["tjwater help"]),
merge_next_commands([f"tjwater-cli help {' '.join(command_path)}"], ["tjwater-cli help"]),
)
if payload is not None and is_index:
return (
{
"command_group": " ".join(command_path),
"usage": f"tjwater {' '.join(command_path)} help",
"examples": [f"tjwater {' '.join(command_path)} help", f"tjwater help {' '.join(command_path)}"],
"usage": f"tjwater-cli {' '.join(command_path)} help",
"examples": [f"tjwater-cli {' '.join(command_path)} help", f"tjwater-cli help {' '.join(command_path)}"],
},
merge_next_commands(
[f"tjwater {' '.join(command_path)} help", f"tjwater help {' '.join(command_path)}"],
["tjwater help"],
[f"tjwater-cli {' '.join(command_path)} help", f"tjwater-cli help {' '.join(command_path)}"],
["tjwater-cli help"],
),
)
return ({"usage": usage} if usage else None, ["tjwater help"])
return ({"usage": usage} if usage else None, ["tjwater-cli help"])
def classify_click_error(exc: click.ClickException) -> tuple[str, str]:
@@ -305,55 +302,61 @@ def classify_click_error(exc: click.ClickException) -> tuple[str, str]:
return "CLI 参数错误", "USAGE_ERROR"
def render_help_text(payload: dict[str, Any], *, is_index: bool) -> str:
lines: list[str] = [str(payload.get("summary", ""))]
if is_index:
is_top_level = payload.get("menu_level") == 1
lines.append("")
lines.append("Commands:")
for command in payload.get("commands", []):
lines.append(f" {command['command']}: {command['summary']}")
if not is_top_level and command.get("usage"):
lines.append(f" usage: {command['usage']}")
if not is_top_level and command.get("example"):
lines.append(f" example: {command['example']}")
lines.append("")
if is_top_level:
lines.append("Use `tjwater <menu> help` to see subcommands.")
else:
lines.append("Use `tjwater help --json` for structured output.")
return "\n".join(lines)
def _build_root_help_epilog() -> str:
return "\n".join(
[
"\b",
"Examples:",
" tjwater-cli help",
" tjwater-cli help simulation run",
" tjwater-cli simulation run --help",
]
)
lines.append("")
lines.append(f"Command: {payload['command']}")
lines.append(f"Description: {payload['description']}")
if payload.get("usage"):
lines.append(f"Usage: {payload['usage']}")
options = payload.get("options", [])
if options:
lines.append("")
lines.append("Options:")
for option in options:
suffix = " (required)" if option.get("required") else ""
lines.append(f" --{option['name']}{suffix}: {option['description']}")
def _build_leaf_help_epilog(path: tuple[str, ...], payload: dict[str, Any]) -> str:
lines = ["\b"]
description = payload.get("description")
usage = payload.get("usage")
examples = payload.get("examples", [])
next_commands = payload.get("next_commands", [])
if description:
lines.extend([f"Description: {description}", ""])
if usage:
lines.extend([f"Usage example: {usage}", ""])
if examples:
lines.append("")
lines.append("Examples:")
for example in examples:
lines.append(f" {example}")
lines.append("")
lines.append("Use `tjwater help --json` for structured output.")
lines.extend(f" {example}" for example in examples)
lines.append("")
if next_commands:
lines.append("Next steps:")
lines.extend(f" {command}" for command in next_commands)
lines.append("")
lines.extend(["Structured JSON:", f" tjwater-cli help {' '.join(path)}"])
return "\n".join(lines)
def _build_group_help_epilog(path: tuple[str, ...], payload: dict[str, Any]) -> str:
lines = ["\b", "Examples:", f" tjwater-cli help {' '.join(path)}"]
for command in payload.get("commands", [])[:2]:
example = command.get("example")
if example:
lines.append(f" {example}")
return "\n".join(lines)
def build_group_help_appendix(click_ctx: click.Context | None) -> str | None:
path = context_command_path(click_ctx)
if not path:
return _build_root_help_epilog()
payload, is_index = resolve_help_payload(path)
if payload is None or not is_index:
return None
return _build_group_help_epilog(path, payload)
def make_group_help_handler(path_prefix: tuple[str, ...]):
def group_help(
json_output: Annotated[bool, typer.Option("--json", help="输出 JSON")] = False,
) -> None:
def group_help() -> None:
payload, is_index = resolve_help_payload(path_prefix)
if payload is None:
raise CLIError(
@@ -361,9 +364,9 @@ def make_group_help_handler(path_prefix: tuple[str, ...]):
code="COMMAND_NOT_FOUND",
message=f"unknown command path: {' '.join(path_prefix)}",
exit_code=2,
next_commands=["tjwater help"],
next_commands=["tjwater-cli help"],
)
emit_help_payload(payload, json_output=json_output, is_index=is_index)
emit_help_payload(payload)
group_help.__name__ = f"{'_'.join(path_prefix)}_help"
return group_help
@@ -375,19 +378,30 @@ def register_group_help_commands() -> None:
def apply_typer_help_metadata() -> None:
app.help = "TJWater agent CLI"
app.help = "\n".join(
[
"TJWater agent CLI",
"",
"Examples:",
" tjwater-cli help",
" tjwater-cli help simulation run",
" tjwater-cli simulation run --help",
]
)
app.short_help = "TJWater agent CLI"
for group_app, path_prefix in GROUP_HELP_APPS:
for command_info in group_app.registered_commands:
command_path = (*path_prefix, command_info.name)
if command_info.name == "help":
command_info.help = f"显示 {' '.join(path_prefix)} 的帮助信息。"
command_info.help = f"输出 {' '.join(path_prefix)} JSON 帮助信息。"
command_info.short_help = command_info.help
command_info.epilog = "\n".join(["\b", "Example:", f" tjwater-cli help {' '.join(path_prefix)}"])
command_info.hidden = False
continue
payload = get_command_doc(command_path)
command_info.help = None if payload is None else str(payload.get("summary", ""))
command_info.short_help = command_info.help
command_info.epilog = None if payload is None else _build_leaf_help_epilog(command_path, payload)
command_info.hidden = is_hidden_path(command_path)
for group_info in group_app.registered_groups:
group_path = (*path_prefix, group_info.name)
+7 -10
View File
@@ -44,11 +44,8 @@ def root_callback(
register_group_help_commands()
@app.command("help", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
def help_command(
ctx: typer.Context,
json_output: Annotated[bool, typer.Option("--json", help="输出 JSON")] = False,
) -> None:
@app.command("help", context_settings={"allow_extra_args": True})
def help_command(ctx: typer.Context) -> None:
command_path = list(ctx.args)
payload, is_index = resolve_help_payload(tuple(command_path))
if payload is None:
@@ -61,13 +58,13 @@ def help_command(
server=None,
request_id=None,
data={
"usage": "tjwater help <command-path>",
"examples": ["tjwater help simulation run", "tjwater simulation help"],
"usage": "tjwater-cli help <command-path>",
"examples": ["tjwater-cli help simulation run", "tjwater-cli simulation help"],
},
next_commands=["tjwater help", "tjwater help simulation"],
next_commands=["tjwater-cli help", "tjwater-cli help simulation"],
)
raise typer.Exit(code=2)
emit_help_payload(payload, json_output=json_output, is_index=is_index)
emit_help_payload(payload)
# Must run at import time because tests call runner.invoke(app, ...) directly.
@@ -76,7 +73,7 @@ apply_typer_help_metadata()
def main(argv: list[str] | None = None) -> int:
try:
app(args=argv if argv is not None else sys.argv[1:], prog_name="tjwater", standalone_mode=False)
app(args=argv if argv is not None else sys.argv[1:], prog_name="tjwater-cli", standalone_mode=False)
return 0
except CLIError as exc:
click_ctx = click.get_current_context(silent=True)
+12 -12
View File
@@ -39,15 +39,15 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
path=("project", "list"),
summary="列出当前用户可访问项目",
description="调用 /meta/projects 返回项目列表。",
examples=("tjwater --auth-context auth.json project list",),
next_commands=("tjwater --auth-context auth.json project info",),
examples=("tjwater-cli --auth-context auth.json project list",),
next_commands=("tjwater-cli --auth-context auth.json project info",),
output="项目摘要列表",
),
("project", "info"): CommandDoc(
path=("project", "info"),
summary="读取当前项目元数据",
description="调用 /meta/project 返回当前 project 详情。",
examples=("tjwater --auth-context auth.json project info",),
examples=("tjwater-cli --auth-context auth.json project info",),
output="项目元数据",
),
("project", "db-health"): CommandDoc(
@@ -109,8 +109,8 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
CommandOptionDoc("duration", "持续分钟数", required=True),
),
next_commands=(
"tjwater --auth-context auth.json data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00",
"tjwater --auth-context auth.json data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00",
"tjwater-cli --auth-context auth.json data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00",
"tjwater-cli --auth-context auth.json data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00",
),
output="模拟触发结果;实时数据需通过 data timeseries 命令按时间段查询",
),
@@ -125,11 +125,11 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
CommandOptionDoc("scheme", "方案名称"),
),
examples=(
"tjwater --auth-context auth.json analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 30 --burst-file ./burst.json --scheme burst_case_01",
"tjwater-cli --auth-context auth.json analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 30 --burst-file ./burst.json --scheme burst_case_01",
),
next_commands=(
"tjwater --auth-context auth.json data scheme get --name burst_case_01",
"tjwater --auth-context auth.json data scheme list",
"tjwater-cli --auth-context auth.json data scheme get --name burst_case_01",
"tjwater-cli --auth-context auth.json data scheme list",
),
output="分析执行结果;方案详情需通过 data scheme 命令单独查询",
),
@@ -138,7 +138,7 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
summary="执行阀门关闭或隔离分析",
description="mode=close 使用 valve 列表;mode=isolation 需要 accident element,可选 disabled-valve。",
examples=(
"tjwater --auth-context auth.json analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900",
"tjwater-cli --auth-context auth.json analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900",
),
),
("analysis", "flushing"): CommandDoc(
@@ -422,13 +422,13 @@ def list_subcommands(path_prefix: tuple[str, ...], summary: str | None = None) -
seen.add(subcommand)
current_path = (*path_prefix, subcommand)
is_group = has_subcommands(current_path)
usage = f"tjwater {' '.join(current_path)} help" if is_group else (doc.examples[0] if doc.examples else _build_usage(doc))
usage = f"tjwater-cli {' '.join(current_path)} help" if is_group else (doc.examples[0] if doc.examples else _build_usage(doc))
commands.append(
{
"command": " ".join(current_path),
"summary": get_group_summary(current_path) if is_group else doc.summary,
"usage": usage,
"example": f"tjwater {' '.join(current_path)} help" if is_group else _build_examples(doc)[0],
"example": f"tjwater-cli {' '.join(current_path)} help" if is_group else _build_examples(doc)[0],
}
)
return {
@@ -440,7 +440,7 @@ def list_subcommands(path_prefix: tuple[str, ...], summary: str | None = None) -
def _build_usage(doc: CommandDoc) -> str:
parts = ["tjwater", *doc.path]
parts = ["tjwater-cli", *doc.path]
for option in doc.options:
placeholder = option.name.upper().replace("-", "_")
if option.required:
+63 -63
View File
@@ -7,13 +7,13 @@
首批 CLI 采用 **少量顶层入口 + 业务域二级分组 + 只读/分析优先** 的设计。
```text
tjwater project
tjwater network
tjwater component
tjwater simulation
tjwater analysis
tjwater data
tjwater help
tjwater-cli project
tjwater-cli network
tjwater-cli component
tjwater-cli simulation
tjwater-cli analysis
tjwater-cli data
tjwater-cli help
```
首批默认不暴露:
@@ -45,12 +45,12 @@ tjwater help
| `simulation` | `run` | 模拟运行 |
| `analysis` | `burst``valve``flushing``age``contaminant``sensor-placement``leakage``burst-detection``burst-location``risk` | 任务级分析 |
| `data` | `timeseries``scada``scheme``extension``misc` | 数据查询 |
| `help` | `--json``COMMAND --json` | Agent 能力发现和命令说明 |
| `help` | `COMMAND` | Agent 能力发现和命令说明 |
命令深度建议:
- 常规命令不超过 3 层:`tjwater component option get`
- 时序数据允许 4 层:`tjwater data timeseries realtime links`
- 常规命令不超过 3 层:`tjwater-cli component option get`
- 时序数据允许 4 层:`tjwater-cli data timeseries realtime links`
- `risk` 归入 `analysis risk`
- `scada``scheme``extension` 归入 `data`
@@ -88,7 +88,7 @@ tjwater help
- 用户输入的业务时间默认按 **UTC+8** 理解;若命令直接接收完整时间戳,应使用 ISO 8601 / RFC 3339 并显式包含时区。CLI 可直接传 `+08:00`,也可传其他时区的绝对时间,由服务端统一归一化。
- 范围参数优先拆成 `--start-time` / `--end-time`,不再引入模糊的 `--time-range ...` 写法。
- 复合输入优先使用可重复显式选项或 `--input FILE`,避免把多个语义字段压进 `ID:SIZE``NODE:VALUE``VALVE:OPENING` 这类 shell 内联 DSL。
- 若必须传大批量复合参数,优先支持 `--input FILE`,文件格式由 `help --json` 给出 schema。
- 若必须传大批量复合参数,优先支持 `--input FILE`,文件格式由 `help` 给出 schema。
## 首批 CLI 范围
@@ -138,11 +138,11 @@ Agent 调用认证上下文:
| 命令 | 覆盖接口 | 说明 |
|---|---|---|
| `tjwater project list` | `GET /meta/projects` | 项目列表 |
| `tjwater project info` | `GET /meta/project` | 当前 project 信息 |
| `tjwater project db-health` | `GET /meta/db/health` | 当前 project 数据库健康 |
| `tjwater project export-inp --output PATH` | `GET /exportinp/``GET /dumpinp/``GET /downloadinp/` | 导出当前 project 的 INP 到本地文件 |
| `tjwater project data --kind scada-info\|scheme-list\|burst-locate-result` | `GET /scada-info``GET /scheme-list``GET /burst-locate-result*` | 当前 project 的业务数据 |
| `tjwater-cli project list` | `GET /meta/projects` | 项目列表 |
| `tjwater-cli project info` | `GET /meta/project` | 当前 project 信息 |
| `tjwater-cli project db-health` | `GET /meta/db/health` | 当前 project 数据库健康 |
| `tjwater-cli project export-inp --output PATH` | `GET /exportinp/``GET /dumpinp/``GET /downloadinp/` | 导出当前 project 的 INP 到本地文件 |
| `tjwater-cli project data --kind scada-info\|scheme-list\|burst-locate-result` | `GET /scada-info``GET /scheme-list``GET /burst-locate-result*` | 当前 project 的业务数据 |
暂不暴露:
@@ -181,8 +181,8 @@ app/api/v1/endpoints/network/*.py
| 命令 | 覆盖接口 | 说明 |
|---|---|---|
| `tjwater network get-node-properties --node NODE` | `GET /getnodeproperties/` | 读取当前 project 中指定节点的属性 |
| `tjwater network get-link-properties --link LINK` | `GET /getlinkproperties/` | 读取当前 project 中指定管线的属性 |
| `tjwater-cli network get-node-properties --node NODE` | `GET /getnodeproperties/` | 读取当前 project 中指定节点的属性 |
| `tjwater-cli network get-link-properties --link LINK` | `GET /getlinkproperties/` | 读取当前 project 中指定管线的属性 |
暂不暴露:
@@ -209,14 +209,14 @@ app/api/v1/endpoints/components/*.py
| 命令 | 覆盖接口 | 说明 |
|---|---|---|
| `tjwater component option schema --kind time` | `GET /gettimeschema` | 时间选项 schema |
| `tjwater component option get --kind time` | `GET /gettimeproperties/` | 时间选项属性 |
| `tjwater component option schema --kind energy` | `GET /getenergyschema/` | 全局能耗选项 schema |
| `tjwater component option get --kind energy` | `GET /getenergyproperties/` | 全局能耗选项属性 |
| `tjwater component option schema --kind pump-energy` | `GET /getpumpenergyschema/` | 泵能耗选项 schema |
| `tjwater component option get --kind pump-energy --pump PUMP` | `GET /getpumpenergyproperties//` | 指定泵的能耗选项属性 |
| `tjwater component option schema --kind network` | `GET /getoptionschema/` | 管网选项 schema |
| `tjwater component option get --kind network` | `GET /getoptionproperties/` | 管网选项属性 |
| `tjwater-cli component option schema --kind time` | `GET /gettimeschema` | 时间选项 schema |
| `tjwater-cli component option get --kind time` | `GET /gettimeproperties/` | 时间选项属性 |
| `tjwater-cli component option schema --kind energy` | `GET /getenergyschema/` | 全局能耗选项 schema |
| `tjwater-cli component option get --kind energy` | `GET /getenergyproperties/` | 全局能耗选项属性 |
| `tjwater-cli component option schema --kind pump-energy` | `GET /getpumpenergyschema/` | 泵能耗选项 schema |
| `tjwater-cli component option get --kind pump-energy --pump PUMP` | `GET /getpumpenergyproperties//` | 指定泵的能耗选项属性 |
| `tjwater-cli component option schema --kind network` | `GET /getoptionschema/` | 管网选项 schema |
| `tjwater-cli component option get --kind network` | `GET /getoptionproperties/` | 管网选项属性 |
暂不暴露:
@@ -273,22 +273,22 @@ app/api/v1/endpoints/risk.py
| 命令 | 覆盖接口 | 说明 |
|---|---|---|
| `tjwater simulation run --start-time RFC3339 --duration MINUTES` | `POST /runsimulationmanuallybydate/` | 按指定绝对开始时间触发当前 project 的实时模拟;`start-time` 必须显式带时区,结果写入服务端时序库,后续通过 `tjwater data timeseries realtime *` 查询 |
| `tjwater analysis burst --start-time TIME --duration SEC --scheme SCHEME --burst-file FILE` | `GET /burst_analysis/` | 爆管分析;`FILE` 提供爆管点与流量列表,CLI 负责转换为 `burst_ID[]` / `burst_size[]` |
| `tjwater analysis valve --mode close\|isolation --start-time TIME --valve VALVE` | `GET /valve_close_analysis/``GET /valve_isolation_analysis/` | 阀门分析,`--valve` 可重复 |
| `tjwater analysis flushing --start-time TIME --valve-setting-file FILE --drainage-node NODE --flow FLOW [--duration SEC] [--scheme SCHEME]` | `GET /flushing_analysis/` | 冲洗分析;`FILE` 提供阀门与开度列表,CLI 负责转换为 `valves[]` / `valves_k[]` |
| `tjwater analysis age --start-time TIME --duration SEC` | `GET /age_analysis/` | 水龄分析 |
| `tjwater analysis contaminant --start-time TIME --duration SEC --source-node NODE --concentration VALUE [--pattern PATTERN] [--scheme SCHEME]` | `GET /contaminant_simulation/` | 污染物模拟 |
| `tjwater analysis sensor-placement kmeans --count N` | `GET /pressuresensorplacementkmeans/` | 基于 kmeans 的传感器放置分析;不包含创建方案 |
| `tjwater analysis leakage identify --scheme SCHEME --start-time TIME --end-time TIME` | `POST /leakage/identify/` | 漏损识别 |
| `tjwater analysis leakage schemes list\|get` | `GET /leakage/schemes/``GET /leakage/schemes/{scheme_name}` | 漏损方案查询 |
| `tjwater analysis burst-detection detect --scheme SCHEME --start-time TIME --end-time TIME` | `POST /burst-detection/detect/` | 爆管检测 |
| `tjwater analysis burst-detection schemes list\|get` | `GET /burst-detection/schemes/``GET /burst-detection/schemes/{scheme_name}` | 爆管检测方案查询 |
| `tjwater analysis burst-location locate --scheme SCHEME --start-time TIME --end-time TIME` | `POST /burst-location/locate/` | 爆管定位 |
| `tjwater analysis burst-location schemes list\|get` | `GET /burst-location/schemes/``GET /burst-location/schemes/{scheme_name}` | 爆管定位方案查询 |
| `tjwater analysis risk pipe-now --pipe PIPE` | `GET /getpiperiskprobabilitynow/` | 单条管道当前风险 |
| `tjwater analysis risk pipe-history --pipe PIPE` | `GET /getpiperiskprobability/` | 单条管道历史风险 |
| `tjwater analysis risk network` | `GET /getnetworkpiperiskprobabilitynow/``GET /getpiperiskprobabilitygeometries/` | 当前 project 全网风险 |
| `tjwater-cli simulation run --start-time RFC3339 --duration MINUTES` | `POST /runsimulationmanuallybydate/` | 按指定绝对开始时间触发当前 project 的实时模拟;`start-time` 必须显式带时区,结果写入服务端时序库,后续通过 `tjwater-cli data timeseries realtime *` 查询 |
| `tjwater-cli analysis burst --start-time TIME --duration SEC --scheme SCHEME --burst-file FILE` | `GET /burst_analysis/` | 爆管分析;`FILE` 提供爆管点与流量列表,CLI 负责转换为 `burst_ID[]` / `burst_size[]` |
| `tjwater-cli analysis valve --mode close\|isolation --start-time TIME --valve VALVE` | `GET /valve_close_analysis/``GET /valve_isolation_analysis/` | 阀门分析,`--valve` 可重复 |
| `tjwater-cli analysis flushing --start-time TIME --valve-setting-file FILE --drainage-node NODE --flow FLOW [--duration SEC] [--scheme SCHEME]` | `GET /flushing_analysis/` | 冲洗分析;`FILE` 提供阀门与开度列表,CLI 负责转换为 `valves[]` / `valves_k[]` |
| `tjwater-cli analysis age --start-time TIME --duration SEC` | `GET /age_analysis/` | 水龄分析 |
| `tjwater-cli analysis contaminant --start-time TIME --duration SEC --source-node NODE --concentration VALUE [--pattern PATTERN] [--scheme SCHEME]` | `GET /contaminant_simulation/` | 污染物模拟 |
| `tjwater-cli analysis sensor-placement kmeans --count N` | `GET /pressuresensorplacementkmeans/` | 基于 kmeans 的传感器放置分析;不包含创建方案 |
| `tjwater-cli analysis leakage identify --scheme SCHEME --start-time TIME --end-time TIME` | `POST /leakage/identify/` | 漏损识别 |
| `tjwater-cli analysis leakage schemes list\|get` | `GET /leakage/schemes/``GET /leakage/schemes/{scheme_name}` | 漏损方案查询 |
| `tjwater-cli analysis burst-detection detect --scheme SCHEME --start-time TIME --end-time TIME` | `POST /burst-detection/detect/` | 爆管检测 |
| `tjwater-cli analysis burst-detection schemes list\|get` | `GET /burst-detection/schemes/``GET /burst-detection/schemes/{scheme_name}` | 爆管检测方案查询 |
| `tjwater-cli analysis burst-location locate --scheme SCHEME --start-time TIME --end-time TIME` | `POST /burst-location/locate/` | 爆管定位 |
| `tjwater-cli analysis burst-location schemes list\|get` | `GET /burst-location/schemes/``GET /burst-location/schemes/{scheme_name}` | 爆管定位方案查询 |
| `tjwater-cli analysis risk pipe-now --pipe PIPE` | `GET /getpiperiskprobabilitynow/` | 单条管道当前风险 |
| `tjwater-cli analysis risk pipe-history --pipe PIPE` | `GET /getpiperiskprobability/` | 单条管道历史风险 |
| `tjwater-cli analysis risk network` | `GET /getnetworkpiperiskprobabilitynow/``GET /getpiperiskprobabilitygeometries/` | 当前 project 全网风险 |
暂缓或暂不暴露:
@@ -310,7 +310,7 @@ POST /daily_scheduling_analysis/
- `simulation run` 不直接回传全量模拟结果;它负责触发服务端模拟,并返回执行摘要、时间窗口和后续查询提示。
- 当前 `runsimulationmanuallybydate` 接口会从 `start_time` 指定的绝对时间开始,按 15 分钟步长运行直到达到 `duration`,结果持久化到服务端时序存储。
- `start_time` 必须显式带时区;CLI 推荐直接传 **UTC+8** 时间,服务端统一转换后执行和落库。CLI 文档与帮助信息需要把这条规则写成显式契约,不能把数据库存储时间直接暴露成用户输入语义。
- 模拟结果读取统一走 `tjwater data timeseries realtime *`,而不是再单独设计 `simulation output`
- 模拟结果读取统一走 `tjwater-cli data timeseries realtime *`,而不是再单独设计 `simulation output`
- `analysis` 相关命令首批也按同步请求处理;若后续服务端真的引入任务队列,再单独设计 `job` 类基础设施能力。
### Data
@@ -328,22 +328,22 @@ app/api/v1/endpoints/project_data.py
| 命令 | 覆盖接口 | 说明 |
|---|---|---|
| `tjwater data timeseries realtime links --start-time TIME --end-time TIME` | `GET /realtime/links` | 查询指定时间范围内的实时/模拟管道数据 |
| `tjwater data timeseries realtime nodes --start-time TIME --end-time TIME` | `GET /realtime/nodes` | 查询指定时间范围内的实时/模拟节点数据 |
| `tjwater data timeseries realtime simulation-by-id-time --id ID --type pipe\|junction --time TIME` | `GET /realtime/query/by-id-time` | 查询指定元素在指定时间点的模拟结果 |
| `tjwater data timeseries realtime simulation-by-time-property --type pipe\|junction --time TIME --property PROPERTY` | `GET /realtime/query/by-time-property` | 查询指定时间点某类元素某属性的聚合模拟结果 |
| `tjwater data timeseries scheme links --scheme SCHEME --start-time TIME --end-time TIME` | `GET /scheme/links``GET /scheme/links/{link_id}/field` | 方案管道数据 |
| `tjwater data timeseries scheme node-field --node NODE --field FIELD` | `GET /scheme/nodes/{node_id}/field` | 方案节点字段 |
| `tjwater data timeseries scheme simulation --query by-id-time\|by-scheme-time-property --scheme SCHEME --id ID --time TIME --property PROPERTY` | `GET /scheme/query/*` | 方案模拟查询 |
| `tjwater data timeseries scada query --device-id ID --start-time TIME --end-time TIME [--device-id ID ...] [--field FIELD]` | `GET /scada/by-ids-time-range``GET /scada/by-ids-field-time-range` | SCADA 时序;CLI 把重复 `--device-id` 转换为后端逗号分隔参数 |
| `tjwater data timeseries composite --kind scada-simulation\|element-simulation\|element-scada --feature FEATURE --start-time TIME --end-time TIME` | `GET /composite/*` | 复合查询,`--feature` 可重复 |
| `tjwater data timeseries composite pipeline-health --pipe PIPE --start-time TIME --end-time TIME` | `GET /composite/pipeline-health-prediction` | 管道健康预测 |
| `tjwater data scada schema --kind device\|device-data\|element\|info` | `GET /getscada*schema/` | `SCADA` 元数据 `schema` |
| `tjwater data scada get\|list --kind device\|device-data\|element\|info` | `scada.py``GET` 查询接口 | `SCADA` 元数据 |
| `tjwater data scheme schema\|get\|list` | `schemes.py``GET` 接口 | 当前 project 方案查询 |
| `tjwater data extension keys\|get\|list` | `extension.py``GET` 查询接口 | 当前 project 扩展数据查询 |
| `tjwater data misc sensor-placements` | `GET /getallsensorplacements/` | 当前 project 传感器位置 |
| `tjwater data misc burst-location-results` | `GET /getallburstlocateresults/` | 当前 project 爆管定位结果 |
| `tjwater-cli data timeseries realtime links --start-time TIME --end-time TIME` | `GET /realtime/links` | 查询指定时间范围内的实时/模拟管道数据 |
| `tjwater-cli data timeseries realtime nodes --start-time TIME --end-time TIME` | `GET /realtime/nodes` | 查询指定时间范围内的实时/模拟节点数据 |
| `tjwater-cli data timeseries realtime simulation-by-id-time --id ID --type pipe\|junction --time TIME` | `GET /realtime/query/by-id-time` | 查询指定元素在指定时间点的模拟结果 |
| `tjwater-cli data timeseries realtime simulation-by-time-property --type pipe\|junction --time TIME --property PROPERTY` | `GET /realtime/query/by-time-property` | 查询指定时间点某类元素某属性的聚合模拟结果 |
| `tjwater-cli data timeseries scheme links --scheme SCHEME --start-time TIME --end-time TIME` | `GET /scheme/links``GET /scheme/links/{link_id}/field` | 方案管道数据 |
| `tjwater-cli data timeseries scheme node-field --node NODE --field FIELD` | `GET /scheme/nodes/{node_id}/field` | 方案节点字段 |
| `tjwater-cli data timeseries scheme simulation --query by-id-time\|by-scheme-time-property --scheme SCHEME --id ID --time TIME --property PROPERTY` | `GET /scheme/query/*` | 方案模拟查询 |
| `tjwater-cli data timeseries scada query --device-id ID --start-time TIME --end-time TIME [--device-id ID ...] [--field FIELD]` | `GET /scada/by-ids-time-range``GET /scada/by-ids-field-time-range` | SCADA 时序;CLI 把重复 `--device-id` 转换为后端逗号分隔参数 |
| `tjwater-cli data timeseries composite --kind scada-simulation\|element-simulation\|element-scada --feature FEATURE --start-time TIME --end-time TIME` | `GET /composite/*` | 复合查询,`--feature` 可重复 |
| `tjwater-cli data timeseries composite pipeline-health --pipe PIPE --start-time TIME --end-time TIME` | `GET /composite/pipeline-health-prediction` | 管道健康预测 |
| `tjwater-cli data scada schema --kind device\|device-data\|element\|info` | `GET /getscada*schema/` | `SCADA` 元数据 `schema` |
| `tjwater-cli data scada get\|list --kind device\|device-data\|element\|info` | `scada.py``GET` 查询接口 | `SCADA` 元数据 |
| `tjwater-cli data scheme schema\|get\|list` | `schemes.py``GET` 接口 | 当前 project 方案查询 |
| `tjwater-cli data extension keys\|get\|list` | `extension.py``GET` 查询接口 | 当前 project 扩展数据查询 |
| `tjwater-cli data misc sensor-placements` | `GET /getallsensorplacements/` | 当前 project 传感器位置 |
| `tjwater-cli data misc burst-location-results` | `GET /getallburstlocateresults/` | 当前 project 爆管定位结果 |
- `realtime` 是首批 simulation 结果的主读取域;CLI 可以按任务语义组合 `links``nodes``simulation-by-id-time``simulation-by-time-property`,但底层数据源仍以 `realtime.py` 为准。
- `realtime``scheme``composite` 等时间查询命令面向用户时仍按 **UTC+8** 输入;CLI/服务端负责转换为后端使用的 **UTC0** 条件进行检索。若返回结果直接包含时间戳,必须显式带时区,避免把存储时间和展示时间混淆。
@@ -434,8 +434,8 @@ POST /users/{user_id}/deactivate
| 命令 | 说明 |
|---|---|
| `tjwater help --json` | 返回当前 CLI 能力清单,供 Agent 发现可用命令 |
| `tjwater help COMMAND --json` | 返回某个命令的参数、输出、示例和推荐后续命令 |
| `tjwater-cli help` | 返回当前 CLI 能力清单,供 Agent 发现可用命令 |
| `tjwater-cli help COMMAND` | 返回某个命令的参数、输出、示例和推荐后续命令 |
输出补充约束:
@@ -491,7 +491,7 @@ POST /users/{user_id}/deactivate
"data": null,
"metadata": {},
"next_commands": [
"tjwater <command> --auth-context /path/to/auth-context.json"
"tjwater-cli <command> --auth-context /path/to/auth-context.json"
]
}
```
@@ -500,7 +500,7 @@ POST /users/{user_id}/deactivate
- `metadata` 至少建议包含:`request_id``server``duration_ms``generated_at`
- `next_commands` 是面向 agent 的推荐后续动作,不影响退出码和主结果语义。
- 所有 `help --json` 输出也应带 `schema_version`,便于 agent 做能力协商。
- 所有 `help` 输出也应带 `schema_version`,便于 agent 做能力协商。
## 后续开放条件