@qingflow-tech/qingflow-app-user-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.
Files changed (88) hide show
  1. package/README.md +9 -3
  2. package/docs/local-agent-install.md +54 -3
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/lib/runtime.mjs +304 -13
  6. package/npm/scripts/postinstall.mjs +1 -5
  7. package/package.json +3 -2
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +255 -0
  10. package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
  11. package/skills/qingflow-app-builder/references/create-app.md +149 -0
  12. package/skills/qingflow-app-builder/references/environments.md +63 -0
  13. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  14. package/skills/qingflow-app-builder/references/gotchas.md +107 -0
  15. package/skills/qingflow-app-builder/references/match-rules.md +114 -0
  16. package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
  17. package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
  18. package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
  19. package/skills/qingflow-app-builder/references/update-flow.md +158 -0
  20. package/skills/qingflow-app-builder/references/update-layout.md +68 -0
  21. package/skills/qingflow-app-builder/references/update-schema.md +72 -0
  22. package/skills/qingflow-app-builder/references/update-views.md +284 -0
  23. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  24. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  25. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  26. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  27. package/skills/qingflow-app-user/SKILL.md +12 -11
  28. package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
  29. package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
  30. package/skills/qingflow-app-user/references/record-patterns.md +5 -5
  31. package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
  32. package/skills/qingflow-mcp-setup/SKILL.md +113 -0
  33. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  34. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  35. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  36. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  37. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  38. package/skills/qingflow-record-analysis/SKILL.md +6 -7
  39. package/skills/qingflow-record-analysis/manifest.yaml +10 -0
  40. package/skills/qingflow-record-delete/SKILL.md +5 -3
  41. package/skills/qingflow-record-import/SKILL.md +6 -2
  42. package/skills/qingflow-record-insert/SKILL.md +48 -4
  43. package/skills/qingflow-record-insert/manifest.yaml +6 -0
  44. package/skills/qingflow-record-update/SKILL.md +36 -24
  45. package/skills/qingflow-task-ops/SKILL.md +25 -25
  46. package/skills/qingflow-task-ops/references/environments.md +0 -1
  47. package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
  48. package/src/qingflow_mcp/__main__.py +6 -2
  49. package/src/qingflow_mcp/builder_facade/models.py +11 -0
  50. package/src/qingflow_mcp/builder_facade/service.py +1488 -288
  51. package/src/qingflow_mcp/cli/commands/builder.py +2 -2
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +1 -1
  54. package/src/qingflow_mcp/cli/commands/record.py +91 -19
  55. package/src/qingflow_mcp/cli/context.py +0 -3
  56. package/src/qingflow_mcp/cli/formatters.py +206 -7
  57. package/src/qingflow_mcp/cli/main.py +47 -3
  58. package/src/qingflow_mcp/errors.py +43 -2
  59. package/src/qingflow_mcp/public_surface.py +21 -15
  60. package/src/qingflow_mcp/response_trim.py +74 -13
  61. package/src/qingflow_mcp/server.py +11 -9
  62. package/src/qingflow_mcp/server_app_builder.py +3 -2
  63. package/src/qingflow_mcp/server_app_user.py +19 -13
  64. package/src/qingflow_mcp/session_store.py +11 -7
  65. package/src/qingflow_mcp/solution/executor.py +112 -15
  66. package/src/qingflow_mcp/tools/ai_builder_tools.py +36 -11
  67. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  68. package/src/qingflow_mcp/tools/approval_tools.py +196 -34
  69. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  70. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  71. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  72. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  73. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  74. package/src/qingflow_mcp/tools/file_tools.py +7 -3
  75. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  76. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  77. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  78. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  79. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  80. package/src/qingflow_mcp/tools/record_tools.py +1067 -349
  81. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  82. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  83. package/src/qingflow_mcp/tools/solution_tools.py +57 -15
  84. package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
  85. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  86. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  87. package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
  88. 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", default="")
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", default="")
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", default="system:all")
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", default="system:all")
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(dest="record_schema_command")
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("--column", dest="columns", action="append", type=int, default=[])
75
- list_parser.add_argument("--columns-file")
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("--query-field", dest="query_fields", action="append", type=int, default=[])
78
- list_parser.add_argument("--query-fields-file")
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("--column", dest="columns", action="append", type=int, default=[])
91
- access_parser.add_argument("--columns-file")
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("--column", dest="columns", action="append", type=int, default=[])
101
- get.add_argument("--columns-file")
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.set_defaults(handler=_handle_delete, format_hint="")
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 applicant --app-key APP_KEY",
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 <applicant|browse|insert|update|import|code-block> ...")
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 applicant`, `record schema browse`, `record schema insert`, `record schema update`, `record schema import`, or `record schema code-block`.",
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
- formatter = _FORMATTERS.get(hint, _format_generic)
12
- text = formatter(result)
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,