tinet-agent-cli 0.1.0.dev36__py3-none-any.whl

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 (104) hide show
  1. taco/__init__.py +1 -0
  2. taco/agent_openapi/__init__.py +1 -0
  3. taco/agent_openapi/catalog.py +198 -0
  4. taco/agent_openapi/models.py +47 -0
  5. taco/agent_openapi/parameters.py +111 -0
  6. taco/agent_openapi/service.py +73 -0
  7. taco/aikb/__init__.py +4 -0
  8. taco/aikb/catalog.py +380 -0
  9. taco/aikb/executors.py +157 -0
  10. taco/aikb/file_input.py +55 -0
  11. taco/aikb/models.py +42 -0
  12. taco/aikb/parameters.py +135 -0
  13. taco/aikb/service.py +68 -0
  14. taco/assets/__init__.py +1 -0
  15. taco/assets/agent_openapi/catalog.json +14 -0
  16. taco/assets/agent_openapi/operations/conversation/list-conversations.json +67 -0
  17. taco/assets/agent_openapi/operations/message/list-conversation-messages.json +80 -0
  18. taco/assets/agent_openapi/operations/trace/list-message-traces.json +72 -0
  19. taco/assets/aikb/catalog.json +41 -0
  20. taco/assets/aikb/operations/conversation/chat-conversation-on-open.json +99 -0
  21. taco/assets/aikb/operations/directory/delete-directory.json +81 -0
  22. taco/assets/aikb/operations/directory/edit-directory.json +97 -0
  23. taco/assets/aikb/operations/directory/list-directory-tree.json +106 -0
  24. taco/assets/aikb/operations/directory/save-directory.json +109 -0
  25. taco/assets/aikb/operations/faq/create-faq.json +193 -0
  26. taco/assets/aikb/operations/faq/delete-faq.json +81 -0
  27. taco/assets/aikb/operations/faq/describe-faq.json +82 -0
  28. taco/assets/aikb/operations/faq/edit-faq.json +193 -0
  29. taco/assets/aikb/operations/faq/list-faqs.json +136 -0
  30. taco/assets/aikb/operations/file/create-file.json +145 -0
  31. taco/assets/aikb/operations/file/delete-files.json +110 -0
  32. taco/assets/aikb/operations/file/describe-file.json +82 -0
  33. taco/assets/aikb/operations/file/get-file-upload-url.json +154 -0
  34. taco/assets/aikb/operations/file/list-files.json +146 -0
  35. taco/assets/aikb/operations/media/describe-faq-media-url.json +98 -0
  36. taco/assets/aikb/operations/media/describe-file-media-url.json +90 -0
  37. taco/assets/aikb/operations/oss/signature-for-upload.json +85 -0
  38. taco/assets/aikb/operations/recycle-bin/list-recycled-items.json +151 -0
  39. taco/assets/aikb/operations/repository/describe-bot-repository.json +71 -0
  40. taco/assets/aikb/operations/repository/list-private-repositories.json +75 -0
  41. taco/assets/aikb/operations/repository/list-public-repositories.json +75 -0
  42. taco/assets/aikb/operations/search/search-knowledge-on-open.json +180 -0
  43. taco/assets/examples/README.md +3 -0
  44. taco/assets/examples/agent-basic.json +16 -0
  45. taco/assets/examples/agent-builtin-tool.json +28 -0
  46. taco/assets/examples/agent-workflow-tool.json +30 -0
  47. taco/assets/examples/chatflow-basic.json +24 -0
  48. taco/assets/examples/workflow-http.json +34 -0
  49. taco/assets/manifest.json +12 -0
  50. taco/assets/scenarios/README.md +3 -0
  51. taco/assets/scenarios/agent-basic.json +80 -0
  52. taco/assets/scenarios/agent-builtin-tool.json +116 -0
  53. taco/assets/scenarios/agent-workflow-tool.json +277 -0
  54. taco/assets/schemas/README.md +3 -0
  55. taco/assets/schemas/agent-tool.json +71 -0
  56. taco/assets/schemas/agent.json +199 -0
  57. taco/assets/schemas/chatflow.json +1048 -0
  58. taco/assets/schemas/workflow.http.json +1052 -0
  59. taco/assets/schemas/workflow.json +1052 -0
  60. taco/assets/skills/README.md +3 -0
  61. taco/assets/skills/taco/SKILL.md +68 -0
  62. taco/assets/skills/taco/manifest.json +10 -0
  63. taco/cli.py +127 -0
  64. taco/commands/__init__.py +1 -0
  65. taco/commands/agent.py +760 -0
  66. taco/commands/aikb.py +132 -0
  67. taco/commands/app.py +151 -0
  68. taco/commands/category.py +37 -0
  69. taco/commands/chatflow.py +183 -0
  70. taco/commands/credential.py +222 -0
  71. taco/commands/discovery.py +409 -0
  72. taco/commands/model.py +55 -0
  73. taco/commands/profile.py +124 -0
  74. taco/commands/tool.py +61 -0
  75. taco/commands/workflow.py +714 -0
  76. taco/core/__init__.py +1 -0
  77. taco/core/access_token_manager.py +98 -0
  78. taco/core/api_client.py +263 -0
  79. taco/core/assets.py +81 -0
  80. taco/core/config_store.py +209 -0
  81. taco/core/context.py +19 -0
  82. taco/core/developer_center_client.py +165 -0
  83. taco/core/errors.py +19 -0
  84. taco/core/file_lock.py +80 -0
  85. taco/core/http_headers.py +21 -0
  86. taco/core/openapi_signer.py +221 -0
  87. taco/core/output.py +72 -0
  88. taco/core/profile_manager.py +217 -0
  89. taco/core/profile_resolver.py +151 -0
  90. taco/core/secure_file.py +76 -0
  91. taco/release.py +363 -0
  92. taco/services/__init__.py +1 -0
  93. taco/services/agent_service.py +314 -0
  94. taco/services/app_service.py +153 -0
  95. taco/services/category_service.py +57 -0
  96. taco/services/model_service.py +117 -0
  97. taco/services/tool_service.py +482 -0
  98. taco/services/workflow_compiler.py +1665 -0
  99. taco/services/workflow_service.py +652 -0
  100. taco/services/workflow_tool_service.py +439 -0
  101. tinet_agent_cli-0.1.0.dev36.dist-info/METADATA +158 -0
  102. tinet_agent_cli-0.1.0.dev36.dist-info/RECORD +104 -0
  103. tinet_agent_cli-0.1.0.dev36.dist-info/WHEEL +4 -0
  104. tinet_agent_cli-0.1.0.dev36.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import sys
7
+ from json import JSONDecodeError
8
+
9
+ import typer
10
+
11
+ from taco.core.context import get_cli_context
12
+ from taco.core.errors import TacoError
13
+ from taco.core.output import emit_success, exit_with_error
14
+ from taco.services.tool_service import ToolService, validate_credentials_payload
15
+
16
+
17
+ credential_app = typer.Typer(help="内置工具 Provider 凭据管理")
18
+
19
+
20
+ @credential_app.command("schema", help="读取 Provider 凭据字段定义,不读取值。")
21
+ def credential_schema(
22
+ ctx: typer.Context,
23
+ provider: str = typer.Argument(..., help="完整 Provider ID。"),
24
+ ) -> None:
25
+ cli_context = get_cli_context(ctx)
26
+ try:
27
+ schema = ToolService().credential_schema(
28
+ provider,
29
+ profile=cli_context.profile,
30
+ )
31
+ except TacoError as error:
32
+ exit_with_error(ctx, error, json_default=True)
33
+ emit_success(
34
+ ctx,
35
+ {"provider": provider, "schema": schema},
36
+ json_default=True,
37
+ )
38
+
39
+
40
+ @credential_app.command("status", help="读取凭据 schema 与服务端脱敏状态。")
41
+ def credential_status(
42
+ ctx: typer.Context,
43
+ provider: str = typer.Argument(..., help="完整 Provider ID。"),
44
+ ) -> None:
45
+ cli_context = get_cli_context(ctx)
46
+ try:
47
+ result = ToolService().credential_status(
48
+ provider,
49
+ profile=cli_context.profile,
50
+ )
51
+ except TacoError as error:
52
+ exit_with_error(ctx, error, json_default=True)
53
+ emit_success(ctx, result, json_default=True)
54
+
55
+
56
+ @credential_app.command("set", help="从 stdin 或环境变量设置 Provider 凭据。")
57
+ def set_credentials(
58
+ ctx: typer.Context,
59
+ provider: str = typer.Argument(..., help="完整 Provider ID。"),
60
+ from_stdin: bool = typer.Option(
61
+ False,
62
+ "--from-stdin",
63
+ help="从 stdin 读取凭据 JSON 对象。",
64
+ ),
65
+ from_env: str | None = typer.Option(
66
+ None,
67
+ "--from-env",
68
+ help="从指定环境变量读取完整凭据 JSON 对象。",
69
+ ),
70
+ env_prefix: str | None = typer.Option(
71
+ None,
72
+ "--env-prefix",
73
+ help="按 schema 字段从指定环境变量前缀读取。",
74
+ ),
75
+ yes: bool = typer.Option(False, "--yes", help="确认写入租户共享凭据。"),
76
+ ) -> None:
77
+ if not yes:
78
+ exit_with_error(
79
+ ctx,
80
+ TacoError(
81
+ code="CREDENTIAL_CONFIRM_REQUIRED",
82
+ message="写入 Provider 凭据前必须显式传入 --yes。",
83
+ exit_code=2,
84
+ ),
85
+ json_default=True,
86
+ )
87
+ source_count = sum(
88
+ (
89
+ int(from_stdin),
90
+ int(from_env is not None),
91
+ int(env_prefix is not None),
92
+ )
93
+ )
94
+ if source_count != 1:
95
+ exit_with_error(
96
+ ctx,
97
+ TacoError(
98
+ code="CREDENTIAL_SOURCE_REQUIRED",
99
+ message=(
100
+ "必须且只能选择 --from-stdin、--from-env 或 --env-prefix。"
101
+ ),
102
+ exit_code=2,
103
+ ),
104
+ json_default=True,
105
+ )
106
+
107
+ cli_context = get_cli_context(ctx)
108
+ try:
109
+ service = ToolService()
110
+ schema = service.credential_schema(
111
+ provider,
112
+ profile=cli_context.profile,
113
+ )
114
+ credentials = (
115
+ _read_credentials_from_stdin()
116
+ if from_stdin
117
+ else (
118
+ _read_credentials_from_named_env(from_env)
119
+ if from_env is not None
120
+ else _read_credentials_from_env(schema, env_prefix or "")
121
+ )
122
+ )
123
+ validate_credentials_payload(credentials, schema)
124
+ service.set_credentials(
125
+ provider,
126
+ credentials,
127
+ profile=cli_context.profile,
128
+ )
129
+ except TacoError as error:
130
+ exit_with_error(ctx, error, json_default=True)
131
+ emit_success(
132
+ ctx,
133
+ {
134
+ "provider": provider,
135
+ "updated": True,
136
+ "fields": sorted(credentials),
137
+ },
138
+ json_default=True,
139
+ )
140
+
141
+
142
+ @credential_app.command("delete", help="删除租户共享的 Provider 凭据。")
143
+ def delete_credentials(
144
+ ctx: typer.Context,
145
+ provider: str = typer.Argument(..., help="完整 Provider ID。"),
146
+ yes: bool = typer.Option(False, "--yes", help="确认删除共享凭据。"),
147
+ ) -> None:
148
+ if not yes:
149
+ exit_with_error(
150
+ ctx,
151
+ TacoError(
152
+ code="CREDENTIAL_DELETE_CONFIRM_REQUIRED",
153
+ message="删除 Provider 凭据前必须显式传入 --yes。",
154
+ exit_code=2,
155
+ ),
156
+ json_default=True,
157
+ )
158
+ cli_context = get_cli_context(ctx)
159
+ try:
160
+ ToolService().delete_credentials(
161
+ provider,
162
+ profile=cli_context.profile,
163
+ )
164
+ except TacoError as error:
165
+ exit_with_error(ctx, error, json_default=True)
166
+ emit_success(
167
+ ctx,
168
+ {"provider": provider, "deleted": True},
169
+ json_default=True,
170
+ )
171
+
172
+
173
+ def _read_credentials_from_stdin() -> dict[str, object]:
174
+ return _parse_credentials_json(sys.stdin.read(), source="stdin")
175
+
176
+
177
+ def _read_credentials_from_named_env(env_name: str) -> dict[str, object]:
178
+ value = os.getenv(env_name)
179
+ if value is None:
180
+ raise TacoError(
181
+ code="CREDENTIAL_INPUT_INVALID",
182
+ message=f"环境变量 {env_name} 不存在。",
183
+ exit_code=2,
184
+ )
185
+ return _parse_credentials_json(value, source=f"环境变量 {env_name}")
186
+
187
+
188
+ def _parse_credentials_json(
189
+ value: str,
190
+ *,
191
+ source: str,
192
+ ) -> dict[str, object]:
193
+ try:
194
+ payload = json.loads(value)
195
+ except (JSONDecodeError, UnicodeError) as error:
196
+ raise TacoError(
197
+ code="CREDENTIAL_INPUT_INVALID",
198
+ message=f"{source} 必须是合法的凭据 JSON 对象。",
199
+ exit_code=2,
200
+ ) from error
201
+ if not isinstance(payload, dict):
202
+ raise TacoError(
203
+ code="CREDENTIAL_INPUT_INVALID",
204
+ message=f"{source} 凭据必须是 JSON 对象。",
205
+ exit_code=2,
206
+ )
207
+ return payload
208
+
209
+
210
+ def _read_credentials_from_env(
211
+ schema: list[dict[str, object]],
212
+ prefix: str,
213
+ ) -> dict[str, object]:
214
+ credentials: dict[str, object] = {}
215
+ for field in schema:
216
+ name = field.get("name")
217
+ if not isinstance(name, str):
218
+ continue
219
+ env_name = prefix + re.sub(r"[^A-Za-z0-9]", "_", name).upper()
220
+ if env_name in os.environ:
221
+ credentials[name] = os.environ[env_name]
222
+ return credentials
@@ -0,0 +1,409 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import shutil
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Any, Literal
9
+ from uuid import uuid4
10
+
11
+ import typer
12
+ from typer.main import get_command
13
+
14
+ from taco.core.assets import list_json_assets, load_json_asset, load_skill_files
15
+ from taco.core.errors import TacoError
16
+ from taco.core.output import emit_success, exit_with_error
17
+
18
+
19
+ scenario_app = typer.Typer(help="场景 playbook 发现")
20
+ example_app = typer.Typer(help="内置 DSL 示例发现")
21
+ skills_app = typer.Typer(help="Agent Skill 安装与状态")
22
+ schema_app = typer.Typer(help="读取当前 CLI 内置 DSL schema")
23
+
24
+ _SKILL_TARGETS = {
25
+ "cursor": Path.home() / ".cursor" / "skills" / "taco",
26
+ "codex": Path.home() / ".agents" / "skills" / "taco",
27
+ "openclaw": Path.home() / ".openclaw" / "skills" / "taco",
28
+ }
29
+ OutputFormat = Literal["text", "json"]
30
+ AgentTarget = Literal["cursor", "codex", "openclaw"]
31
+ _INTERNAL_PARAMETER_NAMES = {"install_completion", "show_completion"}
32
+
33
+
34
+ def machine_help(
35
+ ctx: typer.Context,
36
+ output: OutputFormat = typer.Option(
37
+ "text",
38
+ "-o",
39
+ "--output",
40
+ help="text 或 json。",
41
+ ),
42
+ ) -> None:
43
+ from taco.cli import app
44
+
45
+ tree = _serialize_command(get_command(app), name="taco")
46
+ tree["format_version"] = 1
47
+ if output == "json":
48
+ emit_success(ctx, tree, json_default=True)
49
+ return
50
+ if output != "text":
51
+ exit_with_error(ctx, _invalid_output_error(), json_default=True)
52
+ typer.echo("\n".join(_command_lines(tree)))
53
+
54
+
55
+ @schema_app.command("show", help="读取指定的内置 DSL schema。")
56
+ def schema_command(
57
+ ctx: typer.Context,
58
+ resource: str = typer.Argument(..., help="资源名称,例如 agent。"),
59
+ output: OutputFormat = typer.Option(
60
+ "text",
61
+ "-o",
62
+ "--output",
63
+ help="text 或 json。",
64
+ ),
65
+ ) -> None:
66
+ try:
67
+ schema = load_json_asset("schemas", resource)
68
+ except TacoError as error:
69
+ exit_with_error(ctx, error, json_default=True)
70
+ _emit_discovery_payload(ctx, schema, output)
71
+
72
+
73
+ @schema_app.command("list", help="列出当前 CLI 内置 DSL schema。")
74
+ def list_schemas(
75
+ ctx: typer.Context,
76
+ output: OutputFormat = typer.Option(
77
+ "text",
78
+ "-o",
79
+ "--output",
80
+ help="text 或 json。",
81
+ ),
82
+ ) -> None:
83
+ _emit_discovery_payload(
84
+ ctx,
85
+ {"items": list_json_assets("schemas")},
86
+ output,
87
+ )
88
+
89
+
90
+ @example_app.command("describe", help="读取当前 CLI 内置的 DSL 示例。")
91
+ def describe_example(
92
+ ctx: typer.Context,
93
+ example_id: str = typer.Argument(..., help="示例 ID。"),
94
+ output: OutputFormat = typer.Option(
95
+ "text",
96
+ "-o",
97
+ "--output",
98
+ help="text 或 json。",
99
+ ),
100
+ ) -> None:
101
+ try:
102
+ example = load_json_asset("examples", example_id)
103
+ except TacoError as error:
104
+ exit_with_error(ctx, error, json_default=True)
105
+ _emit_discovery_payload(ctx, example, output)
106
+
107
+
108
+ @example_app.command("list", help="列出当前 CLI 内置的 DSL 示例。")
109
+ def list_examples(
110
+ ctx: typer.Context,
111
+ output: OutputFormat = typer.Option(
112
+ "text",
113
+ "-o",
114
+ "--output",
115
+ help="text 或 json。",
116
+ ),
117
+ ) -> None:
118
+ _emit_discovery_payload(
119
+ ctx,
120
+ {"items": list_json_assets("examples")},
121
+ output,
122
+ )
123
+
124
+
125
+ @scenario_app.command("describe", help="读取内置场景步骤与风险元数据。")
126
+ def describe_scenario(
127
+ ctx: typer.Context,
128
+ scenario_id: str = typer.Argument(..., help="场景 ID。"),
129
+ output: OutputFormat = typer.Option(
130
+ "text",
131
+ "-o",
132
+ "--output",
133
+ help="text 或 json。",
134
+ ),
135
+ ) -> None:
136
+ try:
137
+ scenario = load_json_asset("scenarios", scenario_id)
138
+ except TacoError as error:
139
+ exit_with_error(ctx, error, json_default=True)
140
+ _emit_discovery_payload(ctx, scenario, output)
141
+
142
+
143
+ @scenario_app.command("list", help="列出当前 CLI 内置的场景 playbook。")
144
+ def list_scenarios(
145
+ ctx: typer.Context,
146
+ output: OutputFormat = typer.Option(
147
+ "text",
148
+ "-o",
149
+ "--output",
150
+ help="text 或 json。",
151
+ ),
152
+ ) -> None:
153
+ payload = {"items": list_json_assets("scenarios")}
154
+ _emit_discovery_payload(ctx, payload, output)
155
+
156
+
157
+ @skills_app.command("install", help="安装或输出当前 wheel 内置的 taco Skill。")
158
+ def install_skill(
159
+ ctx: typer.Context,
160
+ agent: AgentTarget = typer.Option(
161
+ "cursor",
162
+ "--agent",
163
+ help="cursor、codex 或 openclaw。",
164
+ ),
165
+ target_dir: Path | None = typer.Option(None, "--dir", help="自定义 Skill 目录。"),
166
+ yes: bool = typer.Option(False, "--yes", help="确认写入或覆盖 Skill。"),
167
+ stdout: bool = typer.Option(False, "--stdout", help="仅输出 SKILL.md,不写文件。"),
168
+ ) -> None:
169
+ try:
170
+ files = load_skill_files()
171
+ if stdout:
172
+ typer.echo(files["SKILL.md"], nl=False)
173
+ return
174
+ if not yes:
175
+ raise TacoError(
176
+ code="SKILL_INSTALL_CONFIRM_REQUIRED",
177
+ message="安装 Skill 前必须显式传入 --yes。",
178
+ exit_code=2,
179
+ )
180
+ target = _skill_target(agent, target_dir)
181
+ _install_skill_files(target, files)
182
+ except TacoError as error:
183
+ exit_with_error(ctx, error, json_default=True)
184
+ except OSError:
185
+ exit_with_error(
186
+ ctx,
187
+ TacoError(
188
+ code="SKILL_INSTALL_FAILED",
189
+ message="无法写入 Agent Skill 目录。",
190
+ exit_code=2,
191
+ hint="请检查 --dir 路径和目录权限。",
192
+ ),
193
+ json_default=True,
194
+ )
195
+ emit_success(
196
+ ctx,
197
+ {
198
+ "agent": agent,
199
+ "path": str(target),
200
+ "installed": True,
201
+ "fingerprint": _skill_fingerprint(files),
202
+ },
203
+ json_default=True,
204
+ )
205
+
206
+
207
+ @skills_app.command("status", help="检查已安装 Skill 是否与当前 CLI 一致。")
208
+ def skill_status(
209
+ ctx: typer.Context,
210
+ agent: AgentTarget = typer.Option(
211
+ "cursor",
212
+ "--agent",
213
+ help="cursor、codex 或 openclaw。",
214
+ ),
215
+ target_dir: Path | None = typer.Option(None, "--dir", help="自定义 Skill 目录。"),
216
+ stdout: bool = typer.Option(False, "--stdout", help="输出当前内置 SKILL.md。"),
217
+ ) -> None:
218
+ try:
219
+ bundled = load_skill_files()
220
+ if stdout:
221
+ typer.echo(bundled["SKILL.md"], nl=False)
222
+ return
223
+ target = _skill_target(agent, target_dir)
224
+ if target.is_symlink():
225
+ raise OSError("Skill 目标不能是符号链接")
226
+ installed = {
227
+ name: target.joinpath(name).read_text(encoding="utf-8")
228
+ for name in bundled
229
+ if target.joinpath(name).is_file()
230
+ }
231
+ except TacoError as error:
232
+ exit_with_error(ctx, error, json_default=True)
233
+ except OSError:
234
+ exit_with_error(
235
+ ctx,
236
+ TacoError(
237
+ code="SKILL_STATUS_FAILED",
238
+ message="无法读取 Agent Skill 目录。",
239
+ exit_code=2,
240
+ ),
241
+ json_default=True,
242
+ )
243
+ is_installed = set(installed) == set(bundled)
244
+ emit_success(
245
+ ctx,
246
+ {
247
+ "agent": agent,
248
+ "path": str(target),
249
+ "installed": is_installed,
250
+ "current": is_installed
251
+ and _skill_fingerprint(installed) == _skill_fingerprint(bundled),
252
+ "bundled_fingerprint": _skill_fingerprint(bundled),
253
+ "installed_fingerprint": (
254
+ _skill_fingerprint(installed) if is_installed else None
255
+ ),
256
+ },
257
+ json_default=True,
258
+ )
259
+
260
+
261
+ def _skill_target(agent: str, target_dir: Path | None) -> Path:
262
+ if target_dir is not None:
263
+ return target_dir.expanduser()
264
+ target = _SKILL_TARGETS.get(agent)
265
+ if target is None:
266
+ raise TacoError(
267
+ code="SKILL_AGENT_UNSUPPORTED",
268
+ message=f"不支持的 Agent 工作台:{agent}",
269
+ exit_code=2,
270
+ )
271
+ return target
272
+
273
+
274
+ def _install_skill_files(target: Path, files: dict[str, str]) -> None:
275
+ if target.is_symlink() or (target.exists() and not target.is_dir()):
276
+ raise OSError("Skill 目标必须是普通目录")
277
+ if target.exists() and any(
278
+ target.joinpath(name).is_symlink() for name in files
279
+ ):
280
+ raise OSError("Skill 文件不能是符号链接")
281
+ if target.exists():
282
+ entries = {entry.name for entry in target.iterdir()}
283
+ manifest_path = target / "manifest.json"
284
+ try:
285
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
286
+ except (OSError, json.JSONDecodeError) as error:
287
+ raise _unmanaged_skill_target_error(target) from error
288
+ if (
289
+ not isinstance(manifest, dict)
290
+ or manifest.get("name") != "taco"
291
+ or not entries.issubset(files)
292
+ ):
293
+ raise _unmanaged_skill_target_error(target)
294
+
295
+ parent = target.parent
296
+ parent.mkdir(parents=True, exist_ok=True)
297
+ staging = Path(tempfile.mkdtemp(dir=parent, prefix=f".{target.name}.staging-"))
298
+ backup: Path | None = None
299
+ try:
300
+ for name, content in files.items():
301
+ staging.joinpath(name).write_text(content, encoding="utf-8")
302
+ if target.exists():
303
+ backup = parent / f".{target.name}.backup-{uuid4().hex}"
304
+ target.rename(backup)
305
+ try:
306
+ staging.rename(target)
307
+ except OSError:
308
+ if backup is not None and backup.exists() and not target.exists():
309
+ backup.rename(target)
310
+ raise
311
+ if backup is not None:
312
+ shutil.rmtree(backup)
313
+ backup = None
314
+ finally:
315
+ if staging.exists():
316
+ shutil.rmtree(staging, ignore_errors=True)
317
+ if backup is not None and backup.exists() and not target.exists():
318
+ try:
319
+ backup.rename(target)
320
+ except OSError:
321
+ pass
322
+
323
+
324
+ def _unmanaged_skill_target_error(target: Path) -> TacoError:
325
+ return TacoError(
326
+ code="SKILL_TARGET_NOT_OWNED",
327
+ message=f"目标目录不是仅由 TACO 管理的 Skill 目录:{target}",
328
+ exit_code=2,
329
+ hint="请传入 taco Skill 的专用目录,不要传入 skills 根目录。",
330
+ )
331
+
332
+
333
+ def _skill_fingerprint(files: dict[str, str]) -> str:
334
+ digest = hashlib.sha256()
335
+ for name in sorted(files):
336
+ digest.update(name.encode())
337
+ digest.update(b"\0")
338
+ digest.update(files[name].encode())
339
+ digest.update(b"\0")
340
+ return digest.hexdigest()
341
+
342
+
343
+ def _emit_discovery_payload(
344
+ ctx: typer.Context,
345
+ payload: dict[str, Any],
346
+ output: OutputFormat,
347
+ ) -> None:
348
+ if output == "json":
349
+ emit_success(ctx, payload, json_default=True)
350
+ elif output == "text":
351
+ typer.echo(json.dumps(payload, ensure_ascii=False, indent=2))
352
+ else:
353
+ exit_with_error(ctx, _invalid_output_error(), json_default=True)
354
+
355
+
356
+ def _invalid_output_error() -> TacoError:
357
+ return TacoError(
358
+ code="OUTPUT_FORMAT_INVALID",
359
+ message="输出格式只支持 text 或 json。",
360
+ exit_code=2,
361
+ )
362
+
363
+
364
+ def _serialize_command(command: Any, *, name: str) -> dict[str, Any]:
365
+ payload: dict[str, Any] = {
366
+ "name": name,
367
+ "help": command.help or "",
368
+ "params": [
369
+ _serialize_parameter(param)
370
+ for param in command.params
371
+ if param.name not in _INTERNAL_PARAMETER_NAMES
372
+ ],
373
+ }
374
+ commands = getattr(command, "commands", None)
375
+ if isinstance(commands, dict):
376
+ payload["commands"] = [
377
+ _serialize_command(child, name=child_name)
378
+ for child_name, child in sorted(commands.items())
379
+ ]
380
+ return payload
381
+
382
+
383
+ def _serialize_parameter(parameter: Any) -> dict[str, Any]:
384
+ is_option = type(parameter).__name__.endswith("Option")
385
+ parameter_type = getattr(parameter, "type", None)
386
+ payload: dict[str, Any] = {
387
+ "name": parameter.name,
388
+ "required": parameter.required,
389
+ "kind": "option" if is_option else "argument",
390
+ "type": getattr(parameter_type, "name", type(parameter_type).__name__.lower()),
391
+ "help": getattr(parameter, "help", None) or "",
392
+ }
393
+ if is_option:
394
+ payload["flags"] = [*parameter.opts, *parameter.secondary_opts]
395
+ choices = getattr(parameter_type, "choices", None)
396
+ if choices is not None:
397
+ payload["choices"] = list(choices)
398
+ default = parameter.default
399
+ if isinstance(default, (str, int, float, bool, list, dict)) or default is None:
400
+ payload["default"] = default
401
+ return payload
402
+
403
+
404
+ def _command_lines(command: dict[str, Any], prefix: str = "") -> list[str]:
405
+ name = f"{prefix} {command['name']}".strip()
406
+ lines = [f"{name}: {command['help']}".rstrip()]
407
+ for child in command.get("commands", []):
408
+ lines.extend(_command_lines(child, name))
409
+ return lines
taco/commands/model.py ADDED
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from taco.core.context import get_cli_context
6
+ from taco.core.errors import TacoError
7
+ from taco.core.output import emit_success, exit_with_error
8
+ from taco.services.model_service import ModelService
9
+
10
+
11
+ model_app = typer.Typer(help="模型发现")
12
+
13
+
14
+ @model_app.command("list", help="列出 LLM,可按智能体分类过滤。")
15
+ def list_models(
16
+ ctx: typer.Context,
17
+ category: str | None = typer.Option(
18
+ None,
19
+ "--category",
20
+ help="只返回该分类可用模型。",
21
+ ),
22
+ ) -> None:
23
+ cli_context = get_cli_context(ctx)
24
+ try:
25
+ result = ModelService().list_models(
26
+ category=category,
27
+ profile=cli_context.profile,
28
+ )
29
+ except TacoError as error:
30
+ exit_with_error(ctx, error, json_default=True)
31
+ emit_success(ctx, result, json_default=True)
32
+
33
+
34
+ @model_app.command("describe", help="获取模型详情及 completion 参数规则。")
35
+ def describe_model(
36
+ ctx: typer.Context,
37
+ provider: str = typer.Option(..., "--provider", help="Provider ID。"),
38
+ model: str = typer.Option(..., "--model", help="模型 ID。"),
39
+ category: str | None = typer.Option(
40
+ None,
41
+ "--category",
42
+ help="校验模型是否支持该分类。",
43
+ ),
44
+ ) -> None:
45
+ cli_context = get_cli_context(ctx)
46
+ try:
47
+ result = ModelService().describe_model(
48
+ provider=provider,
49
+ model=model,
50
+ category=category,
51
+ profile=cli_context.profile,
52
+ )
53
+ except TacoError as error:
54
+ exit_with_error(ctx, error, json_default=True)
55
+ emit_success(ctx, result, json_default=True)