拆分代码;约束cli命令
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
build/
|
||||
__pycache__/
|
||||
+31
-20
@@ -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
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
from tjwater_cli.main import console_entry
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
console_entry()
|
||||
@@ -0,0 +1 @@
|
||||
pyinstaller>=6.11,<7
|
||||
@@ -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
@@ -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 "$@"
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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 做能力协商。
|
||||
|
||||
## 后续开放条件
|
||||
|
||||
|
||||
Reference in New Issue
Block a user