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
taco/commands/agent.py ADDED
@@ -0,0 +1,760 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+ from json import JSONDecodeError
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from uuid import UUID, uuid4
10
+
11
+ import typer
12
+
13
+ from taco.commands.app import app_app
14
+ from taco.commands.category import category_app
15
+ from taco.commands.chatflow import chatflow_app
16
+ from taco.commands.credential import credential_app
17
+ from taco.commands.discovery import example_app, scenario_app, schema_app, skills_app
18
+ from taco.commands.model import model_app
19
+ from taco.commands.tool import tool_app
20
+ from taco.commands.workflow import workflow_app
21
+ from taco.agent_openapi.catalog import load_catalog, operation_not_found
22
+ from taco.agent_openapi.models import thaw
23
+ from taco.agent_openapi.parameters import parse_parameters
24
+ from taco.agent_openapi.service import AgentOpenApiService
25
+ from taco.core.context import get_cli_context
26
+ from taco.core.errors import TacoError
27
+ from taco.core.output import emit_success, exit_with_error, to_json_line
28
+ from taco.services.agent_service import (
29
+ AgentService,
30
+ validate_agent_config,
31
+ validate_completion_params,
32
+ )
33
+ from taco.services.model_service import ModelService
34
+ from taco.services.tool_service import ToolService
35
+ from taco.services.workflow_tool_service import WorkflowToolService
36
+
37
+
38
+ agent_app = typer.Typer(help="Agent 智能体管理")
39
+ agent_tool_app = typer.Typer(help="本地 Agent DSL 工具挂载")
40
+ agent_operation_app = typer.Typer(help="发现 Agent 数据 OpenAPI operation")
41
+ agent_app.add_typer(agent_tool_app, name="tool")
42
+ agent_app.add_typer(agent_operation_app, name="operation")
43
+ agent_app.add_typer(app_app, name="app")
44
+ agent_app.add_typer(category_app, name="category")
45
+ agent_app.add_typer(model_app, name="model")
46
+ agent_app.add_typer(workflow_app, name="workflow")
47
+ agent_app.add_typer(chatflow_app, name="chatflow")
48
+ agent_app.add_typer(example_app, name="example")
49
+ agent_app.add_typer(scenario_app, name="scenario")
50
+ agent_app.add_typer(skills_app, name="skills")
51
+ agent_app.add_typer(schema_app, name="schema")
52
+ agent_tool_app.add_typer(tool_app, name="catalog")
53
+ agent_tool_app.add_typer(credential_app, name="credential")
54
+
55
+
56
+ @agent_operation_app.command("list", help="列出 Agent 数据 OpenAPI operation。")
57
+ def list_agent_operations(
58
+ ctx: typer.Context,
59
+ group: str | None = typer.Option(None, "--group", help="按分组筛选。"),
60
+ ) -> None:
61
+ operations = [
62
+ _agent_operation_summary(item)
63
+ for item in load_catalog().operations
64
+ if group is None or item.group == group
65
+ ]
66
+ emit_success(ctx, operations, json_default=True)
67
+
68
+
69
+ @agent_operation_app.command("search", help="按关键词搜索 Agent 数据 OpenAPI operation。")
70
+ def search_agent_operations(ctx: typer.Context, query: str = typer.Argument(...)) -> None:
71
+ needle = query.casefold()
72
+ matches = []
73
+ for operation in load_catalog().operations:
74
+ haystack = " ".join(
75
+ [operation.id, operation.title, operation.description, *operation.keywords]
76
+ ).casefold()
77
+ if needle in haystack:
78
+ matches.append(_agent_operation_summary(operation))
79
+ emit_success(ctx, matches[:10], json_default=True)
80
+
81
+
82
+ @agent_operation_app.command("describe", help="查看 Agent 数据 OpenAPI operation 完整契约。")
83
+ def describe_agent_operation(
84
+ ctx: typer.Context,
85
+ operation_id: str = typer.Argument(...),
86
+ ) -> None:
87
+ operation = load_catalog().by_id.get(operation_id)
88
+ if operation is None:
89
+ exit_with_error(ctx, operation_not_found(operation_id), json_default=True)
90
+ emit_success(
91
+ ctx,
92
+ {
93
+ **_agent_operation_summary(operation),
94
+ "description": operation.description,
95
+ "keywords": list(operation.keywords),
96
+ "http": thaw(operation.http),
97
+ "response": thaw(operation.response),
98
+ "risk": thaw(operation.risk),
99
+ "execution": thaw(operation.execution),
100
+ "examples": thaw(operation.examples),
101
+ },
102
+ json_default=True,
103
+ )
104
+
105
+
106
+ @agent_app.command("call", help="校验参数并执行 Agent 数据 OpenAPI operation。")
107
+ def call_agent_operation(
108
+ ctx: typer.Context,
109
+ operation_id: str = typer.Argument(...),
110
+ param: list[str] | None = typer.Option(
111
+ None,
112
+ "--param",
113
+ help="语义参数 key=value,可重复。",
114
+ ),
115
+ params_file: Path | None = typer.Option(
116
+ None,
117
+ "--params-file",
118
+ help="语义参数 JSON 文件。",
119
+ ),
120
+ body_file: Path | None = typer.Option(
121
+ None,
122
+ "--body-file",
123
+ help="JSON body 文件。",
124
+ ),
125
+ ) -> None:
126
+ operation = load_catalog().by_id.get(operation_id)
127
+ if operation is None:
128
+ exit_with_error(ctx, operation_not_found(operation_id), json_default=True)
129
+ try:
130
+ query, body = parse_parameters(
131
+ operation,
132
+ params=param,
133
+ params_file=params_file,
134
+ body_file=body_file,
135
+ )
136
+ result = AgentOpenApiService().call(
137
+ operation_id,
138
+ query=query,
139
+ body=body,
140
+ profile=get_cli_context(ctx).profile,
141
+ )
142
+ except TacoError as error:
143
+ exit_with_error(ctx, error, json_default=True)
144
+ emit_success(ctx, result, json_default=True)
145
+
146
+
147
+ def _agent_operation_summary(operation) -> dict[str, Any]:
148
+ return {
149
+ "id": operation.id,
150
+ "title": operation.title,
151
+ "group": operation.group,
152
+ "method": operation.http["method"],
153
+ "path": operation.http["path"],
154
+ "risk": operation.risk["level"],
155
+ }
156
+
157
+
158
+ @agent_app.command("create", help="创建 Agent 并把服务端默认配置写入本地 DSL。")
159
+ def create_agent(
160
+ ctx: typer.Context,
161
+ name: str = typer.Option(..., "--name", help="智能体名称。"),
162
+ category: str = typer.Option(..., "--category", help="服务端分类 code。"),
163
+ description: str = typer.Option("", "--description", help="智能体描述。"),
164
+ output: Path | None = typer.Option(
165
+ None,
166
+ "--output",
167
+ help="本地 Agent DSL JSON 输出路径,默认生成唯一文件名。",
168
+ ),
169
+ yes: bool = typer.Option(False, "--yes", help="确认创建远端 Agent。"),
170
+ overwrite: bool = typer.Option(
171
+ False,
172
+ "--overwrite",
173
+ help="允许覆盖已存在的本地 DSL,仍需 --yes。",
174
+ ),
175
+ ) -> None:
176
+ cli_context = get_cli_context(ctx)
177
+ if not yes:
178
+ exit_with_error(
179
+ ctx,
180
+ TacoError(
181
+ code="AGENT_CREATE_CONFIRM_REQUIRED",
182
+ message="创建远端 Agent 前必须显式传入 --yes。",
183
+ exit_code=2,
184
+ ),
185
+ json_default=True,
186
+ )
187
+ try:
188
+ target = output or Path(f"taco-agent-{uuid4()}.json")
189
+ _prepare_agent_output(target, overwrite=overwrite)
190
+ app = AgentService().create_agent(
191
+ name=name,
192
+ category=category,
193
+ description=description,
194
+ profile=cli_context.profile,
195
+ )
196
+ app_id = app.get("id")
197
+ model_config = app.get("model_config")
198
+ if not isinstance(app_id, str) or not isinstance(model_config, dict):
199
+ raise TacoError(
200
+ code="AGENT_CREATE_RESPONSE_INVALID",
201
+ message="创建响应缺少应用 ID 或 model_config。",
202
+ exit_code=4,
203
+ )
204
+ _write_json_file(target, model_config)
205
+ except TacoError as error:
206
+ exit_with_error(ctx, error, json_default=True)
207
+ emit_success(
208
+ ctx,
209
+ {
210
+ "app_id": app_id,
211
+ "name": app.get("name"),
212
+ "category": category,
213
+ "path": str(target),
214
+ },
215
+ json_default=True,
216
+ )
217
+
218
+
219
+ @agent_app.command("pull", help="从服务端拉取 Agent model_config 到本地 DSL。")
220
+ def pull_agent(
221
+ ctx: typer.Context,
222
+ app_id: str = typer.Argument(..., help="应用 ID。"),
223
+ output: Path = typer.Option(..., "--output", help="本地 Agent DSL JSON 输出路径。"),
224
+ overwrite: bool = typer.Option(
225
+ False,
226
+ "--overwrite",
227
+ help="允许覆盖已存在的本地 DSL。",
228
+ ),
229
+ ) -> None:
230
+ cli_context = get_cli_context(ctx)
231
+ try:
232
+ _prepare_agent_output(output, overwrite=overwrite)
233
+ model_config = AgentService().pull_agent_config(
234
+ app_id,
235
+ profile=cli_context.profile,
236
+ )
237
+ _write_json_file(output, model_config)
238
+ except TacoError as error:
239
+ exit_with_error(ctx, error, json_default=True)
240
+
241
+ emit_success(ctx, {"path": str(output)}, json_default=True)
242
+
243
+
244
+ @agent_app.command("validate", help="校验本地 Agent DSL 及可选分类模型契约。")
245
+ def validate_agent(
246
+ ctx: typer.Context,
247
+ dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
248
+ category: str | None = typer.Option(
249
+ None,
250
+ "--category",
251
+ help="通过服务端确认模型支持该分类。",
252
+ ),
253
+ ) -> None:
254
+ cli_context = get_cli_context(ctx)
255
+ try:
256
+ model_config = _read_json_object(dsl)
257
+ result = validate_agent_config(model_config)
258
+ parameter_rules: list[dict[str, Any]] = []
259
+ if category is not None:
260
+ model = model_config["model"]
261
+ model_description = ModelService().describe_model(
262
+ provider=model["provider"],
263
+ model=model["name"],
264
+ category=category,
265
+ profile=cli_context.profile,
266
+ )
267
+ parameter_rules = model_description["parameter_rules"]
268
+ result["model_verified"] = True
269
+ validate_completion_params(
270
+ model_config["model"]["completion_params"],
271
+ parameter_rules,
272
+ )
273
+ except TacoError as error:
274
+ exit_with_error(ctx, error, json_default=True)
275
+ emit_success(ctx, result, json_default=True)
276
+
277
+
278
+ @agent_app.command("run", help="使用本地 DSL 通过 SSE 调试 Agent。")
279
+ def run_agent(
280
+ ctx: typer.Context,
281
+ app_id: str = typer.Argument(..., help="应用 ID。"),
282
+ dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
283
+ query: str = typer.Option(..., "--query", help="调试问题。"),
284
+ inputs: Path | None = typer.Option(None, "--inputs", help="调试 inputs JSON 文件。"),
285
+ conversation_id: str | None = typer.Option(
286
+ None,
287
+ "--conversation-id",
288
+ help="续聊会话 ID。",
289
+ ),
290
+ parent_message_id: str | None = typer.Option(
291
+ None,
292
+ "--parent-message-id",
293
+ help="父消息 ID。",
294
+ ),
295
+ files: Path | None = typer.Option(
296
+ None,
297
+ "--files",
298
+ help="Dify 消息文件数组 JSON。",
299
+ ),
300
+ stream_events: bool = typer.Option(
301
+ False,
302
+ "--stream-events",
303
+ help="实时输出 NDJSON 事件,便于获取 task_id 后调用 stop。",
304
+ ),
305
+ ) -> None:
306
+ cli_context = get_cli_context(ctx)
307
+ try:
308
+ model_config = _read_json_object(dsl)
309
+ input_payload = _read_json_object(inputs) if inputs else {}
310
+ run_options: dict[str, Any] = {
311
+ "model_config": model_config,
312
+ "query": query,
313
+ "inputs": input_payload,
314
+ "profile": cli_context.profile,
315
+ }
316
+ if conversation_id is not None:
317
+ run_options["conversation_id"] = conversation_id
318
+ if parent_message_id is not None:
319
+ run_options["parent_message_id"] = parent_message_id
320
+ if files is not None:
321
+ run_options["files"] = _read_json_array(files)
322
+ if stream_events:
323
+ run_options["on_event"] = lambda event: typer.echo(
324
+ to_json_line({"type": "event", "event": event})
325
+ )
326
+ result = AgentService().run_agent(app_id, **run_options)
327
+ except TacoError as error:
328
+ exit_with_error(ctx, error, json_default=True)
329
+
330
+ emit_success(ctx, result, json_default=True)
331
+
332
+
333
+ @agent_app.command("stop", help="使用 task_id 停止正在运行的 Agent 调试任务。")
334
+ def stop_agent(
335
+ ctx: typer.Context,
336
+ app_id: str = typer.Argument(..., help="应用 ID。"),
337
+ task_id: str = typer.Option(..., "--task-id", help="SSE 返回的任务 ID。"),
338
+ ) -> None:
339
+ cli_context = get_cli_context(ctx)
340
+ try:
341
+ result = AgentService().stop_agent(
342
+ app_id,
343
+ task_id=task_id,
344
+ profile=cli_context.profile,
345
+ )
346
+ except TacoError as error:
347
+ exit_with_error(ctx, error, json_default=True)
348
+ emit_success(ctx, result, json_default=True)
349
+
350
+
351
+ @agent_tool_app.command("list", help="列出本地 Agent DSL 已挂载工具。")
352
+ def list_agent_tools(
353
+ ctx: typer.Context,
354
+ dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
355
+ ) -> None:
356
+ try:
357
+ model_config = _read_json_object(dsl)
358
+ tools = _agent_tools(model_config)
359
+ except TacoError as error:
360
+ exit_with_error(ctx, error, json_default=True)
361
+ emit_success(ctx, {"path": str(dsl), "items": tools}, json_default=True)
362
+
363
+
364
+ @agent_tool_app.command("add", help="发现工具并挂载到本地 Agent DSL。")
365
+ def add_agent_tool(
366
+ ctx: typer.Context,
367
+ tool_type: str = typer.Option(
368
+ "builtin",
369
+ "--type",
370
+ help="工具类型:builtin|workflow;默认 builtin。",
371
+ ),
372
+ dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
373
+ provider: str = typer.Option(
374
+ ...,
375
+ "--provider",
376
+ help="builtin Provider ID;workflow 时为 workflow_tool_id(不是 app ID)。",
377
+ ),
378
+ tool: str = typer.Option(
379
+ ...,
380
+ "--tool",
381
+ help="builtin action 名称或 Workflow Tool 机器名。",
382
+ ),
383
+ parameters: Path | None = typer.Option(
384
+ None,
385
+ "--parameters",
386
+ help="工具 form 参数 JSON 对象,不得包含 Provider 凭据。",
387
+ ),
388
+ replace: bool = typer.Option(
389
+ False,
390
+ "--replace",
391
+ help="替换同类型、同 Provider、同名称的已挂载工具。",
392
+ ),
393
+ yes: bool = typer.Option(False, "--yes", help="确认修改本地 DSL。"),
394
+ ) -> None:
395
+ _require_agent_tool_confirmation(ctx, yes)
396
+ if tool_type not in {"builtin", "workflow"}:
397
+ exit_with_error(
398
+ ctx,
399
+ TacoError(
400
+ code="AGENT_TOOL_TYPE_INVALID",
401
+ message="Agent 工具类型必须是 builtin 或 workflow。",
402
+ exit_code=2,
403
+ ),
404
+ json_default=True,
405
+ )
406
+ cli_context = get_cli_context(ctx)
407
+ try:
408
+ _ensure_mutable_agent_dsl(dsl)
409
+ model_config = _read_json_object(dsl)
410
+ tools = _agent_tools(model_config)
411
+ parameter_values = _read_json_object(parameters) if parameters else {}
412
+ if tool_type == "builtin":
413
+ resolved = ToolService().resolve_agent_tool(
414
+ provider=provider,
415
+ tool=tool,
416
+ parameters=parameter_values,
417
+ profile=cli_context.profile,
418
+ )
419
+ else:
420
+ resolved = WorkflowToolService().resolve_agent_tool(
421
+ workflow_tool_id=provider,
422
+ tool_name=tool,
423
+ parameters=parameter_values,
424
+ profile=cli_context.profile,
425
+ )
426
+ matching_indices = [
427
+ position
428
+ for position, item in enumerate(tools)
429
+ if _same_agent_tool(item, tool_type, provider, tool)
430
+ ]
431
+ if matching_indices and not replace:
432
+ raise TacoError(
433
+ code="AGENT_TOOL_EXISTS",
434
+ message="本地 DSL 已挂载同一个工具。",
435
+ exit_code=6,
436
+ hint="确认替换后传入 --replace --yes。",
437
+ )
438
+ replaced_count = len(matching_indices)
439
+ if not matching_indices:
440
+ tools.append(resolved)
441
+ else:
442
+ first_index = matching_indices[0]
443
+ tools[:] = [
444
+ item
445
+ for item in tools
446
+ if not _same_agent_tool(item, tool_type, provider, tool)
447
+ ]
448
+ tools.insert(first_index, resolved)
449
+ validate_agent_config(model_config)
450
+ _write_json_file(dsl, model_config)
451
+ except TacoError as error:
452
+ exit_with_error(ctx, error, json_default=True)
453
+ emit_success(
454
+ ctx,
455
+ {
456
+ "path": str(dsl),
457
+ "tool": resolved,
458
+ "tools_count": len(tools),
459
+ "replaced_count": replaced_count,
460
+ },
461
+ json_default=True,
462
+ )
463
+
464
+
465
+ @agent_tool_app.command("remove", help="从本地 Agent DSL 移除已挂载工具。")
466
+ def remove_agent_tool(
467
+ ctx: typer.Context,
468
+ tool_type: str = typer.Option(
469
+ "builtin",
470
+ "--type",
471
+ help="工具类型:builtin|workflow;默认 builtin。",
472
+ ),
473
+ dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
474
+ provider: str = typer.Option(
475
+ ...,
476
+ "--provider",
477
+ help="builtin Provider ID;workflow 时为 workflow_tool_id(不是 app ID)。",
478
+ ),
479
+ tool: str = typer.Option(
480
+ ...,
481
+ "--tool",
482
+ help="builtin action 名称或 Workflow Tool 机器名。",
483
+ ),
484
+ yes: bool = typer.Option(False, "--yes", help="确认修改本地 DSL。"),
485
+ ) -> None:
486
+ _require_agent_tool_confirmation(ctx, yes)
487
+ if tool_type not in {"builtin", "workflow"}:
488
+ exit_with_error(
489
+ ctx,
490
+ TacoError(
491
+ code="AGENT_TOOL_TYPE_INVALID",
492
+ message="Agent 工具类型必须是 builtin 或 workflow。",
493
+ exit_code=2,
494
+ ),
495
+ json_default=True,
496
+ )
497
+ try:
498
+ _ensure_mutable_agent_dsl(dsl)
499
+ model_config = _read_json_object(dsl)
500
+ tools = _agent_tools(model_config)
501
+ remaining = [
502
+ item
503
+ for item in tools
504
+ if not _same_agent_tool(item, tool_type, provider, tool)
505
+ ]
506
+ if len(remaining) == len(tools):
507
+ raise TacoError(
508
+ code="AGENT_TOOL_NOT_FOUND",
509
+ message="本地 DSL 未挂载指定工具。",
510
+ exit_code=6,
511
+ )
512
+ model_config["agent_mode"]["tools"] = remaining
513
+ validate_agent_config(model_config)
514
+ _write_json_file(dsl, model_config)
515
+ except TacoError as error:
516
+ exit_with_error(ctx, error, json_default=True)
517
+ emit_success(
518
+ ctx,
519
+ {"path": str(dsl), "tools_count": len(remaining)},
520
+ json_default=True,
521
+ )
522
+
523
+
524
+ @agent_app.command("publish", help="发布本地 Agent DSL 并可在成功后删除文件。")
525
+ def publish_agent(
526
+ ctx: typer.Context,
527
+ app_id: str = typer.Argument(..., help="应用 ID。"),
528
+ dsl: Path = typer.Option(..., "--dsl", help="本地 Agent DSL JSON 文件。"),
529
+ yes: bool = typer.Option(False, "--yes", help="确认发布本地 DSL。"),
530
+ keep_dsl: bool = typer.Option(False, "--keep-dsl", help="发布成功后保留本地 DSL 文件。"),
531
+ ) -> None:
532
+ cli_context = get_cli_context(ctx)
533
+ if not yes:
534
+ exit_with_error(
535
+ ctx,
536
+ TacoError(
537
+ code="AGENT_PUBLISH_CONFIRM_REQUIRED",
538
+ message="发布 Agent 前必须显式传入 --yes。",
539
+ exit_code=2,
540
+ hint="确认本地 DSL 已调试通过后,重新执行 taco agent publish --yes。",
541
+ ),
542
+ json_default=True,
543
+ )
544
+
545
+ try:
546
+ model_config = _read_json_object(dsl)
547
+ result = AgentService().publish_agent(
548
+ app_id,
549
+ model_config=model_config,
550
+ profile=cli_context.profile,
551
+ )
552
+ except TacoError as error:
553
+ exit_with_error(ctx, error, json_default=True)
554
+
555
+ dsl_deleted = False
556
+ if not keep_dsl:
557
+ try:
558
+ dsl.unlink()
559
+ except OSError:
560
+ exit_with_error(
561
+ ctx,
562
+ TacoError(
563
+ code="AGENT_DSL_CLEANUP_FAILED",
564
+ message="Agent 已发布,但本地 DSL 删除失败。",
565
+ exit_code=2,
566
+ hint=f"请手动删除文件:{dsl}",
567
+ ),
568
+ json_default=True,
569
+ )
570
+ dsl_deleted = True
571
+
572
+ data = dict(result) if isinstance(result, dict) else {"result": result}
573
+ data.update({"dsl_deleted": dsl_deleted, "path": str(dsl)})
574
+ emit_success(ctx, data, json_default=True)
575
+
576
+
577
+ def _read_json_object(path: Path | None) -> dict[str, Any]:
578
+ if path is None:
579
+ return {}
580
+ try:
581
+ payload = json.loads(path.read_text(encoding="utf-8"))
582
+ except (OSError, JSONDecodeError) as error:
583
+ raise TacoError(
584
+ code="AGENT_DSL_INVALID",
585
+ message="Agent DSL 文件不存在或不是合法 JSON。",
586
+ exit_code=2,
587
+ hint="请确认 --dsl/--inputs 指向有效 JSON 文件。",
588
+ ) from error
589
+
590
+ if not isinstance(payload, dict):
591
+ raise TacoError(
592
+ code="AGENT_DSL_INVALID",
593
+ message="Agent DSL JSON 必须是对象。",
594
+ exit_code=2,
595
+ hint="请将本地 DSL 调整为 JSON object。",
596
+ )
597
+
598
+ return payload
599
+
600
+
601
+ def _read_json_array(path: Path) -> list[dict[str, Any]]:
602
+ try:
603
+ payload = json.loads(path.read_text(encoding="utf-8"))
604
+ except (OSError, JSONDecodeError) as error:
605
+ raise TacoError(
606
+ code="AGENT_FILES_INVALID",
607
+ message="Agent files 文件不存在或不是合法 JSON。",
608
+ exit_code=2,
609
+ ) from error
610
+ if not isinstance(payload, list) or any(
611
+ not isinstance(item, dict) for item in payload
612
+ ):
613
+ raise TacoError(
614
+ code="AGENT_FILES_INVALID",
615
+ message="Agent files JSON 必须是对象数组。",
616
+ exit_code=2,
617
+ )
618
+ return payload
619
+
620
+
621
+ def _agent_tools(model_config: dict[str, Any]) -> list[dict[str, Any]]:
622
+ agent_mode = model_config.get("agent_mode")
623
+ tools = agent_mode.get("tools") if isinstance(agent_mode, dict) else None
624
+ if not isinstance(tools, list) or any(
625
+ not isinstance(item, dict) for item in tools
626
+ ):
627
+ raise TacoError(
628
+ code="AGENT_DSL_VALIDATION_FAILED",
629
+ message="Agent DSL 的 agent_mode.tools 必须是对象数组。",
630
+ exit_code=6,
631
+ )
632
+ return tools
633
+
634
+
635
+ def _same_agent_tool(
636
+ tool: dict[str, Any],
637
+ provider_type: str,
638
+ provider: str,
639
+ tool_name: str,
640
+ ) -> bool:
641
+ return (
642
+ tool.get("provider_type") == provider_type
643
+ and _provider_ids_match(
644
+ provider_type,
645
+ tool.get("provider_id"),
646
+ provider,
647
+ )
648
+ and tool.get("tool_name") == tool_name
649
+ )
650
+
651
+
652
+ def _provider_ids_match(
653
+ provider_type: str,
654
+ actual: Any,
655
+ expected: str,
656
+ ) -> bool:
657
+ if not isinstance(actual, str) or provider_type != "workflow":
658
+ return actual == expected
659
+ try:
660
+ return UUID(actual) == UUID(expected)
661
+ except ValueError:
662
+ return actual == expected
663
+
664
+
665
+ def _ensure_mutable_agent_dsl(path: Path) -> None:
666
+ if path.is_symlink():
667
+ raise TacoError(
668
+ code="AGENT_DSL_SYMLINK_UNSUPPORTED",
669
+ message="修改 Agent 工具时不允许 DSL 路径是符号链接。",
670
+ exit_code=2,
671
+ hint="请使用普通文件路径。",
672
+ )
673
+
674
+
675
+ def _require_agent_tool_confirmation(ctx: typer.Context, yes: bool) -> None:
676
+ if not yes:
677
+ exit_with_error(
678
+ ctx,
679
+ TacoError(
680
+ code="AGENT_TOOL_CONFIRM_REQUIRED",
681
+ message="修改本地 Agent 工具前必须显式传入 --yes。",
682
+ exit_code=2,
683
+ ),
684
+ json_default=True,
685
+ )
686
+
687
+
688
+ def _prepare_agent_output(path: Path | None, *, overwrite: bool) -> None:
689
+ parent = (path.parent if path is not None else Path.cwd()).resolve()
690
+ if path is not None and path.is_symlink():
691
+ raise TacoError(
692
+ code="AGENT_DSL_PATH_INVALID",
693
+ message=f"Agent DSL 输出不能是符号链接:{path}",
694
+ exit_code=2,
695
+ )
696
+ if path is not None and path.exists():
697
+ if not path.is_file():
698
+ raise TacoError(
699
+ code="AGENT_DSL_PATH_INVALID",
700
+ message=f"Agent DSL 输出必须是普通文件路径:{path}",
701
+ exit_code=2,
702
+ )
703
+ if not overwrite:
704
+ raise TacoError(
705
+ code="AGENT_DSL_EXISTS",
706
+ message=f"本地 Agent DSL 已存在:{path}",
707
+ exit_code=2,
708
+ hint="请显式传入 --overwrite;Agent create 还需 --yes。",
709
+ )
710
+ if not os.access(path, os.W_OK):
711
+ raise TacoError(
712
+ code="AGENT_DSL_PATH_INVALID",
713
+ message=f"本地 Agent DSL 不可覆盖:{path}",
714
+ exit_code=2,
715
+ )
716
+ try:
717
+ parent.mkdir(parents=True, exist_ok=True)
718
+ with tempfile.NamedTemporaryFile(
719
+ dir=parent,
720
+ prefix=".taco-write-check-",
721
+ ):
722
+ pass
723
+ except OSError as error:
724
+ raise TacoError(
725
+ code="AGENT_DSL_WRITE_FAILED",
726
+ message="创建远端 Agent 前无法确认本地 DSL 目录可写。",
727
+ exit_code=2,
728
+ hint=f"请检查目录权限和磁盘空间:{parent}",
729
+ ) from error
730
+
731
+
732
+ def _write_json_file(path: Path, payload: Any) -> None:
733
+ temporary: Path | None = None
734
+ try:
735
+ path.parent.mkdir(parents=True, exist_ok=True)
736
+ descriptor, temporary_name = tempfile.mkstemp(
737
+ dir=path.parent,
738
+ prefix=f".{path.name}.",
739
+ suffix=".tmp",
740
+ text=True,
741
+ )
742
+ temporary = Path(temporary_name)
743
+ with os.fdopen(descriptor, "w", encoding="utf-8") as file:
744
+ file.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
745
+ file.flush()
746
+ os.fsync(file.fileno())
747
+ temporary.replace(path)
748
+ except OSError as error:
749
+ raise TacoError(
750
+ code="AGENT_DSL_WRITE_FAILED",
751
+ message="无法写入 Agent DSL 文件。",
752
+ exit_code=2,
753
+ hint=f"请检查目录权限和磁盘空间:{path}",
754
+ ) from error
755
+ finally:
756
+ if temporary is not None and temporary.exists():
757
+ try:
758
+ temporary.unlink()
759
+ except OSError:
760
+ pass