@qingflow-tech/qingflow-app-user-mcp 1.0.11 → 1.0.12
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 +9 -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 +255 -0
- package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder/references/create-app.md +149 -0
- package/skills/qingflow-app-builder/references/environments.md +63 -0
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +107 -0
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
- package/skills/qingflow-app-builder/references/update-flow.md +158 -0
- package/skills/qingflow-app-builder/references/update-layout.md +68 -0
- package/skills/qingflow-app-builder/references/update-schema.md +72 -0
- package/skills/qingflow-app-builder/references/update-views.md +284 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/skills/qingflow-app-user/SKILL.md +12 -11
- package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
- package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
- package/skills/qingflow-app-user/references/record-patterns.md +5 -5
- package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
- package/skills/qingflow-mcp-setup/SKILL.md +113 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-record-analysis/SKILL.md +6 -7
- package/skills/qingflow-record-analysis/manifest.yaml +10 -0
- package/skills/qingflow-record-delete/SKILL.md +5 -3
- package/skills/qingflow-record-import/SKILL.md +6 -2
- package/skills/qingflow-record-insert/SKILL.md +48 -4
- package/skills/qingflow-record-insert/manifest.yaml +6 -0
- package/skills/qingflow-record-update/SKILL.md +36 -24
- package/skills/qingflow-task-ops/SKILL.md +25 -25
- package/skills/qingflow-task-ops/references/environments.md +0 -1
- package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
- package/src/qingflow_mcp/__main__.py +6 -2
- 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 +39 -11
- 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 +68 -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 +15 -13
- 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 +296 -39
- 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 +230 -33
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +293 -40
- 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 +1042 -338
- 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="读取导入表结构")
|
|
@@ -105,7 +114,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
105
114
|
logs = record_subparsers.add_parser("logs", help="读取单条记录全量日志并写入本地 JSONL")
|
|
106
115
|
logs.add_argument("--app-key", required=True)
|
|
107
116
|
logs.add_argument("--record-id", required=True)
|
|
108
|
-
logs.add_argument("--view-id")
|
|
117
|
+
logs.add_argument("--view-id", required=True)
|
|
109
118
|
logs.set_defaults(handler=_handle_logs, format_hint="record_logs")
|
|
110
119
|
|
|
111
120
|
insert = record_subparsers.add_parser("insert", help="新增记录")
|
|
@@ -113,22 +122,25 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
113
122
|
insert.add_argument("--fields-file", help=argparse.SUPPRESS)
|
|
114
123
|
insert.add_argument("--items-file")
|
|
115
124
|
insert.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
|
|
116
|
-
insert.set_defaults(handler=_handle_insert, format_hint="")
|
|
125
|
+
insert.set_defaults(handler=_handle_insert, format_hint="record_write")
|
|
117
126
|
|
|
118
127
|
update = record_subparsers.add_parser("update", help="更新记录")
|
|
119
128
|
update.add_argument("--app-key", required=True)
|
|
120
129
|
update.add_argument("--record-id")
|
|
121
130
|
update.add_argument("--fields-file")
|
|
122
131
|
update.add_argument("--items-file")
|
|
132
|
+
update.add_argument("--view-id")
|
|
123
133
|
update.add_argument("--dry-run", action=argparse.BooleanOptionalAction, default=False)
|
|
124
134
|
update.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
|
|
125
|
-
update.set_defaults(handler=_handle_update, format_hint="")
|
|
135
|
+
update.set_defaults(handler=_handle_update, format_hint="record_write")
|
|
126
136
|
|
|
127
137
|
delete = record_subparsers.add_parser("delete", help="删除记录")
|
|
128
138
|
delete.add_argument("--app-key", required=True)
|
|
129
139
|
delete.add_argument("--record-id")
|
|
130
140
|
delete.add_argument("--record-ids-file")
|
|
131
|
-
delete.
|
|
141
|
+
delete.add_argument("--view-id")
|
|
142
|
+
delete.add_argument("--list-type", dest="legacy_list_type", type=int, help=argparse.SUPPRESS)
|
|
143
|
+
delete.set_defaults(handler=_handle_delete, format_hint="record_delete")
|
|
132
144
|
|
|
133
145
|
analyze = record_subparsers.add_parser("analyze", help=argparse.SUPPRESS)
|
|
134
146
|
record_subparsers._choices_actions = [ # type: ignore[attr-defined]
|
|
@@ -153,6 +165,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
153
165
|
code_block.add_argument("--app-key", required=True)
|
|
154
166
|
code_block.add_argument("--record-id", required=True)
|
|
155
167
|
code_block.add_argument("--code-block-field", required=True)
|
|
168
|
+
code_block.add_argument("--view-id")
|
|
156
169
|
code_block.add_argument("--role", type=int, default=1)
|
|
157
170
|
code_block.add_argument("--workflow-node-id", type=int)
|
|
158
171
|
code_block.add_argument("--answers-file")
|
|
@@ -161,7 +174,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
161
174
|
code_block.add_argument("--apply-writeback", action=argparse.BooleanOptionalAction, default=True)
|
|
162
175
|
code_block.add_argument("--verify-writeback", action=argparse.BooleanOptionalAction, default=True)
|
|
163
176
|
code_block.add_argument("--force-refresh-form", action="store_true")
|
|
164
|
-
code_block.set_defaults(handler=_handle_code_block_run, format_hint="")
|
|
177
|
+
code_block.set_defaults(handler=_handle_code_block_run, format_hint="code_block_run")
|
|
165
178
|
|
|
166
179
|
|
|
167
180
|
def _columns(args: argparse.Namespace) -> list[Any]:
|
|
@@ -182,20 +195,20 @@ def _handle_schema_root(args: argparse.Namespace, _context: CliContext) -> dict:
|
|
|
182
195
|
mode = (args.legacy_mode or "").strip()
|
|
183
196
|
if mode:
|
|
184
197
|
replacement = {
|
|
185
|
-
"applicant": "record schema
|
|
198
|
+
"applicant": "record schema insert --app-key APP_KEY",
|
|
186
199
|
"browse": "record schema browse --app-key APP_KEY --view-id VIEW_ID",
|
|
187
200
|
"insert": "record schema insert --app-key APP_KEY",
|
|
188
|
-
"update": "record schema update --app-key APP_KEY --record-id RECORD_ID",
|
|
201
|
+
"update": "record schema update --app-key APP_KEY --record-id RECORD_ID [--view-id VIEW_ID]",
|
|
189
202
|
"import": "record schema import --app-key APP_KEY",
|
|
190
203
|
"code-block": "record schema code-block --app-key APP_KEY",
|
|
191
|
-
}.get(mode, "record schema <
|
|
204
|
+
}.get(mode, "record schema <browse|insert|update|import|code-block> ...")
|
|
192
205
|
raise_config_error(
|
|
193
206
|
"record schema --mode is no longer accepted.",
|
|
194
207
|
fix_hint=f"Use `{replacement}` instead.",
|
|
195
208
|
)
|
|
196
209
|
raise_config_error(
|
|
197
210
|
"record schema requires an explicit subcommand.",
|
|
198
|
-
fix_hint="Use one of: `record schema
|
|
211
|
+
fix_hint="Use one of: `record schema browse`, `record schema insert`, `record schema update`, `record schema import`, or `record schema code-block`.",
|
|
199
212
|
)
|
|
200
213
|
|
|
201
214
|
|
|
@@ -224,6 +237,7 @@ def _handle_schema_update(args: argparse.Namespace, context: CliContext) -> dict
|
|
|
224
237
|
profile=args.profile,
|
|
225
238
|
app_key=args.app_key,
|
|
226
239
|
record_id=args.record_id,
|
|
240
|
+
view_id=args.view_id,
|
|
227
241
|
)
|
|
228
242
|
|
|
229
243
|
|
|
@@ -382,6 +396,7 @@ def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
382
396
|
record_id=None,
|
|
383
397
|
fields=None,
|
|
384
398
|
items=require_list_arg(args.items_file, option_name="--items-file"),
|
|
399
|
+
view_id=args.view_id,
|
|
385
400
|
dry_run=bool(args.dry_run),
|
|
386
401
|
verify_write=bool(args.verify_write),
|
|
387
402
|
)
|
|
@@ -400,17 +415,29 @@ def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
400
415
|
app_key=args.app_key,
|
|
401
416
|
record_id=args.record_id,
|
|
402
417
|
fields=require_object_arg(args.fields_file, option_name="--fields-file"),
|
|
418
|
+
view_id=args.view_id,
|
|
403
419
|
verify_write=bool(args.verify_write),
|
|
404
420
|
)
|
|
405
421
|
|
|
406
422
|
|
|
407
423
|
def _handle_delete(args: argparse.Namespace, context: CliContext) -> dict:
|
|
424
|
+
if args.legacy_list_type is not None:
|
|
425
|
+
raise_config_error(
|
|
426
|
+
"record delete no longer accepts list_type.",
|
|
427
|
+
fix_hint="Call `app_get` first and pass an accessible system `view_id`, for example `--view-id system:all`.",
|
|
428
|
+
)
|
|
429
|
+
if not (args.view_id or "").strip():
|
|
430
|
+
raise_config_error(
|
|
431
|
+
"record delete requires --view-id.",
|
|
432
|
+
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.",
|
|
433
|
+
)
|
|
408
434
|
record_ids = load_list_arg(args.record_ids_file, option_name="--record-ids-file")
|
|
409
435
|
return context.record.record_delete_public(
|
|
410
436
|
profile=args.profile,
|
|
411
437
|
app_key=args.app_key,
|
|
412
438
|
record_id=args.record_id,
|
|
413
439
|
record_ids=record_ids,
|
|
440
|
+
view_id=args.view_id,
|
|
414
441
|
)
|
|
415
442
|
|
|
416
443
|
|
|
@@ -446,6 +473,7 @@ def _handle_code_block_run(args: argparse.Namespace, context: CliContext) -> dic
|
|
|
446
473
|
app_key=args.app_key,
|
|
447
474
|
record_id=args.record_id,
|
|
448
475
|
code_block_field=args.code_block_field,
|
|
476
|
+
view_id=args.view_id,
|
|
449
477
|
role=args.role,
|
|
450
478
|
workflow_node_id=args.workflow_node_id,
|
|
451
479
|
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,
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import sys
|
|
6
6
|
from typing import Any, Callable, TextIO
|
|
7
7
|
|
|
8
|
-
from ..errors import QingflowApiError
|
|
8
|
+
from ..errors import QingflowApiError, backend_code_value_int, message_looks_like_invalid_token
|
|
9
9
|
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
10
10
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
11
11
|
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
@@ -323,6 +323,8 @@ def _emit_error(payload: dict[str, Any], *, json_mode: bool, stdout: TextIO, std
|
|
|
323
323
|
f"Category: {payload.get('category') or 'error'}",
|
|
324
324
|
f"Message: {payload.get('message') or 'Unknown error'}",
|
|
325
325
|
]
|
|
326
|
+
if payload.get("error_code"):
|
|
327
|
+
lines.append(f"Error Code: {payload.get('error_code')}")
|
|
326
328
|
if payload.get("backend_code") is not None:
|
|
327
329
|
lines.append(f"Backend Code: {payload.get('backend_code')}")
|
|
328
330
|
if payload.get("request_id"):
|
|
@@ -337,23 +339,65 @@ def _emit_error(payload: dict[str, Any], *, json_mode: bool, stdout: TextIO, std
|
|
|
337
339
|
|
|
338
340
|
|
|
339
341
|
def _error_exit_code(payload: dict[str, Any]) -> int:
|
|
340
|
-
|
|
341
|
-
if category in {"auth", "workspace"}:
|
|
342
|
+
if _is_auth_or_workspace_payload(payload):
|
|
342
343
|
return 3
|
|
343
344
|
return 4
|
|
344
345
|
|
|
345
346
|
|
|
347
|
+
def _is_auth_or_workspace_payload(payload: dict[str, Any]) -> bool:
|
|
348
|
+
category = str(payload.get("category") or "").lower()
|
|
349
|
+
error_code = str(payload.get("error_code") or "").upper()
|
|
350
|
+
http_status = backend_code_value_int(payload.get("http_status"))
|
|
351
|
+
if category in {"auth", "workspace"} or error_code in {"AUTH_REQUIRED", "WORKSPACE_NOT_SELECTED"}:
|
|
352
|
+
return True
|
|
353
|
+
if http_status == 401 or message_looks_like_invalid_token(payload.get("message")):
|
|
354
|
+
return True
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
|
|
346
358
|
def _result_exit_code(result: dict[str, Any]) -> int:
|
|
347
359
|
if not isinstance(result, dict):
|
|
348
360
|
return 0
|
|
361
|
+
if _is_executed_nonfatal_result(result):
|
|
362
|
+
return 0
|
|
363
|
+
if _is_auth_or_workspace_payload(result):
|
|
364
|
+
return 3
|
|
349
365
|
if result.get("ok") is False:
|
|
350
366
|
return 4
|
|
351
367
|
status = str(result.get("status") or "").lower()
|
|
368
|
+
if status == "partial_success" and result.get("ok") is not True and not _has_readback_unavailable_verification(result):
|
|
369
|
+
return 4
|
|
352
370
|
if status in {"failed", "blocked"}:
|
|
353
371
|
return 4
|
|
354
372
|
return 0
|
|
355
373
|
|
|
356
374
|
|
|
375
|
+
def _is_executed_nonfatal_result(result: dict[str, Any]) -> bool:
|
|
376
|
+
status = str(result.get("status") or "").lower()
|
|
377
|
+
executed = bool(
|
|
378
|
+
result.get("write_executed")
|
|
379
|
+
or result.get("delete_executed")
|
|
380
|
+
or result.get("action_executed")
|
|
381
|
+
or result.get("export_executed")
|
|
382
|
+
)
|
|
383
|
+
return executed and status in {"partial_success", "verification_failed", "running", "queued", "unknown"}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _has_readback_unavailable_verification(result: dict[str, Any]) -> bool:
|
|
387
|
+
verification = result.get("verification")
|
|
388
|
+
if not isinstance(verification, dict):
|
|
389
|
+
return False
|
|
390
|
+
return any(
|
|
391
|
+
bool(verification.get(key))
|
|
392
|
+
for key in (
|
|
393
|
+
"readback_unavailable",
|
|
394
|
+
"readback_pending",
|
|
395
|
+
"metadata_unverified",
|
|
396
|
+
"views_read_unavailable",
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
357
401
|
def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliContext, *, stream: TextIO) -> None:
|
|
358
402
|
spec = cli_public_tool_spec_from_namespace(args)
|
|
359
403
|
if spec is None or not spec.cli_show_effective_context:
|
|
@@ -9,11 +9,24 @@ from .json_types import JSONObject, JSONScalar
|
|
|
9
9
|
INVALID_TOKEN_MARKERS = (
|
|
10
10
|
"invalid token",
|
|
11
11
|
"token invalid",
|
|
12
|
+
"token expired",
|
|
13
|
+
"expired token",
|
|
12
14
|
"token失效",
|
|
13
15
|
"无效token",
|
|
14
16
|
"登录失效",
|
|
17
|
+
"登录过期",
|
|
18
|
+
"会话过期",
|
|
19
|
+
"session expired",
|
|
20
|
+
"session invalid",
|
|
15
21
|
"login token invalid",
|
|
16
22
|
"access token invalid",
|
|
23
|
+
"not logged in",
|
|
24
|
+
"not login",
|
|
25
|
+
"please login",
|
|
26
|
+
"please log in",
|
|
27
|
+
"未登录",
|
|
28
|
+
"请登录",
|
|
29
|
+
"重新登录",
|
|
17
30
|
)
|
|
18
31
|
|
|
19
32
|
|
|
@@ -36,8 +49,7 @@ class QingflowApiError(Exception):
|
|
|
36
49
|
return self.as_json()
|
|
37
50
|
|
|
38
51
|
def looks_like_invalid_token(self) -> bool:
|
|
39
|
-
|
|
40
|
-
return any(marker in text for marker in INVALID_TOKEN_MARKERS)
|
|
52
|
+
return message_looks_like_invalid_token(self.message)
|
|
41
53
|
|
|
42
54
|
@classmethod
|
|
43
55
|
def auth_required(cls, profile: str) -> "QingflowApiError":
|
|
@@ -64,3 +76,32 @@ class QingflowApiError(Exception):
|
|
|
64
76
|
|
|
65
77
|
def raise_tool_error(error: QingflowApiError) -> None:
|
|
66
78
|
raise RuntimeError(error.as_json())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def backend_code_value_int(code: JSONScalar) -> int | None:
|
|
82
|
+
if isinstance(code, bool) or code is None:
|
|
83
|
+
return None
|
|
84
|
+
if isinstance(code, int):
|
|
85
|
+
return code
|
|
86
|
+
if isinstance(code, str):
|
|
87
|
+
text = code.strip()
|
|
88
|
+
if text:
|
|
89
|
+
try:
|
|
90
|
+
return int(text)
|
|
91
|
+
except ValueError:
|
|
92
|
+
return None
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def backend_code_int(error: QingflowApiError) -> int | None:
|
|
97
|
+
return backend_code_value_int(error.backend_code)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def message_looks_like_invalid_token(message: object) -> bool:
|
|
101
|
+
text = str(message or "").lower()
|
|
102
|
+
return any(marker in text for marker in INVALID_TOKEN_MARKERS)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_auth_like_error(error: QingflowApiError) -> bool:
|
|
106
|
+
category = str(error.category or "").strip().lower()
|
|
107
|
+
return category == "auth" or error.http_status == 401 or error.looks_like_invalid_token()
|