@qingflow-tech/qingflow-app-builder-mcp 1.0.11 → 1.0.13
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.
- package/README.md +6 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +12 -12
- package/skills/qingflow-app-builder/references/create-app.md +3 -3
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +1 -1
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +6 -5
- package/skills/qingflow-app-builder/references/update-views.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -3
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +11 -0
- package/src/qingflow_mcp/builder_facade/service.py +1488 -288
- package/src/qingflow_mcp/cli/commands/builder.py +2 -2
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +91 -19
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +206 -7
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +21 -15
- package/src/qingflow_mcp/response_trim.py +74 -13
- package/src/qingflow_mcp/server.py +11 -9
- package/src/qingflow_mcp/server_app_builder.py +3 -2
- package/src/qingflow_mcp/server_app_user.py +19 -13
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +36 -11
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +244 -34
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +1067 -349
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -50,7 +50,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
50
50
|
member = builder_subparsers.add_parser("member", help="成员目录")
|
|
51
51
|
member_subparsers = member.add_subparsers(dest="builder_member_command", required=True)
|
|
52
52
|
member_search = member_subparsers.add_parser("search", help="搜索成员")
|
|
53
|
-
member_search.add_argument("--query",
|
|
53
|
+
member_search.add_argument("--query", required=True)
|
|
54
54
|
member_search.add_argument("--page-num", type=int, default=1)
|
|
55
55
|
member_search.add_argument("--page-size", type=int, default=20)
|
|
56
56
|
member_search.add_argument("--contain-disable", action=argparse.BooleanOptionalAction, default=False)
|
|
@@ -59,7 +59,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
59
59
|
role = builder_subparsers.add_parser("role", help="角色目录")
|
|
60
60
|
role_subparsers = role.add_subparsers(dest="builder_role_command", required=True)
|
|
61
61
|
role_search = role_subparsers.add_parser("search", help="搜索角色")
|
|
62
|
-
role_search.add_argument("--keyword",
|
|
62
|
+
role_search.add_argument("--keyword", required=True)
|
|
63
63
|
role_search.add_argument("--page-num", type=int, default=1)
|
|
64
64
|
role_search.add_argument("--page-size", type=int, default=20)
|
|
65
65
|
role_search.set_defaults(handler=_handle_role_search, format_hint="builder_summary")
|
|
@@ -12,7 +12,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
12
12
|
|
|
13
13
|
start = export_subparsers.add_parser("start", help="启动导出")
|
|
14
14
|
start.add_argument("--app-key", required=True)
|
|
15
|
-
start.add_argument("--view-id",
|
|
15
|
+
start.add_argument("--view-id", required=True)
|
|
16
16
|
start.add_argument("--column", dest="columns", action="append", type=int, default=[], help="只导出这些 field_id;不传时导出当前视图全部字段")
|
|
17
17
|
start.add_argument("--columns-file", help="JSON/YAML list,内容与 --column 语义一致")
|
|
18
18
|
start.add_argument("--where-file", help="JSON/YAML list,内容与 record list 的 where DSL 一致;内部先查命中 record_id 再走原生导出")
|
|
@@ -33,7 +33,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
33
33
|
|
|
34
34
|
direct = export_subparsers.add_parser("direct", help="直接导出并下载")
|
|
35
35
|
direct.add_argument("--app-key", required=True)
|
|
36
|
-
direct.add_argument("--view-id",
|
|
36
|
+
direct.add_argument("--view-id", required=True)
|
|
37
37
|
direct.add_argument("--column", dest="columns", action="append", type=int, default=[], help="只导出这些 field_id;不传时导出当前视图全部字段")
|
|
38
38
|
direct.add_argument("--columns-file", help="JSON/YAML list,内容与 --column 语义一致")
|
|
39
39
|
direct.add_argument("--where-file", help="JSON/YAML list,内容与 record list 的 where DSL 一致;内部先查命中 record_id 再走原生导出")
|
|
@@ -13,7 +13,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
13
13
|
template = import_subparsers.add_parser("template", help="下载导入模板")
|
|
14
14
|
template.add_argument("--app-key", required=True)
|
|
15
15
|
template.add_argument("--download-to-path")
|
|
16
|
-
template.set_defaults(handler=_handle_template, format_hint="")
|
|
16
|
+
template.set_defaults(handler=_handle_template, format_hint="import_template")
|
|
17
17
|
|
|
18
18
|
verify = import_subparsers.add_parser("verify", help="校验导入文件")
|
|
19
19
|
verify.add_argument("--app-key", required=True)
|
|
@@ -18,12 +18,20 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
18
18
|
|
|
19
19
|
schema = record_subparsers.add_parser("schema", help="读取记录相关表结构")
|
|
20
20
|
schema.add_argument("--mode", dest="legacy_mode", help=argparse.SUPPRESS)
|
|
21
|
-
schema_subparsers = schema.add_subparsers(
|
|
21
|
+
schema_subparsers = schema.add_subparsers(
|
|
22
|
+
dest="record_schema_command",
|
|
23
|
+
metavar="{browse,insert,update,import,code-block}",
|
|
24
|
+
)
|
|
22
25
|
schema.set_defaults(handler=_handle_schema_root, format_hint="")
|
|
23
26
|
|
|
24
|
-
schema_applicant = schema_subparsers.add_parser("applicant", help=
|
|
27
|
+
schema_applicant = schema_subparsers.add_parser("applicant", help=argparse.SUPPRESS)
|
|
25
28
|
schema_applicant.add_argument("--app-key", required=True)
|
|
26
29
|
schema_applicant.set_defaults(handler=_handle_schema_applicant, format_hint="")
|
|
30
|
+
schema_subparsers._choices_actions = [ # type: ignore[attr-defined]
|
|
31
|
+
action
|
|
32
|
+
for action in schema_subparsers._choices_actions # type: ignore[attr-defined]
|
|
33
|
+
if getattr(action, "dest", None) != "applicant"
|
|
34
|
+
]
|
|
27
35
|
|
|
28
36
|
schema_browse = schema_subparsers.add_parser("browse", help="读取浏览视图表结构")
|
|
29
37
|
schema_browse.add_argument("--app-key", required=True)
|
|
@@ -37,6 +45,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
37
45
|
schema_update = schema_subparsers.add_parser("update", help="读取更新记录表结构")
|
|
38
46
|
schema_update.add_argument("--app-key", required=True)
|
|
39
47
|
schema_update.add_argument("--record-id", required=True)
|
|
48
|
+
schema_update.add_argument("--view-id")
|
|
40
49
|
schema_update.set_defaults(handler=_handle_schema_update, format_hint="")
|
|
41
50
|
|
|
42
51
|
schema_import = schema_subparsers.add_parser("import", help="读取导入表结构")
|
|
@@ -71,11 +80,33 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
71
80
|
|
|
72
81
|
list_parser = record_subparsers.add_parser("list", help="列出记录")
|
|
73
82
|
list_parser.add_argument("--app-key", required=True)
|
|
74
|
-
list_parser.add_argument(
|
|
75
|
-
|
|
83
|
+
list_parser.add_argument(
|
|
84
|
+
"--column",
|
|
85
|
+
dest="columns",
|
|
86
|
+
action="append",
|
|
87
|
+
type=int,
|
|
88
|
+
default=[],
|
|
89
|
+
metavar="FIELD_ID",
|
|
90
|
+
help="只返回这些 field_id;可重复传",
|
|
91
|
+
)
|
|
92
|
+
list_parser.add_argument(
|
|
93
|
+
"--columns-file",
|
|
94
|
+
help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
|
|
95
|
+
)
|
|
76
96
|
list_parser.add_argument("--query")
|
|
77
|
-
list_parser.add_argument(
|
|
78
|
-
|
|
97
|
+
list_parser.add_argument(
|
|
98
|
+
"--query-field",
|
|
99
|
+
dest="query_fields",
|
|
100
|
+
action="append",
|
|
101
|
+
type=int,
|
|
102
|
+
default=[],
|
|
103
|
+
metavar="FIELD_ID",
|
|
104
|
+
help="全文搜索范围 field_id;可重复传",
|
|
105
|
+
)
|
|
106
|
+
list_parser.add_argument(
|
|
107
|
+
"--query-fields-file",
|
|
108
|
+
help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
|
|
109
|
+
)
|
|
79
110
|
list_parser.add_argument("--where-file")
|
|
80
111
|
list_parser.add_argument("--order-by-file")
|
|
81
112
|
list_parser.add_argument("--page", type=int, default=1)
|
|
@@ -87,8 +118,19 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
87
118
|
|
|
88
119
|
access_parser = record_subparsers.add_parser("access", help="访问记录并写入本地 CSV 分片")
|
|
89
120
|
access_parser.add_argument("--app-key", required=True)
|
|
90
|
-
access_parser.add_argument(
|
|
91
|
-
|
|
121
|
+
access_parser.add_argument(
|
|
122
|
+
"--column",
|
|
123
|
+
dest="columns",
|
|
124
|
+
action="append",
|
|
125
|
+
type=int,
|
|
126
|
+
default=[],
|
|
127
|
+
metavar="FIELD_ID",
|
|
128
|
+
help="导出这些 field_id 到 CSV;可重复传",
|
|
129
|
+
)
|
|
130
|
+
access_parser.add_argument(
|
|
131
|
+
"--columns-file",
|
|
132
|
+
help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
|
|
133
|
+
)
|
|
92
134
|
access_parser.add_argument("--where-file")
|
|
93
135
|
access_parser.add_argument("--order-by-file")
|
|
94
136
|
access_parser.add_argument("--view-id", required=True)
|
|
@@ -97,15 +139,26 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
97
139
|
get = record_subparsers.add_parser("get", help="读取单条记录")
|
|
98
140
|
get.add_argument("--app-key", required=True)
|
|
99
141
|
get.add_argument("--record-id", required=True)
|
|
100
|
-
get.add_argument(
|
|
101
|
-
|
|
142
|
+
get.add_argument(
|
|
143
|
+
"--column",
|
|
144
|
+
dest="columns",
|
|
145
|
+
action="append",
|
|
146
|
+
type=int,
|
|
147
|
+
default=[],
|
|
148
|
+
metavar="FIELD_ID",
|
|
149
|
+
help="聚焦这些 field_id;可重复传",
|
|
150
|
+
)
|
|
151
|
+
get.add_argument(
|
|
152
|
+
"--columns-file",
|
|
153
|
+
help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
|
|
154
|
+
)
|
|
102
155
|
get.add_argument("--view-id")
|
|
103
156
|
get.set_defaults(handler=_handle_get, format_hint="record_get")
|
|
104
157
|
|
|
105
158
|
logs = record_subparsers.add_parser("logs", help="读取单条记录全量日志并写入本地 JSONL")
|
|
106
159
|
logs.add_argument("--app-key", required=True)
|
|
107
160
|
logs.add_argument("--record-id", required=True)
|
|
108
|
-
logs.add_argument("--view-id")
|
|
161
|
+
logs.add_argument("--view-id", required=True)
|
|
109
162
|
logs.set_defaults(handler=_handle_logs, format_hint="record_logs")
|
|
110
163
|
|
|
111
164
|
insert = record_subparsers.add_parser("insert", help="新增记录")
|
|
@@ -113,22 +166,25 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
113
166
|
insert.add_argument("--fields-file", help=argparse.SUPPRESS)
|
|
114
167
|
insert.add_argument("--items-file")
|
|
115
168
|
insert.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
|
|
116
|
-
insert.set_defaults(handler=_handle_insert, format_hint="")
|
|
169
|
+
insert.set_defaults(handler=_handle_insert, format_hint="record_write")
|
|
117
170
|
|
|
118
171
|
update = record_subparsers.add_parser("update", help="更新记录")
|
|
119
172
|
update.add_argument("--app-key", required=True)
|
|
120
173
|
update.add_argument("--record-id")
|
|
121
174
|
update.add_argument("--fields-file")
|
|
122
175
|
update.add_argument("--items-file")
|
|
176
|
+
update.add_argument("--view-id")
|
|
123
177
|
update.add_argument("--dry-run", action=argparse.BooleanOptionalAction, default=False)
|
|
124
178
|
update.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
|
|
125
|
-
update.set_defaults(handler=_handle_update, format_hint="")
|
|
179
|
+
update.set_defaults(handler=_handle_update, format_hint="record_write")
|
|
126
180
|
|
|
127
181
|
delete = record_subparsers.add_parser("delete", help="删除记录")
|
|
128
182
|
delete.add_argument("--app-key", required=True)
|
|
129
183
|
delete.add_argument("--record-id")
|
|
130
184
|
delete.add_argument("--record-ids-file")
|
|
131
|
-
delete.
|
|
185
|
+
delete.add_argument("--view-id")
|
|
186
|
+
delete.add_argument("--list-type", dest="legacy_list_type", type=int, help=argparse.SUPPRESS)
|
|
187
|
+
delete.set_defaults(handler=_handle_delete, format_hint="record_delete")
|
|
132
188
|
|
|
133
189
|
analyze = record_subparsers.add_parser("analyze", help=argparse.SUPPRESS)
|
|
134
190
|
record_subparsers._choices_actions = [ # type: ignore[attr-defined]
|
|
@@ -153,6 +209,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
153
209
|
code_block.add_argument("--app-key", required=True)
|
|
154
210
|
code_block.add_argument("--record-id", required=True)
|
|
155
211
|
code_block.add_argument("--code-block-field", required=True)
|
|
212
|
+
code_block.add_argument("--view-id")
|
|
156
213
|
code_block.add_argument("--role", type=int, default=1)
|
|
157
214
|
code_block.add_argument("--workflow-node-id", type=int)
|
|
158
215
|
code_block.add_argument("--answers-file")
|
|
@@ -161,7 +218,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
161
218
|
code_block.add_argument("--apply-writeback", action=argparse.BooleanOptionalAction, default=True)
|
|
162
219
|
code_block.add_argument("--verify-writeback", action=argparse.BooleanOptionalAction, default=True)
|
|
163
220
|
code_block.add_argument("--force-refresh-form", action="store_true")
|
|
164
|
-
code_block.set_defaults(handler=_handle_code_block_run, format_hint="")
|
|
221
|
+
code_block.set_defaults(handler=_handle_code_block_run, format_hint="code_block_run")
|
|
165
222
|
|
|
166
223
|
|
|
167
224
|
def _columns(args: argparse.Namespace) -> list[Any]:
|
|
@@ -182,20 +239,20 @@ def _handle_schema_root(args: argparse.Namespace, _context: CliContext) -> dict:
|
|
|
182
239
|
mode = (args.legacy_mode or "").strip()
|
|
183
240
|
if mode:
|
|
184
241
|
replacement = {
|
|
185
|
-
"applicant": "record schema
|
|
242
|
+
"applicant": "record schema insert --app-key APP_KEY",
|
|
186
243
|
"browse": "record schema browse --app-key APP_KEY --view-id VIEW_ID",
|
|
187
244
|
"insert": "record schema insert --app-key APP_KEY",
|
|
188
|
-
"update": "record schema update --app-key APP_KEY --record-id RECORD_ID",
|
|
245
|
+
"update": "record schema update --app-key APP_KEY --record-id RECORD_ID [--view-id VIEW_ID]",
|
|
189
246
|
"import": "record schema import --app-key APP_KEY",
|
|
190
247
|
"code-block": "record schema code-block --app-key APP_KEY",
|
|
191
|
-
}.get(mode, "record schema <
|
|
248
|
+
}.get(mode, "record schema <browse|insert|update|import|code-block> ...")
|
|
192
249
|
raise_config_error(
|
|
193
250
|
"record schema --mode is no longer accepted.",
|
|
194
251
|
fix_hint=f"Use `{replacement}` instead.",
|
|
195
252
|
)
|
|
196
253
|
raise_config_error(
|
|
197
254
|
"record schema requires an explicit subcommand.",
|
|
198
|
-
fix_hint="Use one of: `record schema
|
|
255
|
+
fix_hint="Use one of: `record schema browse`, `record schema insert`, `record schema update`, `record schema import`, or `record schema code-block`.",
|
|
199
256
|
)
|
|
200
257
|
|
|
201
258
|
|
|
@@ -224,6 +281,7 @@ def _handle_schema_update(args: argparse.Namespace, context: CliContext) -> dict
|
|
|
224
281
|
profile=args.profile,
|
|
225
282
|
app_key=args.app_key,
|
|
226
283
|
record_id=args.record_id,
|
|
284
|
+
view_id=args.view_id,
|
|
227
285
|
)
|
|
228
286
|
|
|
229
287
|
|
|
@@ -382,6 +440,7 @@ def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
382
440
|
record_id=None,
|
|
383
441
|
fields=None,
|
|
384
442
|
items=require_list_arg(args.items_file, option_name="--items-file"),
|
|
443
|
+
view_id=args.view_id,
|
|
385
444
|
dry_run=bool(args.dry_run),
|
|
386
445
|
verify_write=bool(args.verify_write),
|
|
387
446
|
)
|
|
@@ -400,17 +459,29 @@ def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
400
459
|
app_key=args.app_key,
|
|
401
460
|
record_id=args.record_id,
|
|
402
461
|
fields=require_object_arg(args.fields_file, option_name="--fields-file"),
|
|
462
|
+
view_id=args.view_id,
|
|
403
463
|
verify_write=bool(args.verify_write),
|
|
404
464
|
)
|
|
405
465
|
|
|
406
466
|
|
|
407
467
|
def _handle_delete(args: argparse.Namespace, context: CliContext) -> dict:
|
|
468
|
+
if args.legacy_list_type is not None:
|
|
469
|
+
raise_config_error(
|
|
470
|
+
"record delete no longer accepts list_type.",
|
|
471
|
+
fix_hint="Call `app_get` first and pass an accessible system `view_id`, for example `--view-id system:all`.",
|
|
472
|
+
)
|
|
473
|
+
if not (args.view_id or "").strip():
|
|
474
|
+
raise_config_error(
|
|
475
|
+
"record delete requires --view-id.",
|
|
476
|
+
fix_hint="Use a system view_id from app get accessible_views, for example `--view-id system:all`; custom views are not supported for deletion.",
|
|
477
|
+
)
|
|
408
478
|
record_ids = load_list_arg(args.record_ids_file, option_name="--record-ids-file")
|
|
409
479
|
return context.record.record_delete_public(
|
|
410
480
|
profile=args.profile,
|
|
411
481
|
app_key=args.app_key,
|
|
412
482
|
record_id=args.record_id,
|
|
413
483
|
record_ids=record_ids,
|
|
484
|
+
view_id=args.view_id,
|
|
414
485
|
)
|
|
415
486
|
|
|
416
487
|
|
|
@@ -446,6 +517,7 @@ def _handle_code_block_run(args: argparse.Namespace, context: CliContext) -> dic
|
|
|
446
517
|
app_key=args.app_key,
|
|
447
518
|
record_id=args.record_id,
|
|
448
519
|
code_block_field=args.code_block_field,
|
|
520
|
+
view_id=args.view_id,
|
|
449
521
|
role=args.role,
|
|
450
522
|
workflow_node_id=args.workflow_node_id,
|
|
451
523
|
answers=load_list_arg(args.answers_file, option_name="--answers-file"),
|
|
@@ -13,7 +13,6 @@ from ..tools.feedback_tools import FeedbackTools
|
|
|
13
13
|
from ..tools.file_tools import FileTools
|
|
14
14
|
from ..tools.import_tools import ImportTools
|
|
15
15
|
from ..tools.record_tools import RecordTools
|
|
16
|
-
from ..tools.repository_dev_tools import RepositoryDevTools
|
|
17
16
|
from ..tools.resource_read_tools import ResourceReadTools
|
|
18
17
|
from ..tools.task_context_tools import TaskContextTools
|
|
19
18
|
from ..tools.workspace_tools import WorkspaceTools
|
|
@@ -35,7 +34,6 @@ class CliContext:
|
|
|
35
34
|
files: FileTools
|
|
36
35
|
builder_feedback: FeedbackTools
|
|
37
36
|
builder: AiBuilderTools
|
|
38
|
-
repo: RepositoryDevTools
|
|
39
37
|
|
|
40
38
|
def close(self) -> None:
|
|
41
39
|
self.backend.close()
|
|
@@ -59,5 +57,4 @@ def build_cli_context() -> CliContext:
|
|
|
59
57
|
files=FileTools(sessions, backend),
|
|
60
58
|
builder_feedback=FeedbackTools(backend, mcp_side="App Builder MCP"),
|
|
61
59
|
builder=AiBuilderTools(sessions, backend),
|
|
62
|
-
repo=RepositoryDevTools(sessions, backend),
|
|
63
60
|
)
|
|
@@ -8,8 +8,11 @@ from typing import Any, TextIO
|
|
|
8
8
|
def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> None:
|
|
9
9
|
text = _format_cancelled_result(result)
|
|
10
10
|
if text is None:
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
if _is_failed_result(result) and hint != "task_action_execute":
|
|
12
|
+
text = _format_failed_result(result)
|
|
13
|
+
else:
|
|
14
|
+
formatter = _FORMATTERS.get(hint, _format_generic)
|
|
15
|
+
text = formatter(result)
|
|
13
16
|
stream.write(text)
|
|
14
17
|
if not text.endswith("\n"):
|
|
15
18
|
stream.write("\n")
|
|
@@ -21,6 +24,50 @@ def _format_cancelled_result(result: dict[str, Any]) -> str | None:
|
|
|
21
24
|
return str(result.get("message") or "已取消") + "\n"
|
|
22
25
|
|
|
23
26
|
|
|
27
|
+
def _is_failed_result(result: dict[str, Any]) -> bool:
|
|
28
|
+
if _is_executed_nonfatal_result(result):
|
|
29
|
+
return False
|
|
30
|
+
if result.get("ok") is False:
|
|
31
|
+
return True
|
|
32
|
+
return str(result.get("status") or "").lower() in {"failed", "blocked"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_executed_nonfatal_result(result: dict[str, Any]) -> bool:
|
|
36
|
+
status = str(result.get("status") or "").lower()
|
|
37
|
+
executed = bool(
|
|
38
|
+
result.get("write_executed")
|
|
39
|
+
or result.get("delete_executed")
|
|
40
|
+
or result.get("action_executed")
|
|
41
|
+
or result.get("export_executed")
|
|
42
|
+
)
|
|
43
|
+
return executed and status in {"partial_success", "verification_failed", "running", "queued", "unknown"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _format_failed_result(result: dict[str, Any]) -> str:
|
|
47
|
+
lines = [f"Status: {result.get('status') or 'failed'}"]
|
|
48
|
+
for key, label in (
|
|
49
|
+
("error_code", "Error Code"),
|
|
50
|
+
("message", "Message"),
|
|
51
|
+
("backend_code", "Backend Code"),
|
|
52
|
+
("request_id", "Request ID"),
|
|
53
|
+
("http_status", "HTTP Status"),
|
|
54
|
+
("category", "Category"),
|
|
55
|
+
):
|
|
56
|
+
value = result.get(key)
|
|
57
|
+
if value not in (None, ""):
|
|
58
|
+
lines.append(f"{label}: {value}")
|
|
59
|
+
|
|
60
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
61
|
+
selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
|
|
62
|
+
if selection:
|
|
63
|
+
lines.append("Selection:")
|
|
64
|
+
lines.extend(f"- {line}" for line in _dict_scalar_lines(selection))
|
|
65
|
+
|
|
66
|
+
_append_warnings(lines, result.get("warnings"))
|
|
67
|
+
_append_verification(lines, result.get("verification"))
|
|
68
|
+
return "\n".join(lines) + "\n"
|
|
69
|
+
|
|
70
|
+
|
|
24
71
|
def _format_generic(result: dict[str, Any]) -> str:
|
|
25
72
|
lines: list[str] = []
|
|
26
73
|
title = _first_present(result, "status", "message")
|
|
@@ -323,6 +370,116 @@ def _format_record_logs(result: dict[str, Any]) -> str:
|
|
|
323
370
|
return "\n".join(lines) + "\n"
|
|
324
371
|
|
|
325
372
|
|
|
373
|
+
def _format_record_write(result: dict[str, Any]) -> str:
|
|
374
|
+
status = str(result.get("status") or "").strip().lower()
|
|
375
|
+
write_executed = bool(result.get("write_executed"))
|
|
376
|
+
verification_status = str(result.get("verification_status") or "").strip().lower()
|
|
377
|
+
if write_executed and verification_status == "failed":
|
|
378
|
+
title = "写入已提交(回读验证未完成)"
|
|
379
|
+
elif write_executed and status in {"success", "completed"}:
|
|
380
|
+
title = "写入成功"
|
|
381
|
+
elif status:
|
|
382
|
+
title = status
|
|
383
|
+
else:
|
|
384
|
+
title = "写入结果"
|
|
385
|
+
|
|
386
|
+
lines = [title]
|
|
387
|
+
if result.get("record_id") not in (None, ""):
|
|
388
|
+
lines.append(f"Record ID: {result.get('record_id')}")
|
|
389
|
+
if result.get("apply_id") not in (None, "") and result.get("apply_id") != result.get("record_id"):
|
|
390
|
+
lines.append(f"Apply ID: {result.get('apply_id')}")
|
|
391
|
+
|
|
392
|
+
update_route = result.get("update_route") if isinstance(result.get("update_route"), dict) else {}
|
|
393
|
+
route_name = update_route.get("route") or update_route.get("type") or update_route.get("label")
|
|
394
|
+
if route_name:
|
|
395
|
+
lines.append(f"Update Route: {route_name}")
|
|
396
|
+
|
|
397
|
+
if verification_status:
|
|
398
|
+
lines.append(f"Verification: {verification_status}")
|
|
399
|
+
if write_executed:
|
|
400
|
+
lines.append("Safe To Retry: false")
|
|
401
|
+
|
|
402
|
+
if write_executed and verification_status == "failed":
|
|
403
|
+
verification = result.get("data", {}).get("verification") if isinstance(result.get("data"), dict) else None
|
|
404
|
+
if isinstance(verification, dict):
|
|
405
|
+
warning_codes = [
|
|
406
|
+
str(item.get("code"))
|
|
407
|
+
for item in verification.get("warnings", [])
|
|
408
|
+
if isinstance(item, dict) and item.get("code")
|
|
409
|
+
]
|
|
410
|
+
if warning_codes:
|
|
411
|
+
lines.append("Verification Note: " + ", ".join(warning_codes[:3]))
|
|
412
|
+
lines.append("说明:写请求已执行;当前结果只表示后置回读未能确认字段值,不等同于写入被拒绝。")
|
|
413
|
+
|
|
414
|
+
_append_warnings(lines, result.get("warnings"))
|
|
415
|
+
return "\n".join(lines) + "\n"
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _format_record_delete(result: dict[str, Any]) -> str:
|
|
419
|
+
status = str(result.get("status") or "").strip().lower()
|
|
420
|
+
deleted_ids = [str(item) for item in result.get("deleted_ids", []) if item not in (None, "")]
|
|
421
|
+
failed_ids = [str(item) for item in result.get("failed_ids", []) if item not in (None, "")]
|
|
422
|
+
write_executed = bool(result.get("write_executed"))
|
|
423
|
+
|
|
424
|
+
if deleted_ids and failed_ids:
|
|
425
|
+
title = "删除部分完成"
|
|
426
|
+
elif deleted_ids:
|
|
427
|
+
title = "删除完成"
|
|
428
|
+
elif status == "failed" or result.get("ok") is False:
|
|
429
|
+
title = "删除未执行"
|
|
430
|
+
else:
|
|
431
|
+
title = "删除结果"
|
|
432
|
+
|
|
433
|
+
lines = [title]
|
|
434
|
+
lines.append(f"Deleted: {len(deleted_ids)}")
|
|
435
|
+
if deleted_ids:
|
|
436
|
+
lines.append("Deleted IDs: " + ", ".join(deleted_ids[:20]))
|
|
437
|
+
lines.append(f"Failed: {len(failed_ids)}")
|
|
438
|
+
if failed_ids:
|
|
439
|
+
lines.append("Failed IDs: " + ", ".join(failed_ids[:20]))
|
|
440
|
+
if write_executed:
|
|
441
|
+
lines.append("Safe To Retry: false")
|
|
442
|
+
elif result.get("safe_to_retry") is not None:
|
|
443
|
+
lines.append(f"Safe To Retry: {str(bool(result.get('safe_to_retry'))).lower()}")
|
|
444
|
+
_append_warnings(lines, result.get("warnings"))
|
|
445
|
+
return "\n".join(lines) + "\n"
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _format_code_block_run(result: dict[str, Any]) -> str:
|
|
449
|
+
status = str(result.get("status") or "").strip().lower()
|
|
450
|
+
execution = result.get("execution") if isinstance(result.get("execution"), dict) else {}
|
|
451
|
+
writeback = result.get("writeback") if isinstance(result.get("writeback"), dict) else {}
|
|
452
|
+
executed = bool(execution.get("executed"))
|
|
453
|
+
writeback_applied = bool(writeback.get("applied"))
|
|
454
|
+
write_verified = writeback.get("write_verified")
|
|
455
|
+
|
|
456
|
+
if executed and writeback_applied and status == "verification_failed":
|
|
457
|
+
title = "代码块已执行,回写已提交(验证未完成)"
|
|
458
|
+
elif executed and writeback_applied:
|
|
459
|
+
title = "代码块已执行,回写成功"
|
|
460
|
+
elif executed:
|
|
461
|
+
title = "代码块已执行"
|
|
462
|
+
elif status:
|
|
463
|
+
title = status
|
|
464
|
+
else:
|
|
465
|
+
title = "代码块结果"
|
|
466
|
+
|
|
467
|
+
lines = [title]
|
|
468
|
+
if result.get("record_id") not in (None, ""):
|
|
469
|
+
lines.append(f"Record ID: {result.get('record_id')}")
|
|
470
|
+
code_block_field = result.get("code_block_field") if isinstance(result.get("code_block_field"), dict) else {}
|
|
471
|
+
if code_block_field.get("title"):
|
|
472
|
+
lines.append(f"Code Block Field: {code_block_field.get('title')}")
|
|
473
|
+
if execution:
|
|
474
|
+
lines.append(f"Result Count: {execution.get('result_count', 0)}")
|
|
475
|
+
if writeback:
|
|
476
|
+
lines.append(f"Writeback: attempted={writeback.get('attempted')} applied={writeback_applied} verified={write_verified}")
|
|
477
|
+
if executed and writeback_applied and status == "verification_failed":
|
|
478
|
+
lines.append("说明:代码块执行和回写请求已完成;当前结果只表示后置回读未能确认字段值,不等同于回写被拒绝。")
|
|
479
|
+
_append_warnings(lines, result.get("warnings"))
|
|
480
|
+
return "\n".join(lines) + "\n"
|
|
481
|
+
|
|
482
|
+
|
|
326
483
|
def _format_task_list(result: dict[str, Any]) -> str:
|
|
327
484
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
328
485
|
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
@@ -525,6 +682,42 @@ def _format_import_verify(result: dict[str, Any]) -> str:
|
|
|
525
682
|
return "\n".join(lines) + "\n"
|
|
526
683
|
|
|
527
684
|
|
|
685
|
+
def _format_import_template(result: dict[str, Any]) -> str:
|
|
686
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
687
|
+
template_source = verification.get("template_source")
|
|
688
|
+
if not template_source:
|
|
689
|
+
template_source = "official" if result.get("template_url") else ("local_generated" if result.get("downloaded_to_path") else "unknown")
|
|
690
|
+
if result.get("downloaded_to_path"):
|
|
691
|
+
title = "导入模板已生成" if template_source == "local_generated" else "导入模板已下载"
|
|
692
|
+
elif result.get("template_url"):
|
|
693
|
+
title = "导入模板已获取"
|
|
694
|
+
else:
|
|
695
|
+
title = "导入模板结果"
|
|
696
|
+
|
|
697
|
+
lines = [
|
|
698
|
+
title,
|
|
699
|
+
f"App Key: {result.get('app_key') or '-'}",
|
|
700
|
+
f"Template Source: {template_source}",
|
|
701
|
+
]
|
|
702
|
+
if result.get("downloaded_to_path"):
|
|
703
|
+
lines.append(f"Local Path: {result.get('downloaded_to_path')}")
|
|
704
|
+
if result.get("template_url"):
|
|
705
|
+
lines.append(f"Template URL: {result.get('template_url')}")
|
|
706
|
+
import_capability = result.get("import_capability") if isinstance(result.get("import_capability"), dict) else {}
|
|
707
|
+
if import_capability:
|
|
708
|
+
lines.append(
|
|
709
|
+
"Import Capability: "
|
|
710
|
+
f"{import_capability.get('auth_source') or 'unknown'} / "
|
|
711
|
+
f"can_import={import_capability.get('can_import')}"
|
|
712
|
+
)
|
|
713
|
+
expected_columns = result.get("expected_columns") if isinstance(result.get("expected_columns"), list) else []
|
|
714
|
+
if expected_columns:
|
|
715
|
+
lines.append(f"Columns: {len(expected_columns)}")
|
|
716
|
+
_append_warnings(lines, result.get("warnings"))
|
|
717
|
+
_append_verification(lines, result.get("verification"))
|
|
718
|
+
return "\n".join(lines) + "\n"
|
|
719
|
+
|
|
720
|
+
|
|
528
721
|
def _format_import_status(result: dict[str, Any]) -> str:
|
|
529
722
|
lines = [
|
|
530
723
|
f"Status: {result.get('status') or '-'}",
|
|
@@ -721,21 +914,23 @@ def _task_action_failure_label(action: str) -> str:
|
|
|
721
914
|
|
|
722
915
|
|
|
723
916
|
def _task_action_partial_success_message(result: dict[str, Any]) -> str:
|
|
724
|
-
error_code = str(result.get("error_code") or "").strip().upper()
|
|
725
|
-
if error_code == "WORKFLOW_CONTINUATION_UNVERIFIED":
|
|
726
|
-
return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
|
|
727
|
-
if error_code == "TASK_ALREADY_PROCESSED":
|
|
728
|
-
return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
|
|
729
917
|
warnings = result.get("warnings")
|
|
730
918
|
if isinstance(warnings, list):
|
|
731
919
|
for warning in warnings:
|
|
732
920
|
if not isinstance(warning, dict):
|
|
733
921
|
continue
|
|
734
922
|
code = str(warning.get("code") or "").strip().upper()
|
|
923
|
+
if code == "TASK_ACTION_VERIFICATION_PERMISSION_UNAVAILABLE":
|
|
924
|
+
return "动作已提交;后置验证读取受当前权限限制,不能据此判断动作被拒绝。可使用 --json 查看详细信息。"
|
|
735
925
|
if code == "TASK_ALREADY_PROCESSED_UNCONFIRMED_ACTOR":
|
|
736
926
|
return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
|
|
737
927
|
if code == "WORKFLOW_CONTINUATION_UNVERIFIED":
|
|
738
928
|
return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
|
|
929
|
+
error_code = str(result.get("error_code") or "").strip().upper()
|
|
930
|
+
if error_code == "WORKFLOW_CONTINUATION_UNVERIFIED":
|
|
931
|
+
return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
|
|
932
|
+
if error_code == "TASK_ALREADY_PROCESSED":
|
|
933
|
+
return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
|
|
739
934
|
return "动作已提交,但结果验证不完整。可使用 --json 查看详细信息。"
|
|
740
935
|
|
|
741
936
|
|
|
@@ -824,11 +1019,15 @@ _FORMATTERS = {
|
|
|
824
1019
|
"record_access": _format_record_access,
|
|
825
1020
|
"record_get": _format_record_get,
|
|
826
1021
|
"record_logs": _format_record_logs,
|
|
1022
|
+
"record_write": _format_record_write,
|
|
1023
|
+
"record_delete": _format_record_delete,
|
|
1024
|
+
"code_block_run": _format_code_block_run,
|
|
827
1025
|
"task_list": _format_task_list,
|
|
828
1026
|
"task_workbench": _format_task_workbench,
|
|
829
1027
|
"task_get": _format_task_get,
|
|
830
1028
|
"task_action_execute": _format_task_action,
|
|
831
1029
|
"task_associated_report_detail_get": _format_task_associated_report_detail,
|
|
1030
|
+
"import_template": _format_import_template,
|
|
832
1031
|
"import_verify": _format_import_verify,
|
|
833
1032
|
"import_status": _format_import_status,
|
|
834
1033
|
"export_start": _format_export_start,
|