@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
@@ -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
- category = str(payload.get("category") or "").lower()
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
- text = self.message.lower()
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()
@@ -51,35 +51,40 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
51
51
  "record_schema_get",
52
52
  cli_route=("record", "schema", "applicant"),
53
53
  mcp_public=False,
54
+ cli_public=False,
54
55
  ),
55
56
  PublicToolSpec(
56
57
  USER_DOMAIN,
57
58
  "record_browse_schema_get",
58
59
  ("record_browse_schema_get_public",),
59
60
  ("record", "schema", "browse"),
61
+ cli_show_effective_context=True,
60
62
  ),
61
63
  PublicToolSpec(
62
64
  USER_DOMAIN,
63
65
  "record_insert_schema_get",
64
66
  ("record_insert_schema_get_public",),
65
67
  ("record", "schema", "insert"),
68
+ cli_show_effective_context=True,
66
69
  ),
67
70
  PublicToolSpec(
68
71
  USER_DOMAIN,
69
72
  "record_update_schema_get",
70
73
  ("record_update_schema_get_public",),
71
74
  ("record", "schema", "update"),
75
+ cli_show_effective_context=True,
72
76
  ),
73
- PublicToolSpec(USER_DOMAIN, "record_import_schema_get", ("record_import_schema_get",), ("record", "schema", "import")),
77
+ PublicToolSpec(USER_DOMAIN, "record_import_schema_get", ("record_import_schema_get",), ("record", "schema", "import"), cli_show_effective_context=True),
74
78
  PublicToolSpec(
75
79
  USER_DOMAIN,
76
80
  "record_code_block_schema_get",
77
81
  ("record_code_block_schema_get_public",),
78
82
  ("record", "schema", "code-block"),
83
+ cli_show_effective_context=True,
79
84
  ),
80
- PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), ("record", "member-candidates")),
81
- PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), ("record", "department-candidates")),
82
- PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze"), mcp_public=False),
85
+ PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), ("record", "member-candidates"), cli_show_effective_context=True),
86
+ PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), ("record", "department-candidates"), cli_show_effective_context=True),
87
+ PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze"), mcp_public=False, cli_public=False),
83
88
  PublicToolSpec(USER_DOMAIN, "record_list", ("record_list",), ("record", "list"), cli_show_effective_context=True),
84
89
  PublicToolSpec(USER_DOMAIN, "record_access", ("record_access",), ("record", "access"), cli_show_effective_context=True),
85
90
  PublicToolSpec(USER_DOMAIN, "record_get", ("record_get_public",), ("record", "get"), cli_show_effective_context=True),
@@ -87,11 +92,11 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
87
92
  PublicToolSpec(USER_DOMAIN, "record_insert", ("record_insert_public",), ("record", "insert"), cli_show_effective_context=True, cli_context_write=True),
88
93
  PublicToolSpec(USER_DOMAIN, "record_update", ("record_update_public",), ("record", "update"), cli_show_effective_context=True, cli_context_write=True),
89
94
  PublicToolSpec(USER_DOMAIN, "record_delete", ("record_delete_public",), ("record", "delete"), cli_show_effective_context=True, cli_context_write=True),
90
- PublicToolSpec(USER_DOMAIN, "record_import_template_get", ("record_import_template_get",), ("import", "template")),
91
- PublicToolSpec(USER_DOMAIN, "record_import_verify", ("record_import_verify",), ("import", "verify")),
95
+ PublicToolSpec(USER_DOMAIN, "record_import_template_get", ("record_import_template_get",), ("import", "template"), cli_show_effective_context=True),
96
+ PublicToolSpec(USER_DOMAIN, "record_import_verify", ("record_import_verify",), ("import", "verify"), cli_show_effective_context=True),
92
97
  PublicToolSpec(USER_DOMAIN, "record_import_repair_local", ("record_import_repair_local",), ("import", "repair")),
93
- PublicToolSpec(USER_DOMAIN, "record_import_start", ("record_import_start",), ("import", "start")),
94
- PublicToolSpec(USER_DOMAIN, "record_import_status_get", ("record_import_status_get",), ("import", "status")),
98
+ PublicToolSpec(USER_DOMAIN, "record_import_start", ("record_import_start",), ("import", "start"), cli_show_effective_context=True, cli_context_write=True),
99
+ PublicToolSpec(USER_DOMAIN, "record_import_status_get", ("record_import_status_get",), ("import", "status"), cli_show_effective_context=True),
95
100
  PublicToolSpec(USER_DOMAIN, "record_export_start", ("record_export_start",), ("export", "start"), cli_show_effective_context=True),
96
101
  PublicToolSpec(USER_DOMAIN, "record_export_status_get", ("record_export_status_get",), ("export", "status"), cli_show_effective_context=True),
97
102
  PublicToolSpec(USER_DOMAIN, "record_export_get", ("record_export_get",), ("export", "get"), cli_show_effective_context=True),
@@ -109,12 +114,12 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
109
114
  ),
110
115
  PublicToolSpec(USER_DOMAIN, "task_workflow_log_get", ("task_workflow_log_get",), ("task", "log"), cli_show_effective_context=True),
111
116
  PublicToolSpec(USER_DOMAIN, "directory_search", ("directory_search",), cli_public=False),
112
- PublicToolSpec(USER_DOMAIN, "directory_list_internal_users", ("directory_list_internal_users",), cli_public=False),
113
- PublicToolSpec(USER_DOMAIN, "directory_list_all_internal_users", ("directory_list_all_internal_users",), cli_public=False),
114
- PublicToolSpec(USER_DOMAIN, "directory_list_internal_departments", ("directory_list_internal_departments",), cli_public=False),
115
- PublicToolSpec(USER_DOMAIN, "directory_list_all_departments", ("directory_list_all_departments",), cli_public=False),
116
- PublicToolSpec(USER_DOMAIN, "directory_list_sub_departments", ("directory_list_sub_departments",), cli_public=False),
117
- PublicToolSpec(USER_DOMAIN, "directory_list_external_members", ("directory_list_external_members",), cli_public=False),
117
+ PublicToolSpec(USER_DOMAIN, "directory_list_internal_users", ("directory_list_internal_users",), mcp_public=False, cli_public=False),
118
+ PublicToolSpec(USER_DOMAIN, "directory_list_all_internal_users", ("directory_list_all_internal_users",), mcp_public=False, cli_public=False),
119
+ PublicToolSpec(USER_DOMAIN, "directory_list_internal_departments", ("directory_list_internal_departments",), mcp_public=False, cli_public=False),
120
+ PublicToolSpec(USER_DOMAIN, "directory_list_all_departments", ("directory_list_all_departments",), mcp_public=False, cli_public=False),
121
+ PublicToolSpec(USER_DOMAIN, "directory_list_sub_departments", ("directory_list_sub_departments",), mcp_public=False, cli_public=False),
122
+ PublicToolSpec(USER_DOMAIN, "directory_list_external_members", ("directory_list_external_members",), mcp_public=False, cli_public=False),
118
123
  )
119
124
 
120
125
 
@@ -157,7 +162,7 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
157
162
  PublicToolSpec(BUILDER_DOMAIN, "app_views_apply", ("app_views_apply",), ("builder", "views", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
158
163
  PublicToolSpec(BUILDER_DOMAIN, "app_charts_apply", ("app_charts_apply",), ("builder", "charts", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
159
164
  PublicToolSpec(BUILDER_DOMAIN, "portal_apply", ("portal_apply",), ("builder", "portal", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
160
- PublicToolSpec(BUILDER_DOMAIN, "app_publish_verify", ("app_publish_verify",), ("builder", "publish", "verify"), has_contract=True, cli_show_effective_context=True),
165
+ PublicToolSpec(BUILDER_DOMAIN, "app_publish_verify", ("app_publish_verify",), ("builder", "publish", "verify"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
161
166
  )
162
167
 
163
168
 
@@ -235,6 +240,7 @@ def cli_route_from_namespace(args: Namespace) -> tuple[str, ...] | None:
235
240
  builder_command = getattr(args, "builder_command", None)
236
241
  if not isinstance(builder_command, str) or not builder_command:
237
242
  return None
243
+ builder_command = "associated-resource" if builder_command == "associated-resources" else builder_command
238
244
  if builder_command == "contract":
239
245
  return ("builder", "contract")
240
246
  if builder_command == "app":
@@ -47,7 +47,6 @@ COMMON_ERROR_DROP_TOP = {
47
47
  "allowed_values",
48
48
  "missing_fields",
49
49
  "noop",
50
- "ok",
51
50
  }
52
51
 
53
52
  SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
@@ -58,11 +57,18 @@ def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict
58
57
  return payload
59
58
  if _looks_like_failure_payload(payload):
60
59
  status = str(payload.get("status") or "").lower()
61
- if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and status in {
62
- "blocked",
63
- "needs_confirmation",
64
- "partial_success",
65
- }:
60
+ if _is_executed_nonfatal_payload(payload) or (
61
+ tool_name in {"user:record_insert", "user:record_update", "user:record_delete"}
62
+ and status in {
63
+ "blocked",
64
+ "needs_confirmation",
65
+ "partial_success",
66
+ }
67
+ ) or (
68
+ tool_name == "user:record_import_verify"
69
+ and status == "failed"
70
+ and "can_import" in payload
71
+ ):
66
72
  return trim_success_response(tool_name, payload)
67
73
  return _trim_returned_failure(payload)
68
74
  return trim_success_response(tool_name, payload)
@@ -151,12 +157,25 @@ def _parse_runtime_error_payload(exc: RuntimeError) -> dict[str, Any] | None:
151
157
 
152
158
 
153
159
  def _looks_like_failure_payload(payload: dict[str, Any]) -> bool:
160
+ if _is_executed_nonfatal_payload(payload):
161
+ return False
154
162
  if payload.get("ok") is False:
155
163
  return True
156
164
  status = str(payload.get("status") or "").lower()
157
165
  return status in {"failed", "blocked"}
158
166
 
159
167
 
168
+ def _is_executed_nonfatal_payload(payload: dict[str, Any]) -> bool:
169
+ status = str(payload.get("status") or "").lower()
170
+ executed = bool(
171
+ payload.get("write_executed")
172
+ or payload.get("delete_executed")
173
+ or payload.get("action_executed")
174
+ or payload.get("export_executed")
175
+ )
176
+ return executed and status in {"partial_success", "verification_failed", "running", "queued", "unknown"}
177
+
178
+
160
179
  def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
161
180
  trimmed = deepcopy(payload)
162
181
  _drop_top_keys(trimmed, COMMON_ERROR_DROP_TOP)
@@ -325,15 +344,22 @@ def _trim_import_schema(payload: JSONObject) -> None:
325
344
  columns = [item for item in payload.get("expected_columns", []) if isinstance(item, dict)]
326
345
  if columns is not None:
327
346
  payload["columns"] = [_compact_import_column(item) for item in columns]
347
+ import_capability = payload.get("import_capability") if isinstance(payload.get("import_capability"), dict) else {}
348
+ if import_capability:
349
+ verification = payload.get("verification") if isinstance(payload.get("verification"), dict) else {}
350
+ if not isinstance(payload.get("verification"), dict):
351
+ payload["verification"] = verification
352
+ verification.setdefault("import_auth_source", import_capability.get("auth_source"))
353
+ verification.setdefault("import_capability_can_import", import_capability.get("can_import"))
328
354
  payload.pop("expected_columns", None)
329
355
  payload.pop("schema_fingerprint", None)
330
356
  payload.pop("import_capability", None)
331
357
  payload.pop("request_route", None)
332
- payload.pop("verification", None)
333
358
 
334
359
  if _looks_like_import_verify(payload):
335
360
  _trim_import_verify_payload(payload)
336
361
  return
362
+ payload.pop("verification", None)
337
363
  if "applied_repairs" in payload or "repaired_file_path" in payload:
338
364
  _trim_import_repair_payload(payload)
339
365
  return
@@ -394,10 +420,14 @@ def _trim_record_write(payload: JSONObject) -> None:
394
420
  data.pop("normalized_payload", None)
395
421
  data.pop("human_review", None)
396
422
  data.pop("action", None)
397
- for key in ("update_route", "tried_routes"):
398
- value = payload.get(key)
399
- if value not in (None, [], {}, ""):
400
- data[key] = value
423
+ if payload.get("status") == "success":
424
+ data.pop("tried_routes", None)
425
+ update_route = payload.get("update_route")
426
+ if update_route not in (None, [], {}, ""):
427
+ data["update_route"] = update_route
428
+ tried_routes = payload.get("tried_routes")
429
+ if payload.get("status") != "success" and tried_routes not in (None, [], {}, ""):
430
+ data["tried_routes"] = tried_routes
401
431
  resource = _compact_record_resource(data.get("resource"))
402
432
  if resource:
403
433
  data["resource"] = resource
@@ -441,6 +471,7 @@ def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
441
471
  "record_id",
442
472
  "apply_id",
443
473
  "write_executed",
474
+ "write_succeeded",
444
475
  "verification_status",
445
476
  "safe_to_retry",
446
477
  "update_route",
@@ -887,6 +918,12 @@ def _compact_import_column(item: dict[str, Any]) -> dict[str, Any]:
887
918
  compact["options"] = options
888
919
  if bool(item.get("accepts_natural_input")):
889
920
  compact["accepts_natural_input"] = True
921
+ import_value_format = item.get("import_value_format")
922
+ if isinstance(import_value_format, str) and import_value_format:
923
+ compact["import_value_format"] = import_value_format
924
+ format_hint = item.get("format_hint")
925
+ if isinstance(format_hint, str) and format_hint:
926
+ compact["format_hint"] = format_hint
890
927
  if bool(item.get("requires_upload")):
891
928
  compact["requires_upload"] = True
892
929
  target_app_key = item.get("target_app_key")
@@ -902,13 +939,37 @@ def _compact_import_column(item: dict[str, Any]) -> dict[str, Any]:
902
939
 
903
940
 
904
941
  def _looks_like_import_verify(payload: JSONObject) -> bool:
905
- return "verification_id" in payload and "can_import" in payload
942
+ if "verification_id" in payload and "can_import" in payload:
943
+ return True
944
+ return "can_import" in payload and (
945
+ "error_code" in payload
946
+ or "issues" in payload
947
+ or "file_path" in payload
948
+ )
906
949
 
907
950
 
908
951
  def _trim_import_verify_payload(payload: JSONObject) -> None:
909
952
  issues = payload.get("issues") if isinstance(payload.get("issues"), list) else []
910
953
  issue_summary = _summarize_import_issues(issues)
911
954
  payload["issue_summary"] = issue_summary
955
+ verification = payload.get("verification") if isinstance(payload.get("verification"), dict) else {}
956
+ compact_verification = {
957
+ key: verification.get(key)
958
+ for key in (
959
+ "import_auth_prechecked",
960
+ "import_auth_precheck_passed",
961
+ "import_auth_source",
962
+ "import_capability_can_import",
963
+ "local_precheck_passed",
964
+ "backend_verification_passed",
965
+ "auto_normalized",
966
+ )
967
+ if key in verification
968
+ }
969
+ if compact_verification:
970
+ payload["verification"] = compact_verification
971
+ else:
972
+ payload.pop("verification", None)
912
973
  columns = payload.get("columns")
913
974
  if "expected_columns" not in payload and isinstance(columns, list):
914
975
  payload["expected_columns"] = columns
@@ -1004,7 +1065,7 @@ def _trim_feedback(payload: JSONObject) -> None:
1004
1065
 
1005
1066
 
1006
1067
  def _trim_builder_envelope(payload: JSONObject) -> None:
1007
- if str(payload.get("status") or "").lower() == "success":
1068
+ if str(payload.get("status") or "").lower() in {"success", "partial_success", "verification_failed"}:
1008
1069
  details = payload.get("details")
1009
1070
  if isinstance(details, dict):
1010
1071
  _drop_deep_keys(details, {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
@@ -50,6 +50,7 @@ All resource tools operate with the logged-in user's Qingflow permissions.
50
50
 
51
51
  If `app_key` is unknown, use `app_list` first. Pass `query` to filter visible apps by keyword.
52
52
  If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
53
+ Treat an explicit `view_id` as the exact frontend view context. If `system:all` fails, do not silently switch to `system:initiated`, `system:todo`, or another system view unless the user, frontend URL, or `app_get.accessible_views` explicitly selects that view.
53
54
  If an accessible view has `analysis_supported=false`, do not use it for `record_access` or `record_list`. `boardView` and `ganttView` are special UI views, not data-access targets.
54
55
  `view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
55
56
 
@@ -57,7 +58,7 @@ If an accessible view has `analysis_supported=false`, do not use it for `record_
57
58
 
58
59
  Call `record_insert_schema_get` before `record_insert`.
59
60
  For simple field changes after the target record is clear, call `record_update` directly. Use `record_update_schema_get` for diagnostics, ambiguous fields, or complex writable-scope inspection.
60
- Call `record_code_block_schema_get` before `record_code_block_run`.
61
+ Prefer `record_code_block_schema_get` before `record_code_block_run` when field selection or binding diagnostics are unclear; if the exact code-block field id is already known from record/task detail, run directly.
61
62
  Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, `record_get`, or `record_logs_get`.
62
63
  Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
63
64
 
@@ -67,7 +68,7 @@ Call `record_import_schema_get` when the import field mapping is unclear before
67
68
  ## Schema Scope
68
69
 
69
70
  `record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
70
- `record_update_schema_get` returns the current record's overall update-ready writable field set and route diagnostics across matched accessible views; read `writable_fields`, `payload_template`, `available_update_routes`, and `recommended_update_route`.
71
+ `record_update_schema_get` returns the current record's update-ready writable field set and route diagnostics. When `view_id` is explicit, it is the exact frontend view context and must not be silently replaced by another view.
71
72
  `record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
72
73
  `record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema.
73
74
  `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
@@ -106,8 +107,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
106
107
  `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get / record_logs_get`
107
108
  `record_insert_schema_get -> record_insert(items)`
108
109
  `record_update` for simple updates; `record_update_schema_get -> record_update` when the writable field scope is unclear.
109
- `record_list / record_get -> record_delete`
110
- `record_code_block_schema_get -> record_code_block_run`
110
+ `record_list / record_get -> record_delete(system view_id)`
111
+ `record_code_block_run` directly when the exact code-block field is known; otherwise `record_code_block_schema_get -> record_code_block_run`
111
112
 
112
113
  - Use `columns` as `[{{field_id}}]`
113
114
  - Use `record_list(query=..., query_fields=[{{field_id}}])` for fuzzy single-record lookup, then follow `lookup.next_action`; `query_fields` is search scope and `columns` is display shape.
@@ -116,14 +117,14 @@ Analysis answers must include concrete numbers. When applicable, include percent
116
117
  - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
117
118
 
118
119
  - `record_insert` defaults to an applicant-node `items` array; each item contains a field-title keyed `fields` map. A single insert is one item.
119
- - `record_update` uses a field-title keyed `fields` map. It first tries the data-manager direct update route, then falls back to the frontend custom-view detail edit route when the selected view can cover the payload; if a unique current-user todo task for the same record exposes editable fields, it can finally use the workflow save-only route. Read `update_route` and `tried_routes` after execution.
120
+ - `record_update` uses a field-title keyed `fields` map. It first tries the data-manager direct update route, then falls back to the frontend custom-view detail edit route when the selected view can cover the payload; if a unique current-user todo task for the same record exposes editable fields, it can finally use the workflow save-only route. On success, read `status`, `update_route`, and `verification_status`; on failure, read the failure reason and route diagnostics.
120
121
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
121
122
  - For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
122
123
  - Read field-level `linkage` whenever present on `record_insert_schema_get` or `record_update_schema_get`; it is the static hint for linked visibility, reference-driven auto fill, and formula/default auto-fill behavior.
123
124
  - `linkage.sources` lists upstream field titles that influence the current field; `linkage.affects_fields` lists downstream fields that may change when the current field changes.
124
125
  - `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
125
- - `record_update_schema_get` exposes the overall writable field set and route candidates for the record, but not every field combination is guaranteed; `record_update` still needs data-manager permission, one single matched custom view that can cover the payload, or one unique editable current-user todo task.
126
- - `record_delete` deletes by `record_id` or `record_ids`.
126
+ - `record_update_schema_get` exposes the writable field set and route candidates for the record; with an explicit `view_id`, diagnostics stay scoped to that selected frontend view. Not every field combination is guaranteed; `record_update` still needs data-manager permission, one single matched custom view that can cover the payload, or one unique editable current-user todo task.
127
+ - `record_delete` deletes by `record_id` or `record_ids`, and requires an accessible system `view_id`; use custom views only to locate records, not as the delete route.
127
128
  - `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
128
129
  - Use `record_logs_get` only when the user needs the full visible data/workflow log history for a specific record. It writes JSONL files locally and returns file paths plus completeness metadata; do not expect full log arrays in the response.
129
130
  - Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; read attachments/documents/tables from `record_get.file_assets.items[].local_path` and `extraction.text_path` when present. `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
@@ -140,7 +141,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
140
141
 
141
142
  Use `record_code_block_run` when the user wants to execute a form code-block field against an existing record.
142
143
 
143
- - Always resolve the exact code-block field from `record_code_block_schema_get` first.
144
+ - Prefer resolving the exact code-block field from `record_code_block_schema_get`, but do not treat applicant-schema 40002 as final denial when record/task detail already exposes the code-block field id.
145
+ - `record_code_block_run` uses the record/task detail answers for the execution context; when applicant schema is unavailable it can still execute with a numeric code-block field id, but schema-bound relation writeback may be skipped.
144
146
  - Treat code-block execution as write-capable, not read-only.
145
147
  - If the code block is bound to relation outputs, Qingflow may calculate target answers and write them back automatically.
146
148
  - For safe debugging, pass `apply_writeback=false` and inspect the parsed alias results plus `relation.calculated_answers_preview` before allowing any writeback.
@@ -186,7 +188,7 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
186
188
  - `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
187
189
  - `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
188
190
  - Use `task_associated_report_detail_get` for associated view or report details.
189
- - Use `task_workflow_log_get` for the current task context workflow log page. For full record-level data/workflow logs, use `record_logs_get(app_key, record_id, view_id?)`.
191
+ - Use `task_workflow_log_get` for the current task context workflow log page. For full record-level data/workflow logs, first choose an accessible view with `app_get`, then call `record_logs_get(app_key, record_id, view_id)` with that same explicit `view_id`.
190
192
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
191
193
 
192
194
  ## Time Handling
@@ -43,9 +43,10 @@ def build_builder_server() -> FastMCP:
43
43
  "Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
44
44
  "For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
45
45
  "For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
46
+ "For builder write/apply results, separate write execution from final readback verification: status=partial_success with write_executed=true and safe_to_retry=false means the write was sent or succeeded, while final readback, publish verification, or metadata confirmation is pending or permission-restricted. Report it as written but unverified; do not repeat the same write blindly. "
46
47
  "For app_schema_apply, configure data title and data cover directly in field JSON with as_data_title=true and as_data_cover=true; data title is required and exactly one field may be marked, while data cover is optional and must be a top-level attachment field. For multi-app creation, pass apps[]/--apps-file on app_schema_apply; each item may have client_key, and relation fields may use target_app_ref to point at another same-call client_key. "
47
- "For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. New views default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless associated_resources is explicitly patched. "
48
- "For custom button body create/update/delete and view placement, use app_custom_buttons_apply. For addData buttons, prefer trigger_add_data_config.target_app_key + field_mappings/default_values; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default and merge-mode view_configs must include buttons; use view_configs[].mode=replace or explicit buttons=[] only when clearing/replacing existing bindings is intended. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
48
+ "For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. New views default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless associated_resources is explicitly patched. View writes use ViewManagementAuth (beingViewManageStatus), falling back to DataManageAuth when advanced app permissions are not enabled. "
49
+ "For custom button body create/update/delete and view placement, use app_custom_buttons_apply. Button body writes require EditAppAuth; view_configs placement writes require ViewManagementAuth, matching the backend viewConfig route. For addData buttons, prefer trigger_add_data_config.target_app_key + field_mappings/default_values; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default and merge-mode view_configs must include buttons; use view_configs[].mode=replace or explicit buttons=[] only when clearing/replacing existing bindings is intended. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
49
50
  "For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; chart dimension/metric/filter/query fields must come from app_get_fields.chart_fields, not record schema or form-only fields; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
50
51
  "For associated views/reports, use app_associated_resources_apply. Use match_mappings for filtering associated resources: dynamic current-record conditions use source_field, static conditions use value. match_mappings also supports 数据ID(-17) and 编号(0). Do not ask agents to write raw match_rules unless preserving a legacy backend config. "
51
52
  "For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "