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.
- taco/__init__.py +1 -0
- taco/agent_openapi/__init__.py +1 -0
- taco/agent_openapi/catalog.py +198 -0
- taco/agent_openapi/models.py +47 -0
- taco/agent_openapi/parameters.py +111 -0
- taco/agent_openapi/service.py +73 -0
- taco/aikb/__init__.py +4 -0
- taco/aikb/catalog.py +380 -0
- taco/aikb/executors.py +157 -0
- taco/aikb/file_input.py +55 -0
- taco/aikb/models.py +42 -0
- taco/aikb/parameters.py +135 -0
- taco/aikb/service.py +68 -0
- taco/assets/__init__.py +1 -0
- taco/assets/agent_openapi/catalog.json +14 -0
- taco/assets/agent_openapi/operations/conversation/list-conversations.json +67 -0
- taco/assets/agent_openapi/operations/message/list-conversation-messages.json +80 -0
- taco/assets/agent_openapi/operations/trace/list-message-traces.json +72 -0
- taco/assets/aikb/catalog.json +41 -0
- taco/assets/aikb/operations/conversation/chat-conversation-on-open.json +99 -0
- taco/assets/aikb/operations/directory/delete-directory.json +81 -0
- taco/assets/aikb/operations/directory/edit-directory.json +97 -0
- taco/assets/aikb/operations/directory/list-directory-tree.json +106 -0
- taco/assets/aikb/operations/directory/save-directory.json +109 -0
- taco/assets/aikb/operations/faq/create-faq.json +193 -0
- taco/assets/aikb/operations/faq/delete-faq.json +81 -0
- taco/assets/aikb/operations/faq/describe-faq.json +82 -0
- taco/assets/aikb/operations/faq/edit-faq.json +193 -0
- taco/assets/aikb/operations/faq/list-faqs.json +136 -0
- taco/assets/aikb/operations/file/create-file.json +145 -0
- taco/assets/aikb/operations/file/delete-files.json +110 -0
- taco/assets/aikb/operations/file/describe-file.json +82 -0
- taco/assets/aikb/operations/file/get-file-upload-url.json +154 -0
- taco/assets/aikb/operations/file/list-files.json +146 -0
- taco/assets/aikb/operations/media/describe-faq-media-url.json +98 -0
- taco/assets/aikb/operations/media/describe-file-media-url.json +90 -0
- taco/assets/aikb/operations/oss/signature-for-upload.json +85 -0
- taco/assets/aikb/operations/recycle-bin/list-recycled-items.json +151 -0
- taco/assets/aikb/operations/repository/describe-bot-repository.json +71 -0
- taco/assets/aikb/operations/repository/list-private-repositories.json +75 -0
- taco/assets/aikb/operations/repository/list-public-repositories.json +75 -0
- taco/assets/aikb/operations/search/search-knowledge-on-open.json +180 -0
- taco/assets/examples/README.md +3 -0
- taco/assets/examples/agent-basic.json +16 -0
- taco/assets/examples/agent-builtin-tool.json +28 -0
- taco/assets/examples/agent-workflow-tool.json +30 -0
- taco/assets/examples/chatflow-basic.json +24 -0
- taco/assets/examples/workflow-http.json +34 -0
- taco/assets/manifest.json +12 -0
- taco/assets/scenarios/README.md +3 -0
- taco/assets/scenarios/agent-basic.json +80 -0
- taco/assets/scenarios/agent-builtin-tool.json +116 -0
- taco/assets/scenarios/agent-workflow-tool.json +277 -0
- taco/assets/schemas/README.md +3 -0
- taco/assets/schemas/agent-tool.json +71 -0
- taco/assets/schemas/agent.json +199 -0
- taco/assets/schemas/chatflow.json +1048 -0
- taco/assets/schemas/workflow.http.json +1052 -0
- taco/assets/schemas/workflow.json +1052 -0
- taco/assets/skills/README.md +3 -0
- taco/assets/skills/taco/SKILL.md +68 -0
- taco/assets/skills/taco/manifest.json +10 -0
- taco/cli.py +127 -0
- taco/commands/__init__.py +1 -0
- taco/commands/agent.py +760 -0
- taco/commands/aikb.py +132 -0
- taco/commands/app.py +151 -0
- taco/commands/category.py +37 -0
- taco/commands/chatflow.py +183 -0
- taco/commands/credential.py +222 -0
- taco/commands/discovery.py +409 -0
- taco/commands/model.py +55 -0
- taco/commands/profile.py +124 -0
- taco/commands/tool.py +61 -0
- taco/commands/workflow.py +714 -0
- taco/core/__init__.py +1 -0
- taco/core/access_token_manager.py +98 -0
- taco/core/api_client.py +263 -0
- taco/core/assets.py +81 -0
- taco/core/config_store.py +209 -0
- taco/core/context.py +19 -0
- taco/core/developer_center_client.py +165 -0
- taco/core/errors.py +19 -0
- taco/core/file_lock.py +80 -0
- taco/core/http_headers.py +21 -0
- taco/core/openapi_signer.py +221 -0
- taco/core/output.py +72 -0
- taco/core/profile_manager.py +217 -0
- taco/core/profile_resolver.py +151 -0
- taco/core/secure_file.py +76 -0
- taco/release.py +363 -0
- taco/services/__init__.py +1 -0
- taco/services/agent_service.py +314 -0
- taco/services/app_service.py +153 -0
- taco/services/category_service.py +57 -0
- taco/services/model_service.py +117 -0
- taco/services/tool_service.py +482 -0
- taco/services/workflow_compiler.py +1665 -0
- taco/services/workflow_service.py +652 -0
- taco/services/workflow_tool_service.py +439 -0
- tinet_agent_cli-0.1.0.dev36.dist-info/METADATA +158 -0
- tinet_agent_cli-0.1.0.dev36.dist-info/RECORD +104 -0
- tinet_agent_cli-0.1.0.dev36.dist-info/WHEEL +4 -0
- 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)
|