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,714 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from taco.core.assets import load_json_asset
|
|
15
|
+
from taco.core.context import get_cli_context
|
|
16
|
+
from taco.core.errors import TacoError
|
|
17
|
+
from taco.core.output import emit_success, exit_with_error, to_json_line
|
|
18
|
+
from taco.core.secure_file import SecureFileReadError, read_regular_file
|
|
19
|
+
from taco.services.workflow_compiler import (
|
|
20
|
+
compile_workflow,
|
|
21
|
+
compile_workflow_file,
|
|
22
|
+
load_workflow_file,
|
|
23
|
+
summarize_graph,
|
|
24
|
+
)
|
|
25
|
+
from taco.services.workflow_service import WorkflowService
|
|
26
|
+
from taco.services.workflow_tool_service import WorkflowToolService
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
workflow_app = typer.Typer(help="HTTP Workflow 本地 DSL 工具")
|
|
30
|
+
workflow_secret_app = typer.Typer(help="Workflow secret 管理")
|
|
31
|
+
workflow_tool_app = typer.Typer(help="Workflow Tool 发布、更新与查询")
|
|
32
|
+
workflow_app.add_typer(workflow_secret_app, name="secret")
|
|
33
|
+
workflow_app.add_typer(workflow_tool_app, name="tool")
|
|
34
|
+
_MAX_INPUTS_FILE_SIZE = 1024 * 1024
|
|
35
|
+
_MAX_INPUTS_DEPTH = 50
|
|
36
|
+
_SECRET_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,29}$")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@workflow_app.command("create", help="创建远端 Workflow 并初始化本地 YAML DSL。")
|
|
40
|
+
def create_workflow(
|
|
41
|
+
ctx: typer.Context,
|
|
42
|
+
name: str = typer.Option(..., "--name", help="Workflow 名称。"),
|
|
43
|
+
description: str = typer.Option(
|
|
44
|
+
...,
|
|
45
|
+
"--description",
|
|
46
|
+
help="Workflow 描述。",
|
|
47
|
+
),
|
|
48
|
+
output: Path = typer.Option(
|
|
49
|
+
...,
|
|
50
|
+
"--output",
|
|
51
|
+
help="本地 Workflow YAML 输出路径。",
|
|
52
|
+
),
|
|
53
|
+
yes: bool = typer.Option(False, "--yes", help="确认创建远端 Workflow。"),
|
|
54
|
+
overwrite: bool = typer.Option(
|
|
55
|
+
False,
|
|
56
|
+
"--overwrite",
|
|
57
|
+
help="允许覆盖已存在的本地 Workflow YAML。",
|
|
58
|
+
),
|
|
59
|
+
) -> None:
|
|
60
|
+
if not yes:
|
|
61
|
+
exit_with_error(
|
|
62
|
+
ctx,
|
|
63
|
+
TacoError(
|
|
64
|
+
code="WORKFLOW_CREATE_CONFIRM_REQUIRED",
|
|
65
|
+
message="创建远端 Workflow 前必须显式传入 --yes。",
|
|
66
|
+
exit_code=2,
|
|
67
|
+
),
|
|
68
|
+
json_default=True,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
cli_context = get_cli_context(ctx)
|
|
72
|
+
temporary: Path | None = None
|
|
73
|
+
try:
|
|
74
|
+
_prepare_workflow_output(output, overwrite=overwrite)
|
|
75
|
+
workflow = load_json_asset("examples", "workflow-http")
|
|
76
|
+
workflow["name"] = name
|
|
77
|
+
workflow["description"] = description
|
|
78
|
+
temporary = _stage_workflow_yaml(output, workflow)
|
|
79
|
+
app = WorkflowService().create_workflow_app(
|
|
80
|
+
name,
|
|
81
|
+
description,
|
|
82
|
+
profile=cli_context.profile,
|
|
83
|
+
)
|
|
84
|
+
app_id = app.get("id")
|
|
85
|
+
if not isinstance(app_id, str) or not app_id.strip():
|
|
86
|
+
raise TacoError(
|
|
87
|
+
code="WORKFLOW_CREATE_RESPONSE_INVALID",
|
|
88
|
+
message="创建 Workflow 的响应缺少有效应用 ID。",
|
|
89
|
+
exit_code=4,
|
|
90
|
+
)
|
|
91
|
+
_commit_workflow_yaml(
|
|
92
|
+
temporary,
|
|
93
|
+
output,
|
|
94
|
+
overwrite=overwrite,
|
|
95
|
+
app_id=app_id,
|
|
96
|
+
)
|
|
97
|
+
except TacoError as error:
|
|
98
|
+
exit_with_error(ctx, error, json_default=True)
|
|
99
|
+
finally:
|
|
100
|
+
_cleanup_workflow_temporary(temporary)
|
|
101
|
+
|
|
102
|
+
emit_success(
|
|
103
|
+
ctx,
|
|
104
|
+
{
|
|
105
|
+
"app_id": app_id,
|
|
106
|
+
"name": app.get("name") if isinstance(app.get("name"), str) else name,
|
|
107
|
+
"path": str(output),
|
|
108
|
+
},
|
|
109
|
+
json_default=True,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@workflow_app.command("apply", help="预览或同步本地 Workflow graph 到远端 draft。")
|
|
114
|
+
def apply_workflow(
|
|
115
|
+
ctx: typer.Context,
|
|
116
|
+
app_id: str = typer.Argument(..., help="应用 ID。"),
|
|
117
|
+
file: Path = typer.Option(
|
|
118
|
+
...,
|
|
119
|
+
"-f",
|
|
120
|
+
"--file",
|
|
121
|
+
help="本地 HTTP Workflow YAML 文件。",
|
|
122
|
+
),
|
|
123
|
+
dry_run: bool = typer.Option(
|
|
124
|
+
False,
|
|
125
|
+
"--dry-run",
|
|
126
|
+
help="仅获取 draft 并输出变更计划。",
|
|
127
|
+
),
|
|
128
|
+
yes: bool = typer.Option(
|
|
129
|
+
False,
|
|
130
|
+
"--yes",
|
|
131
|
+
help="确认同步远端 draft。",
|
|
132
|
+
),
|
|
133
|
+
) -> None:
|
|
134
|
+
if dry_run == yes:
|
|
135
|
+
exit_with_error(
|
|
136
|
+
ctx,
|
|
137
|
+
TacoError(
|
|
138
|
+
code="WORKFLOW_APPLY_MODE_REQUIRED",
|
|
139
|
+
message="workflow apply 必须且只能传入 --dry-run 或 --yes 之一。",
|
|
140
|
+
exit_code=2,
|
|
141
|
+
),
|
|
142
|
+
json_default=True,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
cli_context = get_cli_context(ctx)
|
|
146
|
+
try:
|
|
147
|
+
graph = compile_workflow_file(file)
|
|
148
|
+
service = WorkflowService()
|
|
149
|
+
if dry_run:
|
|
150
|
+
result = service.plan_apply(
|
|
151
|
+
app_id,
|
|
152
|
+
graph,
|
|
153
|
+
profile=cli_context.profile,
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
result = service.apply_draft(
|
|
157
|
+
app_id,
|
|
158
|
+
graph,
|
|
159
|
+
profile=cli_context.profile,
|
|
160
|
+
)
|
|
161
|
+
except TacoError as error:
|
|
162
|
+
exit_with_error(ctx, error, json_default=True)
|
|
163
|
+
|
|
164
|
+
emit_success(ctx, result, json_default=True)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@workflow_app.command("run", help="通过 SSE 调试远端 Workflow draft。")
|
|
168
|
+
def run_workflow(
|
|
169
|
+
ctx: typer.Context,
|
|
170
|
+
app_id: str = typer.Argument(..., help="应用 ID。"),
|
|
171
|
+
inputs_file: Path | None = typer.Option(
|
|
172
|
+
None,
|
|
173
|
+
"--inputs-file",
|
|
174
|
+
help="Workflow inputs JSON 对象文件。",
|
|
175
|
+
),
|
|
176
|
+
query: str | None = typer.Option(
|
|
177
|
+
None,
|
|
178
|
+
"--query",
|
|
179
|
+
help="Chatflow(advanced-chat)调试时的用户问题。",
|
|
180
|
+
),
|
|
181
|
+
stream_events: bool = typer.Option(
|
|
182
|
+
False,
|
|
183
|
+
"--stream-events",
|
|
184
|
+
help="实时输出 NDJSON 事件,便于获取 task_id。",
|
|
185
|
+
),
|
|
186
|
+
) -> None:
|
|
187
|
+
cli_context = get_cli_context(ctx)
|
|
188
|
+
try:
|
|
189
|
+
inputs = _read_workflow_inputs(inputs_file) if inputs_file else {}
|
|
190
|
+
on_event = None
|
|
191
|
+
if stream_events:
|
|
192
|
+
on_event = _emit_workflow_stream_event
|
|
193
|
+
result = WorkflowService().run_draft(
|
|
194
|
+
app_id,
|
|
195
|
+
inputs,
|
|
196
|
+
on_event=on_event,
|
|
197
|
+
profile=cli_context.profile,
|
|
198
|
+
query=query,
|
|
199
|
+
)
|
|
200
|
+
if stream_events:
|
|
201
|
+
result = dict(result)
|
|
202
|
+
result.pop("events", None)
|
|
203
|
+
except TacoError as error:
|
|
204
|
+
exit_with_error(ctx, error, json_default=True)
|
|
205
|
+
|
|
206
|
+
emit_success(ctx, result, json_default=True)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@workflow_app.command("stop", help="使用 task_id 停止 Workflow draft 调试任务。")
|
|
210
|
+
def stop_workflow(
|
|
211
|
+
ctx: typer.Context,
|
|
212
|
+
app_id: str = typer.Argument(..., help="应用 ID。"),
|
|
213
|
+
task_id: str = typer.Option(..., "--task-id", help="SSE 返回的任务 ID。"),
|
|
214
|
+
) -> None:
|
|
215
|
+
cli_context = get_cli_context(ctx)
|
|
216
|
+
try:
|
|
217
|
+
result = WorkflowService().stop_run(
|
|
218
|
+
app_id,
|
|
219
|
+
task_id,
|
|
220
|
+
profile=cli_context.profile,
|
|
221
|
+
)
|
|
222
|
+
except TacoError as error:
|
|
223
|
+
exit_with_error(ctx, error, json_default=True)
|
|
224
|
+
|
|
225
|
+
emit_success(ctx, result, json_default=True)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@workflow_app.command("publish", help="发布当前 Workflow draft。")
|
|
229
|
+
def publish_workflow(
|
|
230
|
+
ctx: typer.Context,
|
|
231
|
+
app_id: str = typer.Argument(..., help="应用 ID。"),
|
|
232
|
+
title: str | None = typer.Option(
|
|
233
|
+
None,
|
|
234
|
+
"--title",
|
|
235
|
+
help="发布版本名称。",
|
|
236
|
+
),
|
|
237
|
+
notes: str | None = typer.Option(
|
|
238
|
+
None,
|
|
239
|
+
"--notes",
|
|
240
|
+
help="发布说明。",
|
|
241
|
+
),
|
|
242
|
+
yes: bool = typer.Option(False, "--yes", help="确认发布 Workflow。"),
|
|
243
|
+
) -> None:
|
|
244
|
+
if not yes:
|
|
245
|
+
exit_with_error(
|
|
246
|
+
ctx,
|
|
247
|
+
TacoError(
|
|
248
|
+
code="WORKFLOW_PUBLISH_CONFIRM_REQUIRED",
|
|
249
|
+
message="发布 Workflow 前必须显式传入 --yes。",
|
|
250
|
+
exit_code=2,
|
|
251
|
+
),
|
|
252
|
+
json_default=True,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
cli_context = get_cli_context(ctx)
|
|
256
|
+
try:
|
|
257
|
+
result = WorkflowService().publish(
|
|
258
|
+
app_id,
|
|
259
|
+
title,
|
|
260
|
+
notes,
|
|
261
|
+
profile=cli_context.profile,
|
|
262
|
+
)
|
|
263
|
+
except TacoError as error:
|
|
264
|
+
exit_with_error(ctx, error, json_default=True)
|
|
265
|
+
|
|
266
|
+
emit_success(ctx, result, json_default=True)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@workflow_tool_app.command("get", help="按应用 ID 或 Tool ID 查询 Workflow Tool。")
|
|
270
|
+
def get_workflow_tool(
|
|
271
|
+
ctx: typer.Context,
|
|
272
|
+
app_id: str | None = typer.Option(
|
|
273
|
+
None,
|
|
274
|
+
"--app-id",
|
|
275
|
+
help="已发布 Workflow 的应用 ID。",
|
|
276
|
+
),
|
|
277
|
+
tool_id: str | None = typer.Option(
|
|
278
|
+
None,
|
|
279
|
+
"--tool-id",
|
|
280
|
+
help="Workflow Tool ID。",
|
|
281
|
+
),
|
|
282
|
+
) -> None:
|
|
283
|
+
if (app_id is None) == (tool_id is None):
|
|
284
|
+
exit_with_error(
|
|
285
|
+
ctx,
|
|
286
|
+
TacoError(
|
|
287
|
+
code="WORKFLOW_TOOL_QUERY_INVALID",
|
|
288
|
+
message="查询 Workflow Tool 必须且只能提供 --app-id 或 --tool-id。",
|
|
289
|
+
exit_code=2,
|
|
290
|
+
),
|
|
291
|
+
json_default=True,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
cli_context = get_cli_context(ctx)
|
|
295
|
+
try:
|
|
296
|
+
result = WorkflowToolService().get_tool(
|
|
297
|
+
workflow_app_id=app_id,
|
|
298
|
+
workflow_tool_id=tool_id,
|
|
299
|
+
profile=cli_context.profile,
|
|
300
|
+
)
|
|
301
|
+
except TacoError as error:
|
|
302
|
+
exit_with_error(ctx, error, json_default=True)
|
|
303
|
+
emit_success(ctx, result, json_default=True)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@workflow_tool_app.command(
|
|
307
|
+
"publish",
|
|
308
|
+
help="把已发布 Workflow 注册为 Tool;不会自动发布 Workflow。",
|
|
309
|
+
)
|
|
310
|
+
def publish_workflow_tool(
|
|
311
|
+
ctx: typer.Context,
|
|
312
|
+
app_id: str = typer.Argument(..., help="已发布 Workflow 的应用 ID。"),
|
|
313
|
+
file: Path = typer.Option(
|
|
314
|
+
...,
|
|
315
|
+
"-f",
|
|
316
|
+
"--file",
|
|
317
|
+
help="本地 HTTP Workflow YAML 文件。",
|
|
318
|
+
),
|
|
319
|
+
name: str = typer.Option(
|
|
320
|
+
...,
|
|
321
|
+
"--name",
|
|
322
|
+
help="ASCII 字母、数字或下划线组成的 Tool 机器名。",
|
|
323
|
+
),
|
|
324
|
+
label: str | None = typer.Option(
|
|
325
|
+
None,
|
|
326
|
+
"--label",
|
|
327
|
+
help="Tool 展示名,默认使用 DSL name。",
|
|
328
|
+
),
|
|
329
|
+
description: str | None = typer.Option(
|
|
330
|
+
None,
|
|
331
|
+
"--description",
|
|
332
|
+
help="Tool 描述,默认使用 DSL description。",
|
|
333
|
+
),
|
|
334
|
+
yes: bool = typer.Option(False, "--yes", help="确认发布 Workflow Tool。"),
|
|
335
|
+
) -> None:
|
|
336
|
+
if not yes:
|
|
337
|
+
exit_with_error(
|
|
338
|
+
ctx,
|
|
339
|
+
TacoError(
|
|
340
|
+
code="WORKFLOW_TOOL_PUBLISH_CONFIRM_REQUIRED",
|
|
341
|
+
message="发布 Workflow Tool 前必须显式传入 --yes。",
|
|
342
|
+
exit_code=2,
|
|
343
|
+
),
|
|
344
|
+
json_default=True,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
cli_context = get_cli_context(ctx)
|
|
348
|
+
try:
|
|
349
|
+
workflow = load_workflow_file(file)
|
|
350
|
+
compile_workflow(workflow)
|
|
351
|
+
result = WorkflowToolService().publish_tool(
|
|
352
|
+
app_id,
|
|
353
|
+
workflow,
|
|
354
|
+
name,
|
|
355
|
+
workflow["name"] if label is None else label,
|
|
356
|
+
workflow.get("description", "") if description is None else description,
|
|
357
|
+
profile=cli_context.profile,
|
|
358
|
+
)
|
|
359
|
+
except TacoError as error:
|
|
360
|
+
exit_with_error(ctx, error, json_default=True)
|
|
361
|
+
emit_success(ctx, result, json_default=True)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@workflow_tool_app.command(
|
|
365
|
+
"update",
|
|
366
|
+
help="用本地 Workflow DSL 更新 Tool 配置;不会自动发布 Workflow。",
|
|
367
|
+
)
|
|
368
|
+
def update_workflow_tool(
|
|
369
|
+
ctx: typer.Context,
|
|
370
|
+
tool_id: str = typer.Argument(..., help="Workflow Tool ID。"),
|
|
371
|
+
file: Path = typer.Option(
|
|
372
|
+
...,
|
|
373
|
+
"-f",
|
|
374
|
+
"--file",
|
|
375
|
+
help="本地 HTTP Workflow YAML 文件。",
|
|
376
|
+
),
|
|
377
|
+
name: str | None = typer.Option(
|
|
378
|
+
None,
|
|
379
|
+
"--name",
|
|
380
|
+
help="新的 Tool 机器名;省略时保留远端值。",
|
|
381
|
+
),
|
|
382
|
+
label: str | None = typer.Option(
|
|
383
|
+
None,
|
|
384
|
+
"--label",
|
|
385
|
+
help="新的 Tool 展示名;省略时保留远端值。",
|
|
386
|
+
),
|
|
387
|
+
description: str | None = typer.Option(
|
|
388
|
+
None,
|
|
389
|
+
"--description",
|
|
390
|
+
help="新的 Tool 描述;省略时保留远端值。",
|
|
391
|
+
),
|
|
392
|
+
yes: bool = typer.Option(False, "--yes", help="确认更新 Workflow Tool。"),
|
|
393
|
+
) -> None:
|
|
394
|
+
if not yes:
|
|
395
|
+
exit_with_error(
|
|
396
|
+
ctx,
|
|
397
|
+
TacoError(
|
|
398
|
+
code="WORKFLOW_TOOL_UPDATE_CONFIRM_REQUIRED",
|
|
399
|
+
message="更新 Workflow Tool 前必须显式传入 --yes。",
|
|
400
|
+
exit_code=2,
|
|
401
|
+
),
|
|
402
|
+
json_default=True,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
cli_context = get_cli_context(ctx)
|
|
406
|
+
try:
|
|
407
|
+
workflow = load_workflow_file(file)
|
|
408
|
+
compile_workflow(workflow)
|
|
409
|
+
result = WorkflowToolService().update_tool(
|
|
410
|
+
tool_id,
|
|
411
|
+
workflow,
|
|
412
|
+
name=name,
|
|
413
|
+
label=label,
|
|
414
|
+
description=description,
|
|
415
|
+
profile=cli_context.profile,
|
|
416
|
+
)
|
|
417
|
+
except TacoError as error:
|
|
418
|
+
exit_with_error(ctx, error, json_default=True)
|
|
419
|
+
emit_success(ctx, result, json_default=True)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@workflow_secret_app.command("set", help="从环境变量设置远端 Workflow secret。")
|
|
423
|
+
def set_workflow_secret(
|
|
424
|
+
ctx: typer.Context,
|
|
425
|
+
app_id: str = typer.Argument(..., help="应用 ID。"),
|
|
426
|
+
name: str = typer.Argument(..., help="Dify 环境变量 selector。"),
|
|
427
|
+
from_env: str = typer.Option(
|
|
428
|
+
...,
|
|
429
|
+
"--from-env",
|
|
430
|
+
help="读取 secret 值的本地环境变量名。",
|
|
431
|
+
),
|
|
432
|
+
yes: bool = typer.Option(False, "--yes", help="确认更新远端 secret。"),
|
|
433
|
+
) -> None:
|
|
434
|
+
if not yes:
|
|
435
|
+
exit_with_error(
|
|
436
|
+
ctx,
|
|
437
|
+
TacoError(
|
|
438
|
+
code="WORKFLOW_SECRET_CONFIRM_REQUIRED",
|
|
439
|
+
message="更新 Workflow secret 前必须显式传入 --yes。",
|
|
440
|
+
exit_code=2,
|
|
441
|
+
),
|
|
442
|
+
json_default=True,
|
|
443
|
+
)
|
|
444
|
+
if _SECRET_NAME.fullmatch(name) is None:
|
|
445
|
+
exit_with_error(
|
|
446
|
+
ctx,
|
|
447
|
+
TacoError(
|
|
448
|
+
code="WORKFLOW_SECRET_NAME_INVALID",
|
|
449
|
+
message="Workflow secret name 不符合 Dify selector 规则。",
|
|
450
|
+
exit_code=2,
|
|
451
|
+
hint="请使用字母或下划线开头、最长 30 位的字母数字下划线。",
|
|
452
|
+
),
|
|
453
|
+
json_default=True,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
value = os.environ.get(from_env)
|
|
457
|
+
if value is None or value == "":
|
|
458
|
+
exit_with_error(
|
|
459
|
+
ctx,
|
|
460
|
+
TacoError(
|
|
461
|
+
code="WORKFLOW_SECRET_ENV_REQUIRED",
|
|
462
|
+
message="指定环境变量不存在或值为空。",
|
|
463
|
+
exit_code=2,
|
|
464
|
+
hint="请先在当前进程环境中设置 --from-env 指定的变量。",
|
|
465
|
+
),
|
|
466
|
+
json_default=True,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
cli_context = get_cli_context(ctx)
|
|
470
|
+
try:
|
|
471
|
+
result = WorkflowService().set_secret(
|
|
472
|
+
app_id,
|
|
473
|
+
name,
|
|
474
|
+
value,
|
|
475
|
+
profile=cli_context.profile,
|
|
476
|
+
)
|
|
477
|
+
action = result.get("action") if isinstance(result, dict) else None
|
|
478
|
+
if action not in {"created", "updated"}:
|
|
479
|
+
raise TacoError(
|
|
480
|
+
code="WORKFLOW_SECRET_RESPONSE_INVALID",
|
|
481
|
+
message="Workflow secret 更新响应格式不合法。",
|
|
482
|
+
exit_code=4,
|
|
483
|
+
)
|
|
484
|
+
except TacoError as error:
|
|
485
|
+
exit_with_error(
|
|
486
|
+
ctx,
|
|
487
|
+
_redact_secret_error(error, value),
|
|
488
|
+
json_default=True,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
emit_success(
|
|
492
|
+
ctx,
|
|
493
|
+
{
|
|
494
|
+
"app_id": app_id,
|
|
495
|
+
"name": name,
|
|
496
|
+
"action": action,
|
|
497
|
+
},
|
|
498
|
+
json_default=True,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@workflow_app.command("validate", help="校验 YAML DSL 并本地编译为 Dify graph。")
|
|
503
|
+
def validate_workflow(
|
|
504
|
+
ctx: typer.Context,
|
|
505
|
+
file: Path = typer.Option(
|
|
506
|
+
...,
|
|
507
|
+
"-f",
|
|
508
|
+
"--file",
|
|
509
|
+
help="本地 HTTP Workflow YAML 文件。",
|
|
510
|
+
),
|
|
511
|
+
) -> None:
|
|
512
|
+
try:
|
|
513
|
+
graph = compile_workflow_file(file)
|
|
514
|
+
except TacoError as error:
|
|
515
|
+
exit_with_error(ctx, error, json_default=True)
|
|
516
|
+
emit_success(
|
|
517
|
+
ctx,
|
|
518
|
+
{
|
|
519
|
+
"valid": True,
|
|
520
|
+
"path": str(file),
|
|
521
|
+
"graph_summary": summarize_graph(graph),
|
|
522
|
+
},
|
|
523
|
+
json_default=True,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _prepare_workflow_output(path: Path, *, overwrite: bool) -> None:
|
|
528
|
+
if path.is_symlink():
|
|
529
|
+
raise TacoError(
|
|
530
|
+
code="WORKFLOW_DSL_PATH_INVALID",
|
|
531
|
+
message="Workflow DSL 输出不能是符号链接。",
|
|
532
|
+
exit_code=2,
|
|
533
|
+
hint="请使用普通文件路径。",
|
|
534
|
+
)
|
|
535
|
+
if path.exists():
|
|
536
|
+
if not path.is_file():
|
|
537
|
+
raise TacoError(
|
|
538
|
+
code="WORKFLOW_DSL_PATH_INVALID",
|
|
539
|
+
message="Workflow DSL 输出必须是普通文件路径。",
|
|
540
|
+
exit_code=2,
|
|
541
|
+
)
|
|
542
|
+
if not overwrite:
|
|
543
|
+
raise TacoError(
|
|
544
|
+
code="WORKFLOW_DSL_EXISTS",
|
|
545
|
+
message="本地 Workflow DSL 已存在。",
|
|
546
|
+
exit_code=2,
|
|
547
|
+
hint="确认覆盖后传入 --overwrite --yes。",
|
|
548
|
+
)
|
|
549
|
+
if not os.access(path, os.W_OK):
|
|
550
|
+
raise TacoError(
|
|
551
|
+
code="WORKFLOW_DSL_PATH_INVALID",
|
|
552
|
+
message="本地 Workflow DSL 不可覆盖。",
|
|
553
|
+
exit_code=2,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
try:
|
|
557
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
558
|
+
with tempfile.NamedTemporaryFile(
|
|
559
|
+
dir=path.parent,
|
|
560
|
+
prefix=".taco-workflow-write-check-",
|
|
561
|
+
):
|
|
562
|
+
pass
|
|
563
|
+
except OSError as error:
|
|
564
|
+
raise TacoError(
|
|
565
|
+
code="WORKFLOW_DSL_WRITE_FAILED",
|
|
566
|
+
message="创建远端 Workflow 前无法确认本地 DSL 目录可写。",
|
|
567
|
+
exit_code=2,
|
|
568
|
+
hint="请检查输出目录权限和磁盘空间。",
|
|
569
|
+
) from error
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _emit_workflow_stream_event(event: dict[str, Any]) -> None:
|
|
573
|
+
typer.echo(to_json_line({"type": "event", "event": event}))
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _stage_workflow_yaml(path: Path, payload: dict[str, Any]) -> Path:
|
|
577
|
+
temporary: Path | None = None
|
|
578
|
+
try:
|
|
579
|
+
content = yaml.safe_dump(
|
|
580
|
+
payload,
|
|
581
|
+
allow_unicode=True,
|
|
582
|
+
sort_keys=False,
|
|
583
|
+
)
|
|
584
|
+
descriptor, temporary_name = tempfile.mkstemp(
|
|
585
|
+
dir=path.parent,
|
|
586
|
+
prefix=f".{path.name}.",
|
|
587
|
+
suffix=".tmp",
|
|
588
|
+
text=True,
|
|
589
|
+
)
|
|
590
|
+
temporary = Path(temporary_name)
|
|
591
|
+
os.fchmod(descriptor, 0o600)
|
|
592
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as file:
|
|
593
|
+
file.write(content)
|
|
594
|
+
file.flush()
|
|
595
|
+
os.fsync(file.fileno())
|
|
596
|
+
return temporary
|
|
597
|
+
except (OSError, yaml.YAMLError) as error:
|
|
598
|
+
_cleanup_workflow_temporary(temporary)
|
|
599
|
+
raise TacoError(
|
|
600
|
+
code="WORKFLOW_DSL_WRITE_FAILED",
|
|
601
|
+
message="无法准备 Workflow DSL 临时文件。",
|
|
602
|
+
exit_code=2,
|
|
603
|
+
hint="请检查输出目录权限和磁盘空间。",
|
|
604
|
+
) from error
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _commit_workflow_yaml(
|
|
608
|
+
temporary: Path,
|
|
609
|
+
path: Path,
|
|
610
|
+
*,
|
|
611
|
+
overwrite: bool,
|
|
612
|
+
app_id: str,
|
|
613
|
+
) -> None:
|
|
614
|
+
try:
|
|
615
|
+
if overwrite:
|
|
616
|
+
os.replace(temporary, path)
|
|
617
|
+
else:
|
|
618
|
+
os.link(temporary, path)
|
|
619
|
+
except OSError as error:
|
|
620
|
+
raise TacoError(
|
|
621
|
+
code="WORKFLOW_DSL_WRITE_FAILED",
|
|
622
|
+
message="远端 Workflow 已创建,但本地 DSL 提交失败。",
|
|
623
|
+
exit_code=2,
|
|
624
|
+
hint="请使用错误中的 app_id 恢复本地文件,避免重复创建应用。",
|
|
625
|
+
app_id=app_id,
|
|
626
|
+
) from error
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _cleanup_workflow_temporary(temporary: Path | None) -> None:
|
|
630
|
+
if temporary is None:
|
|
631
|
+
return
|
|
632
|
+
try:
|
|
633
|
+
temporary.unlink(missing_ok=True)
|
|
634
|
+
except OSError:
|
|
635
|
+
pass
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _read_workflow_inputs(path: Path) -> dict[str, Any]:
|
|
639
|
+
try:
|
|
640
|
+
raw_content = read_regular_file(
|
|
641
|
+
path,
|
|
642
|
+
max_bytes=_MAX_INPUTS_FILE_SIZE,
|
|
643
|
+
)
|
|
644
|
+
content = raw_content.decode("utf-8")
|
|
645
|
+
payload = json.loads(
|
|
646
|
+
content,
|
|
647
|
+
parse_constant=_reject_json_constant,
|
|
648
|
+
)
|
|
649
|
+
if not isinstance(payload, dict):
|
|
650
|
+
raise _inputs_error("inputs JSON 必须是对象。")
|
|
651
|
+
_validate_inputs_structure(payload)
|
|
652
|
+
return payload
|
|
653
|
+
except SecureFileReadError as error:
|
|
654
|
+
reasons = {
|
|
655
|
+
"symlink": "输入文件不能是符号链接。",
|
|
656
|
+
"not_regular": "输入路径不是普通文件。",
|
|
657
|
+
"too_large": "输入文件最大允许 1 MiB。",
|
|
658
|
+
"changed": "输入文件在安全打开期间发生变化。",
|
|
659
|
+
"unreadable": "输入文件不存在或不可读。",
|
|
660
|
+
}
|
|
661
|
+
raise _inputs_error(
|
|
662
|
+
reasons.get(error.reason, "输入文件不可安全读取。")
|
|
663
|
+
) from error
|
|
664
|
+
except TacoError:
|
|
665
|
+
raise
|
|
666
|
+
except (OSError, UnicodeError, ValueError, RecursionError) as error:
|
|
667
|
+
raise _inputs_error("文件不可读或不是合法 JSON。") from error
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _validate_inputs_structure(value: Any, *, depth: int = 0) -> None:
|
|
671
|
+
if depth > _MAX_INPUTS_DEPTH:
|
|
672
|
+
raise _inputs_error("inputs JSON 嵌套层级过深。")
|
|
673
|
+
if value is None or isinstance(value, (str, bool, int)):
|
|
674
|
+
return
|
|
675
|
+
if isinstance(value, float):
|
|
676
|
+
if not math.isfinite(value):
|
|
677
|
+
raise _inputs_error("inputs JSON 包含非有限数值。")
|
|
678
|
+
return
|
|
679
|
+
if isinstance(value, list):
|
|
680
|
+
for item in value:
|
|
681
|
+
_validate_inputs_structure(item, depth=depth + 1)
|
|
682
|
+
return
|
|
683
|
+
if isinstance(value, dict):
|
|
684
|
+
for item in value.values():
|
|
685
|
+
_validate_inputs_structure(item, depth=depth + 1)
|
|
686
|
+
return
|
|
687
|
+
raise _inputs_error("inputs JSON 包含不支持的数据类型。")
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _reject_json_constant(_: str) -> None:
|
|
691
|
+
raise ValueError("non-standard JSON constant")
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _inputs_error(reason: str) -> TacoError:
|
|
695
|
+
return TacoError(
|
|
696
|
+
code="WORKFLOW_INPUTS_INVALID",
|
|
697
|
+
message=f"Workflow inputs 无效:{reason}",
|
|
698
|
+
exit_code=2,
|
|
699
|
+
hint="请提供不超过 1 MiB 的普通 JSON object 文件。",
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _redact_secret_error(error: TacoError, value: str) -> TacoError:
|
|
704
|
+
if value not in error.message and (
|
|
705
|
+
error.hint is None or value not in error.hint
|
|
706
|
+
):
|
|
707
|
+
return error
|
|
708
|
+
return TacoError(
|
|
709
|
+
code="WORKFLOW_SECRET_UPDATE_FAILED",
|
|
710
|
+
message="Workflow secret 更新失败。",
|
|
711
|
+
exit_code=error.exit_code,
|
|
712
|
+
status=error.status,
|
|
713
|
+
hint="请检查认证、网络和 draft 状态后重试。",
|
|
714
|
+
)
|
taco/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core primitives for TACO."""
|