data-ops-cli 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/PKG-INFO +1 -1
  2. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/data_ops_cli.egg-info/PKG-INFO +1 -1
  3. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/data_ops_cli.egg-info/SOURCES.txt +2 -0
  4. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/__init__.py +1 -1
  5. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/factory.py +4 -0
  6. data_ops_cli-0.4.4/src/dataops_cli/cli/kb.py +234 -0
  7. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/skill.py +68 -0
  8. data_ops_cli-0.4.4/src/dataops_cli/cli/tag.py +297 -0
  9. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/workflow.py +10 -6
  10. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/README.md +0 -0
  11. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/pyproject.toml +0 -0
  12. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/setup.cfg +0 -0
  13. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/data_ops_cli.egg-info/dependency_links.txt +0 -0
  14. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/data_ops_cli.egg-info/entry_points.txt +0 -0
  15. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/data_ops_cli.egg-info/requires.txt +0 -0
  16. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/data_ops_cli.egg-info/top_level.txt +0 -0
  17. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/_compat.py +0 -0
  18. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/__init__.py +0 -0
  19. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/alarm.py +0 -0
  20. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/api.py +0 -0
  21. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/auth.py +0 -0
  22. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/data_table.py +0 -0
  23. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/dataset.py +0 -0
  24. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/datasource.py +0 -0
  25. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/dict_.py +0 -0
  26. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/hazard.py +0 -0
  27. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/metrics.py +0 -0
  28. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/ref.py +0 -0
  29. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/report.py +0 -0
  30. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/table.py +0 -0
  31. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/utils.py +0 -0
  32. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/cli/widget.py +0 -0
  33. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/exceptions.py +0 -0
  34. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/runner.py +0 -0
  35. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/schems.py +0 -0
  36. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/sdk.py +0 -0
  37. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/session.py +0 -0
  38. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/src/dataops_cli/utils.py +0 -0
  39. {data_ops_cli-0.4.2 → data_ops_cli-0.4.4}/tests/test_table_relation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-ops-cli
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Command-line wrapper around dataops_sdk for the DataOps platform.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-ops-cli
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Command-line wrapper around dataops_sdk for the DataOps platform.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -24,11 +24,13 @@ src/dataops_cli/cli/datasource.py
24
24
  src/dataops_cli/cli/dict_.py
25
25
  src/dataops_cli/cli/factory.py
26
26
  src/dataops_cli/cli/hazard.py
27
+ src/dataops_cli/cli/kb.py
27
28
  src/dataops_cli/cli/metrics.py
28
29
  src/dataops_cli/cli/ref.py
29
30
  src/dataops_cli/cli/report.py
30
31
  src/dataops_cli/cli/skill.py
31
32
  src/dataops_cli/cli/table.py
33
+ src/dataops_cli/cli/tag.py
32
34
  src/dataops_cli/cli/utils.py
33
35
  src/dataops_cli/cli/widget.py
34
36
  src/dataops_cli/cli/workflow.py
@@ -14,4 +14,4 @@ groups command modules under :mod:`dataops_cli.cli` and re-exports the package
14
14
  removed in ``dataops_cli`` 1.0.
15
15
  """
16
16
 
17
- __version__ = "0.4.2"
17
+ __version__ = "0.4.4"
@@ -53,11 +53,13 @@ def create_app():
53
53
  from dataops_cli.cli.datasource import datasource_typer
54
54
  from dataops_cli.cli.dict_ import dict_typer
55
55
  from dataops_cli.cli.hazard import hazard_typer
56
+ from dataops_cli.cli.kb import kb_typer
56
57
  from dataops_cli.cli.metrics import metrics_typer
57
58
  from dataops_cli.cli.ref import ref_typer
58
59
  from dataops_cli.cli.report import report_typer
59
60
  from dataops_cli.cli.skill import skill_typer
60
61
  from dataops_cli.cli.table import table_typer
62
+ from dataops_cli.cli.tag import tag_typer
61
63
  from dataops_cli.cli.widget import widget_typer
62
64
  from dataops_cli.cli.workflow import workflow_typer
63
65
 
@@ -81,6 +83,8 @@ def create_app():
81
83
  app.add_typer(widget_typer)
82
84
  app.add_typer(dict_typer)
83
85
  app.add_typer(hazard_typer)
86
+ app.add_typer(kb_typer)
87
+ app.add_typer(tag_typer)
84
88
 
85
89
  app.registered_callback = TyperInfo(callback=main)
86
90
 
@@ -0,0 +1,234 @@
1
+ """Knowledge Base(知识库)管理命令组.
2
+
3
+ - 服务层函数 (service_*):纯 Python 接口,返回原始数据
4
+ - CLI 命令:基于服务层的薄包装,负责格式化输出
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Annotated
11
+
12
+ import typer
13
+ from dataops_cli.cli.utils import print_list
14
+ from dataops_sdk.exceptions import DataOpsError
15
+ from dataops_sdk.schemas.wiki import KbType
16
+ from dataops_sdk.services.wiki import (
17
+ service_get_knowledge_base,
18
+ service_list_knowledge_bases,
19
+ service_list_knowledge_documents,
20
+ service_search_knowledge,
21
+ service_search_wiki_pages,
22
+ service_upload_knowledge_file,
23
+ )
24
+
25
+ kb_typer = typer.Typer(
26
+ name="kb",
27
+ help="知识库管理",
28
+ no_args_is_help=True,
29
+ )
30
+ doc_typer = typer.Typer(name="doc", help="知识库文档", no_args_is_help=True)
31
+ page_typer = typer.Typer(name="page", help="Wiki 页面", no_args_is_help=True)
32
+ kb_typer.add_typer(doc_typer)
33
+ kb_typer.add_typer(page_typer)
34
+
35
+
36
+ @kb_typer.command("list")
37
+ def kb_list_cmd(
38
+ kb_type: Annotated[
39
+ KbType | None,
40
+ typer.Option("--type", "-t", help="知识库类型:document / faq / wiki"),
41
+ ] = None,
42
+ page: Annotated[int, typer.Option("--page", help="页码", min=1)] = 1,
43
+ page_size: Annotated[int, typer.Option("--page-size", help="每页条数", min=1)] = 20,
44
+ ) -> None:
45
+ """列出知识库。
46
+
47
+ Examples:
48
+ >>> data-ops-cli kb list
49
+ >>> data-ops-cli kb list --type wiki
50
+ """
51
+ try:
52
+ result = service_list_knowledge_bases(page=page, page_size=page_size, kb_type=kb_type)
53
+ except DataOpsError as e:
54
+ typer.echo(f"错误: {e.message}")
55
+ raise typer.Exit(code=1) from None
56
+
57
+ if not result.items:
58
+ print("未查询到知识库")
59
+ return
60
+
61
+ print_list(
62
+ ("id", "name", "type", "knowledge_count"),
63
+ [
64
+ [it.id, it.name, it.type or "", it.knowledge_count if it.knowledge_count is not None else ""]
65
+ for it in result.items
66
+ ],
67
+ )
68
+ print(f"total={result.total} page={result.page} page_size={result.page_size}")
69
+
70
+
71
+ @kb_typer.command("detail", no_args_is_help=True)
72
+ def kb_detail_cmd(
73
+ kb_id: Annotated[str, typer.Argument(help="知识库 ID")],
74
+ ) -> None:
75
+ """查看知识库详情。
76
+
77
+ Examples:
78
+ >>> data-ops-cli kb detail <kb_id>
79
+ """
80
+ try:
81
+ kb = service_get_knowledge_base(kb_id)
82
+ except DataOpsError as e:
83
+ typer.echo(f"错误: {e.message}")
84
+ raise typer.Exit(code=1) from None
85
+
86
+ print_list(
87
+ ("field", "value"),
88
+ [
89
+ ["id", kb.id],
90
+ ["name", kb.name],
91
+ ["type", kb.type or ""],
92
+ ["description", kb.description or ""],
93
+ ["knowledge_count", kb.knowledge_count if kb.knowledge_count is not None else ""],
94
+ ["chunk_count", kb.chunk_count if kb.chunk_count is not None else ""],
95
+ ],
96
+ )
97
+
98
+
99
+ @doc_typer.command("list", no_args_is_help=True)
100
+ def doc_list_cmd(
101
+ kb_id: Annotated[str, typer.Argument(help="知识库 ID")],
102
+ keyword: Annotated[
103
+ str | None,
104
+ typer.Option("--keyword", "-k", help="关键词"),
105
+ ] = None,
106
+ page: Annotated[int, typer.Option("--page", help="页码", min=1)] = 1,
107
+ page_size: Annotated[int, typer.Option("--page-size", help="每页条数", min=1)] = 20,
108
+ ) -> None:
109
+ """列出知识库文档。
110
+
111
+ Examples:
112
+ >>> data-ops-cli kb doc list <kb_id>
113
+ >>> data-ops-cli kb doc list <kb_id> --keyword "故障处理"
114
+ """
115
+ try:
116
+ result = service_list_knowledge_documents(
117
+ kb_id, page=page, page_size=page_size, keyword=keyword
118
+ )
119
+ except DataOpsError as e:
120
+ typer.echo(f"错误: {e.message}")
121
+ raise typer.Exit(code=1) from None
122
+
123
+ if not result.items:
124
+ print("未查询到文档")
125
+ return
126
+
127
+ print_list(
128
+ ("id", "title", "file_name", "parse_status"),
129
+ [
130
+ [
131
+ it.id,
132
+ it.title or "",
133
+ it.file_name or "",
134
+ it.parse_status or "",
135
+ ]
136
+ for it in result.items
137
+ ],
138
+ )
139
+ print(f"total={result.total} page={result.page} page_size={result.page_size}")
140
+
141
+
142
+ @doc_typer.command("upload", no_args_is_help=True)
143
+ def doc_upload_cmd(
144
+ kb_id: Annotated[str, typer.Argument(help="知识库 ID")],
145
+ file: Annotated[Path, typer.Argument(help="本地文件路径", exists=True, readable=True)],
146
+ name: Annotated[
147
+ str | None,
148
+ typer.Option("--name", help="覆盖上传文件名"),
149
+ ] = None,
150
+ ) -> None:
151
+ """上传文件到知识库。
152
+
153
+ Examples:
154
+ >>> data-ops-cli kb doc upload <kb_id> ./manual.pdf
155
+ """
156
+ try:
157
+ doc = service_upload_knowledge_file(kb_id, file, file_name=name)
158
+ except (DataOpsError, FileNotFoundError) as e:
159
+ msg = getattr(e, "message", None) or str(e)
160
+ typer.echo(f"错误: {msg}")
161
+ raise typer.Exit(code=1) from None
162
+
163
+ print(f"上传成功: id={doc.id} title={doc.title or ''} file_name={doc.file_name or ''}")
164
+
165
+
166
+ @kb_typer.command("search", no_args_is_help=True)
167
+ def search_cmd(
168
+ keyword: Annotated[str, typer.Argument(help="检索关键词")],
169
+ limit: Annotated[int, typer.Option("--limit", help="返回条数", min=1)] = 20,
170
+ offset: Annotated[int, typer.Option("--offset", help="偏移量", min=0)] = 0,
171
+ ) -> None:
172
+ """全局关键词检索知识内容。
173
+
174
+ Examples:
175
+ >>> data-ops-cli kb search "网络割接"
176
+ """
177
+ try:
178
+ result = service_search_knowledge(keyword, limit=limit, offset=offset)
179
+ except DataOpsError as e:
180
+ typer.echo(f"错误: {e.message}")
181
+ raise typer.Exit(code=1) from None
182
+
183
+ if not result.items:
184
+ print("未检索到内容")
185
+ return
186
+
187
+ print_list(
188
+ ("title", "kb_name", "score", "content"),
189
+ [
190
+ [
191
+ it.title or it.knowledge_title or "",
192
+ it.kb_name or "",
193
+ f"{it.score:.4f}" if it.score is not None else "",
194
+ (it.content or "")[:80],
195
+ ]
196
+ for it in result.items
197
+ ],
198
+ )
199
+ print(f"total={result.total}")
200
+
201
+
202
+ @page_typer.command("search", no_args_is_help=True)
203
+ def page_search_cmd(
204
+ kb_id: Annotated[str, typer.Argument(help="知识库 ID")],
205
+ query: Annotated[str, typer.Argument(help="检索词")],
206
+ limit: Annotated[int, typer.Option("--limit", help="返回条数", min=1)] = 20,
207
+ ) -> None:
208
+ """检索知识库 Wiki 页面。
209
+
210
+ Examples:
211
+ >>> data-ops-cli kb page search <kb_id> "运维手册"
212
+ """
213
+ try:
214
+ pages = service_search_wiki_pages(kb_id, query, limit=limit)
215
+ except DataOpsError as e:
216
+ typer.echo(f"错误: {e.message}")
217
+ raise typer.Exit(code=1) from None
218
+
219
+ if not pages:
220
+ print("未检索到 Wiki 页面")
221
+ return
222
+
223
+ print_list(
224
+ ("slug", "title", "page_type", "summary"),
225
+ [
226
+ [
227
+ it.slug,
228
+ it.title or "",
229
+ it.page_type or "",
230
+ (it.summary or "")[:60],
231
+ ]
232
+ for it in pages
233
+ ],
234
+ )
@@ -23,6 +23,8 @@ from dataops_sdk.services import (
23
23
  service_pack_skill_zip,
24
24
  service_skill_call_by_name,
25
25
  service_skill_delete_sub_file,
26
+ service_skill_download,
27
+ service_skill_update,
26
28
  service_skill_upload,
27
29
  service_skill_upload_sub_file,
28
30
  )
@@ -151,6 +153,72 @@ def upload_cmd(
151
153
  print("上传成功")
152
154
 
153
155
 
156
+ @skill_typer.command("update", no_args_is_help=True)
157
+ def update_cmd(
158
+ name: Annotated[str, typer.Argument(help="技能业务名")],
159
+ file: Annotated[Path, typer.Argument(help="技能 ZIP 文件路径")],
160
+ dtype: Annotated[
161
+ MetaSkillSubType | None,
162
+ typer.Option("--type", help="技能子类型(可选)"),
163
+ ] = None,
164
+ title: Annotated[str | None, typer.Option("--title", help="技能简体中文名称")] = None,
165
+ ) -> None:
166
+ """全量覆盖更新已有技能。
167
+
168
+ Examples:
169
+ >>> data-ops-cli skill update my-skill ./my-skill.zip
170
+ >>> data-ops-cli skill update my-skill ./pkg.zip --type api --title "Foo Skill"
171
+ """
172
+ if not file.exists():
173
+ typer.echo(f"错误: 文件不存在 {file}")
174
+ raise typer.Exit(code=2)
175
+
176
+ try:
177
+ service_skill_update(
178
+ name=name,
179
+ file_path=str(file),
180
+ sub_type=dtype,
181
+ title=title,
182
+ )
183
+ except DataOpsError as e:
184
+ typer.echo(f"错误: {e.message}")
185
+ raise typer.Exit(code=1) from None
186
+ print("更新成功")
187
+
188
+
189
+ @skill_typer.command("download", no_args_is_help=True)
190
+ def download_cmd(
191
+ name: Annotated[str, typer.Argument(help="技能业务名")],
192
+ output: Annotated[
193
+ Path | None,
194
+ typer.Option("--output", "-o", help="输出 ZIP 路径"),
195
+ ] = None,
196
+ ) -> None:
197
+ """从平台下载技能 ZIP 包。
198
+
199
+ Examples:
200
+ >>> data-ops-cli skill download my-skill
201
+ >>> data-ops-cli skill download my-skill -o /tmp/my-skill.zip
202
+ >>> data-ops-cli skill download my-skill --output ./out/
203
+ """
204
+ output_path: str | None = None
205
+ if output is not None:
206
+ # Typer Path 会吞掉末尾分隔符;无 .zip 后缀时视为目录
207
+ output_path = (
208
+ str(output) if output.suffix.lower() == ".zip" else f"{output}/"
209
+ )
210
+
211
+ try:
212
+ zip_path = service_skill_download(name=name, output_path=output_path)
213
+ except DataOpsError as e:
214
+ typer.echo(f"错误: {e.message}")
215
+ raise typer.Exit(code=1) from None
216
+ except OSError as e:
217
+ typer.echo(f"错误: 写入失败 {e}")
218
+ raise typer.Exit(code=1) from None
219
+ print(f"已下载技能包: {zip_path}")
220
+
221
+
154
222
  @skill_typer.command("zip", no_args_is_help=True)
155
223
  def zip_cmd(
156
224
  folder: Annotated[Path, typer.Argument(help="技能源文件夹路径")],
@@ -0,0 +1,297 @@
1
+ """元标签管理命令组.
2
+
3
+ 提供标签树 list/detail,以及实体标签关联的 list/add/remove/set。
4
+ 标签作用域由 ``type`` 指定(如 skill / agent),非 meta-release 实体。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import Annotated
11
+
12
+ import typer
13
+ from dataops_sdk.exceptions import DataOpsError
14
+ from dataops_sdk.schemas.tag import TagTreeNode
15
+ from dataops_sdk.services.meta import release_list_list
16
+ from dataops_sdk.services.tag import (
17
+ flatten_tag_tree,
18
+ service_add_entity_tags,
19
+ service_get_tag_detail,
20
+ service_list_entity_tags,
21
+ service_list_entity_tags_by_codes,
22
+ service_list_tags,
23
+ service_remove_entity_tags,
24
+ service_set_entity_tags,
25
+ )
26
+
27
+ from dataops_cli.cli.utils import print_list
28
+
29
+ tag_typer = typer.Typer(name="tag", help="Manage meta tags.", no_args_is_help=True)
30
+ rel_typer = typer.Typer(name="rel", help="实体与标签关联", no_args_is_help=True)
31
+ tag_typer.add_typer(rel_typer)
32
+
33
+
34
+ def _print_tag_tree(
35
+ nodes: list[TagTreeNode], query: str | None = None, *, indent: int = 0
36
+ ) -> list[str]:
37
+ """缩进打印标签树;有 query 时仅保留匹配子树."""
38
+ pattern = re.compile(query) if query else None
39
+ lines: list[str] = []
40
+ for node in nodes:
41
+ child_lines = _print_tag_tree(node.children or [], query, indent=indent + 1)
42
+ title = node.title or ""
43
+ code = node.code or ""
44
+ self_match = pattern is None or bool(pattern.search(title) or pattern.search(code))
45
+ if not self_match and not child_lines:
46
+ continue
47
+ prefix = " " * indent
48
+ lines.append(f"{prefix}- {title}[{code}]({node.cuid})")
49
+ lines.extend(child_lines)
50
+ return lines
51
+
52
+
53
+ def _echo_final_tags(tag_type: str, entity_code: str, cuids: list[str]) -> None:
54
+ typer.echo(f"已更新 {tag_type}/{entity_code} 标签: {', '.join(cuids) if cuids else '(空)'}")
55
+
56
+
57
+ @tag_typer.command("list", no_args_is_help=True)
58
+ def list_tags(
59
+ tag_type: Annotated[str, typer.Argument(help="实体类型作用域,如 skill / agent")],
60
+ query: Annotated[
61
+ str | None, typer.Option("-q", "--query", help="标题/编码关键字(支持正则)")
62
+ ] = None,
63
+ as_tree: Annotated[bool, typer.Option("--tree", help="以树形缩进输出")] = False,
64
+ ) -> None:
65
+ """列出指定类型下的标签定义.
66
+
67
+ Examples:
68
+ >>> data-ops-cli tag list skill
69
+ >>> data-ops-cli tag list skill --tree
70
+ >>> data-ops-cli tag list skill -q "运维"
71
+ """
72
+ try:
73
+ tree = service_list_tags(tag_type)
74
+ except DataOpsError as e:
75
+ typer.echo(f"错误: {e.message}")
76
+ raise typer.Exit(code=1) from None
77
+
78
+ if as_tree:
79
+ lines = _print_tag_tree(tree, query)
80
+ if not lines:
81
+ typer.echo("未查询到匹配标签")
82
+ return
83
+ typer.echo("\n".join(lines))
84
+ return
85
+
86
+ flat = flatten_tag_tree(tree)
87
+ if query:
88
+ pattern = re.compile(query)
89
+ flat = [n for n in flat if pattern.search(n.title or "") or pattern.search(n.code or "")]
90
+
91
+ if not flat:
92
+ typer.echo("未查询到匹配标签")
93
+ return
94
+
95
+ print_list(
96
+ ["code", "title", "cuid", "parentId"],
97
+ [[n.code, n.title, n.cuid, n.parentId] for n in flat],
98
+ )
99
+
100
+
101
+ @tag_typer.command("detail", no_args_is_help=True)
102
+ def get_tag_detail(
103
+ tag_type: Annotated[str, typer.Argument(help="实体类型作用域,如 skill")],
104
+ key: Annotated[str, typer.Argument(help="标签 cuid 或 code")],
105
+ full: Annotated[bool, typer.Option("--full", help="显示完整字段")] = False,
106
+ ) -> None:
107
+ """获取标签详情(从标签树中按 cuid/code 查找).
108
+
109
+ Examples:
110
+ >>> data-ops-cli tag detail skill my-tag-code
111
+ >>> data-ops-cli tag detail skill <cuid> --full
112
+ """
113
+ try:
114
+ node = service_get_tag_detail(tag_type, key)
115
+ except DataOpsError as e:
116
+ typer.echo(f"错误: {e.message}")
117
+ raise typer.Exit(code=1) from None
118
+
119
+ if not full:
120
+ print_list(
121
+ ["code", "title", "cuid", "parentId", "color"],
122
+ [[node.code, node.title, node.cuid, node.parentId, node.color]],
123
+ )
124
+ return
125
+
126
+ print_list(
127
+ [
128
+ "code",
129
+ "title",
130
+ "cuid",
131
+ "parentId",
132
+ "type",
133
+ "icon",
134
+ "color",
135
+ "sort",
136
+ "remark",
137
+ "createUser",
138
+ "createTime",
139
+ "lastModifyUser",
140
+ "lastModifyTime",
141
+ ],
142
+ [
143
+ [
144
+ node.code,
145
+ node.title,
146
+ node.cuid,
147
+ node.parentId,
148
+ node.type,
149
+ node.icon,
150
+ node.color,
151
+ node.sort,
152
+ node.remark,
153
+ node.createUser,
154
+ node.createTime,
155
+ node.lastModifyUser,
156
+ node.lastModifyTime,
157
+ ]
158
+ ],
159
+ )
160
+
161
+
162
+ @rel_typer.command("list", no_args_is_help=True)
163
+ def list_entity_tags(
164
+ tag_type: Annotated[str, typer.Argument(help="实体类型作用域,如 skill")],
165
+ entity_code: Annotated[str, typer.Argument(help="实体编码")],
166
+ ) -> None:
167
+ """列出实体已绑定的标签.
168
+
169
+ Examples:
170
+ >>> data-ops-cli tag rel list skill my-skill-code
171
+ """
172
+ try:
173
+ tags = service_list_entity_tags(tag_type, entity_code)
174
+ except DataOpsError as e:
175
+ typer.echo(f"错误: {e.message}")
176
+ raise typer.Exit(code=1) from None
177
+
178
+ if not tags:
179
+ typer.echo("未绑定标签")
180
+ return
181
+
182
+ print_list(
183
+ ["cuid", "title", "color", "icon"],
184
+ [[t.cuid, t.title, t.color, t.icon] for t in tags],
185
+ )
186
+
187
+
188
+ @rel_typer.command("map", no_args_is_help=True)
189
+ def map_entity_tags(
190
+ tag_type: Annotated[str, typer.Argument(help="实体类型作用域,如 skill")],
191
+ codes: Annotated[
192
+ list[str] | None,
193
+ typer.Option("--code", help="实体编码(可多次;省略则拉取该类型全部实体)"),
194
+ ] = None,
195
+ ) -> None:
196
+ """批量展示「实体 → 已绑定标签」映射.
197
+
198
+ 未传 ``--code`` 时,通过 meta-release list 拉取 ``tag_type`` 对应的全部实体,
199
+ 再调用 ``listByCodes`` 批量查标签。未绑定的实体 ``tags`` 列为空,便于盘点补标。
200
+
201
+ Examples:
202
+ >>> data-ops-cli tag rel map skill
203
+ >>> data-ops-cli tag rel map skill --code sk-a --code sk-b
204
+ """
205
+ try:
206
+ if codes:
207
+ entity_codes = list(dict.fromkeys(codes))
208
+ titles: dict[str, str] = {}
209
+ else:
210
+ metas = release_list_list(tag_type)
211
+ entity_codes = [m.code for m in metas if m.code]
212
+ titles = {m.code: m.title or m.name or "" for m in metas if m.code}
213
+ mapping = service_list_entity_tags_by_codes(tag_type, entity_codes)
214
+ except DataOpsError as e:
215
+ typer.echo(f"错误: {e.message}")
216
+ raise typer.Exit(code=1) from None
217
+
218
+ if not entity_codes:
219
+ typer.echo("未查询到实体")
220
+ return
221
+
222
+ rows = []
223
+ for code in entity_codes:
224
+ tags = mapping.get(code) or []
225
+ tag_titles = ",".join(t.title or t.cuid for t in tags)
226
+ tag_cuids = ",".join(t.cuid for t in tags)
227
+ rows.append([code, titles.get(code, ""), tag_titles, tag_cuids])
228
+
229
+ print_list(["code", "title", "tags", "tag_cuids"], rows)
230
+
231
+
232
+ @rel_typer.command("add", no_args_is_help=True)
233
+ def add_entity_tags(
234
+ tag_type: Annotated[str, typer.Argument(help="实体类型作用域,如 skill")],
235
+ entity_code: Annotated[str, typer.Argument(help="实体编码")],
236
+ tags: Annotated[
237
+ list[str],
238
+ typer.Option("--tag", help="标签 cuid 或 code(可多次传入)"),
239
+ ],
240
+ ) -> None:
241
+ """给实体追加标签(读-改-写;自动补齐根标签).
242
+
243
+ Examples:
244
+ >>> data-ops-cli tag rel add skill my-skill-code --tag my-tag-code
245
+ >>> data-ops-cli tag rel add skill my-skill-code --tag root --tag child
246
+ """
247
+ try:
248
+ final = service_add_entity_tags(tag_type, entity_code, tags)
249
+ except DataOpsError as e:
250
+ typer.echo(f"错误: {e.message}")
251
+ raise typer.Exit(code=1) from None
252
+ _echo_final_tags(tag_type, entity_code, final)
253
+
254
+
255
+ @rel_typer.command("remove", no_args_is_help=True)
256
+ def remove_entity_tags(
257
+ tag_type: Annotated[str, typer.Argument(help="实体类型作用域,如 skill")],
258
+ entity_code: Annotated[str, typer.Argument(help="实体编码")],
259
+ tags: Annotated[
260
+ list[str],
261
+ typer.Option("--tag", help="标签 cuid 或 code(可多次传入)"),
262
+ ],
263
+ ) -> None:
264
+ """从实体移除指定标签.
265
+
266
+ Examples:
267
+ >>> data-ops-cli tag rel remove skill my-skill-code --tag my-tag-code
268
+ """
269
+ try:
270
+ final = service_remove_entity_tags(tag_type, entity_code, tags)
271
+ except DataOpsError as e:
272
+ typer.echo(f"错误: {e.message}")
273
+ raise typer.Exit(code=1) from None
274
+ _echo_final_tags(tag_type, entity_code, final)
275
+
276
+
277
+ @rel_typer.command("set", no_args_is_help=True)
278
+ def set_entity_tags(
279
+ tag_type: Annotated[str, typer.Argument(help="实体类型作用域,如 skill")],
280
+ entity_code: Annotated[str, typer.Argument(help="实体编码")],
281
+ tags: Annotated[
282
+ list[str] | None,
283
+ typer.Option("--tag", help="标签 cuid 或 code(可多次;省略则清空)"),
284
+ ] = None,
285
+ ) -> None:
286
+ """全量设置实体标签;不传 ``--tag`` 则清空.
287
+
288
+ Examples:
289
+ >>> data-ops-cli tag rel set skill my-skill-code --tag root --tag child
290
+ >>> data-ops-cli tag rel set skill my-skill-code
291
+ """
292
+ try:
293
+ final = service_set_entity_tags(tag_type, entity_code, tags or [])
294
+ except DataOpsError as e:
295
+ typer.echo(f"错误: {e.message}")
296
+ raise typer.Exit(code=1) from None
297
+ _echo_final_tags(tag_type, entity_code, final)
@@ -160,6 +160,7 @@ def get_workflow_template(
160
160
  @workflow_typer.command("run", no_args_is_help=True)
161
161
  def run_workflow(
162
162
  code: Annotated[str, typer.Argument(help="工作流编码")],
163
+ start_node_id: Annotated[str, typer.Argument(help="启动节点ID")],
163
164
  params: Annotated[
164
165
  str | None,
165
166
  typer.Option(help='启动参数 JSON 字符串,例如 \'{"key":"value"}\''),
@@ -172,14 +173,15 @@ def run_workflow(
172
173
 
173
174
  Args:
174
175
  code: 工作流编码。
176
+ start_node_id: 启动节点ID。
175
177
  params: 可选启动参数 JSON 字符串。
176
178
 
177
179
  Examples:
178
180
  无参启动
179
- >>> data-ops-cli workflow run wf_code_001
181
+ >>> data-ops-cli workflow run wf_code_001 manually-trigger
180
182
 
181
183
  带参数启动
182
- >>> data-ops-cli workflow run wf_code_001 --params '{"date":"2026-07-16"}'
184
+ >>> data-ops-cli workflow run wf_code_001 manually-trigger --params '{"date":"2026-07-16"}'
183
185
  """
184
186
  try:
185
187
  params_dict = json.loads(params) if params else None
@@ -188,7 +190,7 @@ def run_workflow(
188
190
  raise typer.Exit(code=1) from None
189
191
 
190
192
  try:
191
- result = service_start_workflow(code, params_dict)
193
+ result = service_start_workflow(code, start_node_id, params_dict)
192
194
  except DataOpsError as e:
193
195
  typer.echo(f"错误: {e.message}")
194
196
  raise typer.Exit(code=1) from None
@@ -199,6 +201,7 @@ def run_workflow(
199
201
  @workflow_typer.command("run-async", no_args_is_help=True)
200
202
  def run_workflow_async(
201
203
  code: Annotated[str, typer.Argument(help="工作流编码")],
204
+ start_node_id: Annotated[str, typer.Argument(help="启动节点ID")],
202
205
  params: Annotated[
203
206
  str | None,
204
207
  typer.Option(help='启动参数 JSON 字符串,例如 \'{"key":"value"}\''),
@@ -211,14 +214,15 @@ def run_workflow_async(
211
214
 
212
215
  Args:
213
216
  code: 工作流编码。
217
+ start_node_id: 启动节点ID。
214
218
  params: 可选启动参数 JSON 字符串。
215
219
 
216
220
  Examples:
217
221
  无参异步启动
218
- >>> data-ops-cli workflow run-async wf_code_001
222
+ >>> data-ops-cli workflow run-async wf_code_001 manually-trigger
219
223
 
220
224
  带参数异步启动
221
- >>> data-ops-cli workflow run-async wf_code_001 --params '{"date":"2026-07-16"}'
225
+ >>> data-ops-cli workflow run-async wf_code_001 manually-trigger --params '{"date":"2026-07-16"}'
222
226
  """
223
227
  try:
224
228
  params_dict = json.loads(params) if params else None
@@ -227,7 +231,7 @@ def run_workflow_async(
227
231
  raise typer.Exit(code=1) from None
228
232
 
229
233
  try:
230
- result = service_start_workflow_async(code, params_dict)
234
+ result = service_start_workflow_async(code, start_node_id, params_dict)
231
235
  except DataOpsError as e:
232
236
  typer.echo(f"错误: {e.message}")
233
237
  raise typer.Exit(code=1) from None
File without changes
File without changes