@josephyan/qingflow-cli 1.1.4 → 1.1.6

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 (154) hide show
  1. package/README.md +7 -3
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/bin/qingflow.mjs +1 -34
  6. package/npm/lib/runtime.mjs +21 -101
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-cli/SKILL.md +58 -44
  11. package/skills/qingflow-cli/manifest.yaml +1 -1
  12. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  13. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
  14. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
  15. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
  16. package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
  17. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
  18. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
  19. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
  21. package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
  22. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
  23. package/skills/qingflow-cli/reference/builder/README.md +41 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  25. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  26. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  27. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
  28. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
  29. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
  30. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
  31. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
  32. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
  33. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
  34. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
  35. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
  36. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
  37. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
  38. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
  39. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
  40. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
  41. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
  42. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
  43. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
  44. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
  45. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
  46. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
  47. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
  48. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  49. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  50. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
  51. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
  52. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
  53. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  54. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  55. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
  56. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  57. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  58. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
  59. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
  60. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
  61. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
  62. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
  63. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
  64. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
  65. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  66. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
  67. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
  68. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  69. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  70. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  71. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  72. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  73. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  74. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  75. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  76. package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
  77. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
  78. package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
  79. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  80. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
  81. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  82. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  83. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
  84. package/skills/qingflow-mcp-setup/SKILL.md +115 -0
  85. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  86. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  87. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  88. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  89. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  90. package/src/qingflow_mcp/__init__.py +1 -1
  91. package/src/qingflow_mcp/__main__.py +6 -2
  92. package/src/qingflow_mcp/builder_facade/models.py +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4192 -935
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -298
  95. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  96. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  97. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  98. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  99. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  100. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  101. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  102. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  103. package/src/qingflow_mcp/cli/context.py +0 -3
  104. package/src/qingflow_mcp/cli/formatters.py +784 -16
  105. package/src/qingflow_mcp/cli/main.py +117 -33
  106. package/src/qingflow_mcp/errors.py +43 -2
  107. package/src/qingflow_mcp/public_surface.py +26 -17
  108. package/src/qingflow_mcp/response_trim.py +81 -17
  109. package/src/qingflow_mcp/server.py +14 -12
  110. package/src/qingflow_mcp/server_app_builder.py +65 -21
  111. package/src/qingflow_mcp/server_app_user.py +22 -16
  112. package/src/qingflow_mcp/session_store.py +11 -7
  113. package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
  114. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  115. package/src/qingflow_mcp/solution/executor.py +245 -18
  116. package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
  117. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  118. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  119. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  120. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  121. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  122. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  123. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  124. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  125. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  126. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  127. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  128. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  129. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  130. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  131. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  132. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  133. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  134. package/src/qingflow_mcp/tools/solution_tools.py +59 -45
  135. package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
  136. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  137. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  138. package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
  139. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  140. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
  141. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
  142. /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
  143. /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
  144. /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
  145. /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
  146. /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
  147. /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
  148. /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
  149. /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
  150. /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
  151. /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
  152. /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
  153. /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
  154. /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
@@ -5,7 +5,7 @@ from typing import cast
5
5
  from mcp.server.fastmcp import FastMCP
6
6
 
7
7
  from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE
8
- from ..errors import QingflowApiError, raise_tool_error
8
+ from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error, message_looks_like_invalid_token, raise_tool_error
9
9
  from ..json_types import JSONObject, JSONValue
10
10
  from .base import tool_cn_name
11
11
  from .record_tools import (
@@ -19,9 +19,11 @@ from .record_tools import (
19
19
  FieldIndex,
20
20
  FormField,
21
21
  RecordTools,
22
+ _build_answer_backed_field_index,
22
23
  _coerce_count,
23
24
  _collect_question_relations,
24
25
  _field_ref_payload,
26
+ _merge_field_indexes,
25
27
  _normalize_optional_text,
26
28
  _relation_ids_from_answer,
27
29
  _stringify_json,
@@ -31,6 +33,7 @@ from .record_tools import (
31
33
  CODE_BLOCK_QUE_TYPE = 26
32
34
  CODE_BLOCK_RELATION_TYPE = 3
33
35
  SUPPORTED_CODE_BLOCK_ROLES = {1, 2, 3, 5}
36
+ _CODE_BLOCK_SCHEMA_PERMISSION_CODES = {40002, 40027, 404}
34
37
 
35
38
 
36
39
  class CodeBlockTools(RecordTools):
@@ -65,17 +68,48 @@ class CodeBlockTools(RecordTools):
65
68
  self._form_cache[cache_key] = normalized
66
69
  return normalized
67
70
 
71
+ def _get_code_block_relation_schema_optional(
72
+ self,
73
+ profile: str,
74
+ context, # type: ignore[no-untyped-def]
75
+ app_key: str,
76
+ *,
77
+ force_refresh: bool,
78
+ warnings: list[JSONObject],
79
+ ) -> JSONObject:
80
+ try:
81
+ return self._get_code_block_relation_schema(
82
+ profile,
83
+ context,
84
+ app_key,
85
+ force_refresh=force_refresh,
86
+ )
87
+ except QingflowApiError as exc:
88
+ if not _is_optional_code_block_schema_error(exc):
89
+ raise
90
+ warnings.append(
91
+ {
92
+ "code": "CODE_BLOCK_SCHEMA_UNAVAILABLE",
93
+ "message": "applicant form schema was not readable in this permission context; code-block execution will use record/task answers and skip schema-bound relation writeback.",
94
+ "backend_code": exc.backend_code,
95
+ "http_status": exc.http_status,
96
+ "request_id": exc.request_id,
97
+ }
98
+ )
99
+ return {}
100
+
68
101
  def register(self, mcp: FastMCP) -> None:
69
102
  """注册当前工具到 MCP 服务。"""
70
103
  super().register(mcp)
71
104
 
72
105
  @mcp.tool()
73
106
  def record_code_block_schema_get(
107
+ profile: str = DEFAULT_PROFILE,
74
108
  app_key: str = "",
75
109
  output_profile: str = "normal",
76
110
  ) -> JSONObject:
77
111
  return self.record_code_block_schema_get_public(
78
- profile=DEFAULT_PROFILE,
112
+ profile=profile,
79
113
  app_key=app_key,
80
114
  output_profile=output_profile,
81
115
  )
@@ -84,7 +118,8 @@ class CodeBlockTools(RecordTools):
84
118
  description=(
85
119
  "Run a form code-block field against the current record data, parse alias results, and optionally "
86
120
  "reuse Qingflow's existing relation-calculation chain to compute bound outputs and write them back. "
87
- "Use record_code_block_schema_get first and choose an exact code-block field selector. "
121
+ "Use record_code_block_schema_get when field selection or binding diagnostics are unclear; "
122
+ "if the exact code-block field id is known from record/task detail, run directly. "
88
123
  "For safe debugging, pass apply_writeback=false to inspect parsed results without writing back."
89
124
  )
90
125
  )
@@ -93,6 +128,7 @@ class CodeBlockTools(RecordTools):
93
128
  app_key: str = "",
94
129
  record_id: str = "",
95
130
  code_block_field: str = "",
131
+ view_id: str | None = None,
96
132
  role: int = 1,
97
133
  workflow_node_id: int | None = None,
98
134
  answers: list[JSONObject] | None = None,
@@ -108,6 +144,7 @@ class CodeBlockTools(RecordTools):
108
144
  app_key=app_key,
109
145
  record_id=record_id,
110
146
  code_block_field=code_block_field,
147
+ view_id=view_id,
111
148
  role=role,
112
149
  workflow_node_id=workflow_node_id,
113
150
  answers=answers or [],
@@ -133,8 +170,45 @@ class CodeBlockTools(RecordTools):
133
170
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
134
171
 
135
172
  def runner(session_profile, context):
136
- relation_schema = self._get_code_block_relation_schema(profile, context, app_key, force_refresh=False)
137
- index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=False)
173
+ try:
174
+ relation_schema = self._get_code_block_relation_schema(profile, context, app_key, force_refresh=False)
175
+ index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=False)
176
+ except QingflowApiError as exc:
177
+ if not _is_optional_code_block_schema_error(exc):
178
+ raise
179
+ return {
180
+ "profile": profile,
181
+ "ws_id": session_profile.selected_ws_id,
182
+ "ok": False,
183
+ "status": "failed",
184
+ "error_code": "CODE_BLOCK_SCHEMA_UNAVAILABLE",
185
+ "message": (
186
+ "applicant form schema was not readable in this permission context; "
187
+ "record schema code-block is only a diagnostic helper. "
188
+ "If the code-block field is known from record/task detail, run record code-block-run directly."
189
+ ),
190
+ "backend_code": exc.backend_code,
191
+ "http_status": exc.http_status,
192
+ "request_id": exc.request_id,
193
+ "request_route": self._request_route_payload(context),
194
+ "warnings": [
195
+ {
196
+ "code": "CODE_BLOCK_SCHEMA_UNAVAILABLE",
197
+ "message": "schema diagnostic unavailable; code-block run can still use record/task answers when code_block_field is known.",
198
+ "backend_code": exc.backend_code,
199
+ "http_status": exc.http_status,
200
+ "request_id": exc.request_id,
201
+ }
202
+ ],
203
+ "app_key": app_key,
204
+ "schema_scope": "code_block_ready",
205
+ "code_block_fields": [],
206
+ "input_fields": [],
207
+ "suggested_next_call": {
208
+ "tool_name": "record_code_block_run",
209
+ "required": ["app_key", "record_id", "code_block_field"],
210
+ },
211
+ }
138
212
  input_fields = [
139
213
  self._ready_schema_field_payload(
140
214
  profile,
@@ -199,6 +273,7 @@ class CodeBlockTools(RecordTools):
199
273
  app_key: str,
200
274
  record_id: int | str,
201
275
  code_block_field: str,
276
+ view_id: str | None = None,
202
277
  role: int = 1,
203
278
  workflow_node_id: int | None = None,
204
279
  answers: list[JSONObject] | None = None,
@@ -220,14 +295,42 @@ class CodeBlockTools(RecordTools):
220
295
  raise_tool_error(QingflowApiError.config_error("code_block_field is required"))
221
296
 
222
297
  def runner(session_profile, context):
223
- relation_schema = self._get_code_block_relation_schema(
298
+ warnings: list[JSONObject] = []
299
+ current_answers = self._load_record_answers_for_code_block(
300
+ context,
301
+ profile=profile,
302
+ app_key=app_key,
303
+ apply_id=normalized_record_id,
304
+ view_id=view_id,
305
+ role=role,
306
+ audit_node_id=workflow_node_id,
307
+ )
308
+ answer_index = _build_answer_backed_field_index(current_answers)
309
+ schema_index: FieldIndex | None = None
310
+ relation_schema = self._get_code_block_relation_schema_optional(
224
311
  profile,
225
312
  context,
226
313
  app_key,
227
314
  force_refresh=force_refresh_form,
315
+ warnings=warnings,
228
316
  )
229
- index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
230
- code_block = self._resolve_field_selector(code_block_field, index, location="code_block_field")
317
+ try:
318
+ schema_index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
319
+ except QingflowApiError as exc:
320
+ if not _is_optional_code_block_schema_error(exc):
321
+ raise
322
+ if not any(item.get("code") == "CODE_BLOCK_SCHEMA_UNAVAILABLE" for item in warnings):
323
+ warnings.append(
324
+ {
325
+ "code": "CODE_BLOCK_SCHEMA_UNAVAILABLE",
326
+ "message": "applicant form schema was not readable in this permission context; code-block execution will use record/task answers and skip schema-bound relation writeback.",
327
+ "backend_code": exc.backend_code,
328
+ "http_status": exc.http_status,
329
+ "request_id": exc.request_id,
330
+ }
331
+ )
332
+ index = _merge_field_indexes(schema_index, answer_index) if schema_index is not None else answer_index
333
+ code_block = self._resolve_code_block_field_for_run(code_block_field, index)
231
334
  if code_block.que_type != CODE_BLOCK_QUE_TYPE:
232
335
  raise_tool_error(
233
336
  QingflowApiError(
@@ -241,14 +344,6 @@ class CodeBlockTools(RecordTools):
241
344
  },
242
345
  )
243
346
  )
244
-
245
- current_answers = self._load_record_answers_for_code_block(
246
- context,
247
- app_key=app_key,
248
- apply_id=normalized_record_id,
249
- role=role,
250
- audit_node_id=workflow_node_id,
251
- )
252
347
  override_answers = (
253
348
  self._resolve_answers(
254
349
  profile,
@@ -257,12 +352,13 @@ class CodeBlockTools(RecordTools):
257
352
  answers=answers or [],
258
353
  fields=fields or {},
259
354
  force_refresh_form=force_refresh_form,
355
+ field_index_override=index,
260
356
  )
261
357
  if answers or fields
262
358
  else []
263
359
  )
264
360
  merged_answers = self._merge_record_answers(current_answers, override_answers) if override_answers else current_answers
265
- key_que_values = self._answers_to_open_match_values(merged_answers, index)
361
+ key_que_values = self._answers_to_open_match_values(merged_answers, index, exclude_que_ids={code_block.que_id})
266
362
  run_body: JSONObject = {
267
363
  "role": role,
268
364
  "manual": bool(manual),
@@ -289,6 +385,7 @@ class CodeBlockTools(RecordTools):
289
385
  relation_items: list[JSONObject] = []
290
386
  calculated_answers: list[JSONObject] = []
291
387
  relation_result: JSONObject | None = None
388
+ relation_transport_error: JSONObject | None = None
292
389
  if relation_target_fields:
293
390
  relation_body: JSONObject = {
294
391
  "role": role,
@@ -306,26 +403,38 @@ class CodeBlockTools(RecordTools):
306
403
  relation_result = self.backend.request("POST", context, relation_route, json_body=relation_body)
307
404
  relation_items = _relation_result_items(relation_result)
308
405
  except QingflowApiError as exc:
309
- if exc.http_status != 404:
310
- raise
311
- relation_route = "/que/actuator"
312
- relation_result = self.backend.request("POST", context, relation_route, json_body=relation_body)
313
- relation_items = _relation_result_items(relation_result)
314
- if not relation_items:
406
+ if exc.http_status == 404:
407
+ relation_route = "/que/actuator"
408
+ try:
409
+ relation_result = self.backend.request("POST", context, relation_route, json_body=relation_body)
410
+ relation_items = _relation_result_items(relation_result)
411
+ except QingflowApiError as fallback_exc:
412
+ relation_transport_error = _code_block_transport_error(fallback_exc)
413
+ else:
414
+ relation_transport_error = _code_block_transport_error(exc)
415
+ if relation_transport_error is None and not relation_items:
315
416
  # Keep compatibility with legacy runtime deployments and lightweight test doubles
316
417
  # that still stub the older relation-calculation route only.
317
418
  relation_route = "/que/actuator"
318
- relation_result = self.backend.request("POST", context, relation_route, json_body=relation_body)
319
- relation_items = _relation_result_items(relation_result)
419
+ try:
420
+ relation_result = self.backend.request("POST", context, relation_route, json_body=relation_body)
421
+ relation_items = _relation_result_items(relation_result)
422
+ relation_transport_error = None
423
+ except QingflowApiError as exc:
424
+ relation_transport_error = relation_transport_error or _code_block_transport_error(exc)
320
425
  relation_errors = _relation_result_errors(relation_items)
321
426
  calculated_answers = _relation_result_answers(relation_items)
322
427
  write_result: JSONObject | None = None
428
+ write_error: JSONObject | None = None
323
429
  verification: JSONObject | None = None
324
430
  writeback_attempted = False
325
431
  writeback_applied = False
326
432
  status = "completed"
327
433
  ok = True
328
- if relation_errors:
434
+ if relation_transport_error is not None:
435
+ status = "relation_failed"
436
+ ok = False
437
+ elif relation_errors:
329
438
  status = "relation_failed"
330
439
  ok = False
331
440
  elif not apply_writeback:
@@ -335,21 +444,28 @@ class CodeBlockTools(RecordTools):
335
444
  if workflow_node_id is not None:
336
445
  write_body["auditNodeId"] = workflow_node_id
337
446
  writeback_attempted = True
338
- write_result = cast(
339
- JSONObject,
340
- self.backend.request(
341
- "POST",
342
- context,
343
- f"/app/{app_key}/apply/{normalized_record_id}",
344
- json_body=write_body,
345
- ),
346
- )
347
- writeback_applied = True
348
- if verify_writeback:
447
+ try:
448
+ write_result = cast(
449
+ JSONObject,
450
+ self.backend.request(
451
+ "POST",
452
+ context,
453
+ f"/app/{app_key}/apply/{normalized_record_id}",
454
+ json_body=write_body,
455
+ ),
456
+ )
457
+ writeback_applied = True
458
+ except QingflowApiError as exc:
459
+ write_error = _code_block_transport_error(exc)
460
+ status = "writeback_failed"
461
+ ok = False
462
+ if writeback_applied and verify_writeback:
349
463
  verification = self._verify_code_block_writeback_result(
350
464
  context,
465
+ profile=profile,
351
466
  app_key=app_key,
352
467
  apply_id=normalized_record_id,
468
+ view_id=view_id,
353
469
  expected_answers=calculated_answers,
354
470
  index=index,
355
471
  role=role,
@@ -357,7 +473,16 @@ class CodeBlockTools(RecordTools):
357
473
  )
358
474
  if not bool(verification.get("verified")):
359
475
  status = "verification_failed"
360
- ok = False
476
+ ok = True
477
+ warnings.append(
478
+ {
479
+ "code": "CODE_BLOCK_WRITEBACK_VERIFICATION_FAILED",
480
+ "message": (
481
+ "code-block execution and writeback completed, but field-level readback "
482
+ "could not verify the written values; do not treat this as writeback denial."
483
+ ),
484
+ }
485
+ )
361
486
  else:
362
487
  status = "no_writeback"
363
488
  response: JSONObject = {
@@ -369,9 +494,14 @@ class CodeBlockTools(RecordTools):
369
494
  "apply_id": normalized_record_id,
370
495
  "status": status,
371
496
  "ok": ok,
497
+ "write_executed": writeback_applied,
498
+ "write_succeeded": writeback_applied,
499
+ "safe_to_retry": not writeback_applied,
500
+ "warnings": warnings,
372
501
  "code_block_field": _field_ref_payload(code_block),
373
502
  "execution": {
374
503
  "executed": True,
504
+ "view_id": _normalize_optional_text(view_id),
375
505
  "role": role,
376
506
  "workflow_node_id": workflow_node_id,
377
507
  "manual": bool(manual),
@@ -389,6 +519,7 @@ class CodeBlockTools(RecordTools):
389
519
  "calculated_answer_count": len(calculated_answers),
390
520
  "calculated_answers_preview": calculated_answers,
391
521
  "errors": relation_errors,
522
+ "transport_error": relation_transport_error,
392
523
  },
393
524
  "writeback": {
394
525
  "enabled": bool(apply_writeback),
@@ -398,10 +529,15 @@ class CodeBlockTools(RecordTools):
398
529
  "verify_writeback": verify_writeback,
399
530
  "write_verified": bool(verification.get("verified")) if verification is not None else None,
400
531
  "result": write_result,
532
+ "error": write_error,
401
533
  "verification": verification,
402
534
  },
403
535
  "resource": {"apply_id": normalized_record_id},
404
536
  }
537
+ if not ok:
538
+ failure_error = relation_transport_error if relation_transport_error is not None else write_error
539
+ failure_context = "relation" if relation_transport_error is not None else "writeback"
540
+ response.update(_code_block_failure_fields(failure_error, context=failure_context))
405
541
  if normalized_output_profile == "verbose":
406
542
  response["debug"] = {
407
543
  "run_body": run_body,
@@ -415,16 +551,66 @@ class CodeBlockTools(RecordTools):
415
551
 
416
552
  return self._run_record_tool(profile, runner)
417
553
 
554
+ def _resolve_code_block_field_for_run(self, selector: str | int, index: FieldIndex) -> FormField:
555
+ field_id = _coerce_count(selector)
556
+ if field_id is not None and str(field_id) not in index.by_id:
557
+ return FormField(
558
+ que_id=field_id,
559
+ que_title=str(field_id),
560
+ que_type=CODE_BLOCK_QUE_TYPE,
561
+ required=False,
562
+ readonly=False,
563
+ system=False,
564
+ options=[],
565
+ aliases=[],
566
+ target_app_key=None,
567
+ target_app_name_hint=None,
568
+ member_select_scope_type=None,
569
+ member_select_scope=None,
570
+ dept_select_scope_type=None,
571
+ dept_select_scope=None,
572
+ raw={"queId": field_id, "queTitle": str(field_id), "queType": CODE_BLOCK_QUE_TYPE},
573
+ )
574
+ return self._resolve_field_selector(selector, index, location="code_block_field")
575
+
418
576
  def _load_record_answers_for_code_block(
419
577
  self,
420
578
  context, # type: ignore[no-untyped-def]
421
579
  *,
580
+ profile: str,
422
581
  app_key: str,
423
582
  apply_id: int,
583
+ view_id: str | None,
424
584
  role: int,
425
585
  audit_node_id: int | None,
426
586
  ) -> list[JSONObject]:
427
587
  """执行内部辅助逻辑。"""
588
+ normalized_view_id = _normalize_optional_text(view_id)
589
+ if normalized_view_id:
590
+ try:
591
+ resolved_view, _warnings = self._resolve_accessible_view_route(
592
+ profile,
593
+ context,
594
+ app_key,
595
+ view_id=normalized_view_id,
596
+ list_type=None,
597
+ view_key=None,
598
+ view_name=None,
599
+ allow_default=False,
600
+ )
601
+ record, _used_list_type, _used_role = self._record_get_apply_detail(
602
+ context,
603
+ app_key=app_key,
604
+ record_id=apply_id,
605
+ resolved_view=resolved_view,
606
+ audit_node_id=audit_node_id,
607
+ )
608
+ answers = record.get("answers") if isinstance(record, dict) else None
609
+ return [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
610
+ except QingflowApiError as exc:
611
+ if not _is_optional_code_block_record_read_error(exc):
612
+ raise
613
+
428
614
  last_error: QingflowApiError | None = None
429
615
  for list_type in self._INTERNAL_GET_LIST_TYPE_FALLBACKS:
430
616
  params: JSONObject = {"role": role, "listType": list_type}
@@ -436,19 +622,28 @@ class CodeBlockTools(RecordTools):
436
622
  return [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
437
623
  except QingflowApiError as exc:
438
624
  last_error = exc
439
- if exc.backend_code == 40002:
625
+ if _is_code_block_permission_error(exc):
440
626
  continue
441
627
  raise
442
628
  if last_error is not None:
443
629
  raise last_error
444
630
  raise_tool_error(QingflowApiError.config_error("record answers could not be loaded for code-block execution"))
445
631
 
446
- def _answers_to_open_match_values(self, answers: list[JSONObject], index: FieldIndex) -> list[JSONObject]:
632
+ def _answers_to_open_match_values(
633
+ self,
634
+ answers: list[JSONObject],
635
+ index: FieldIndex,
636
+ *,
637
+ exclude_que_ids: set[int] | None = None,
638
+ ) -> list[JSONObject]:
447
639
  """执行内部辅助逻辑。"""
448
640
  values: list[JSONObject] = []
449
641
  for answer in answers:
450
642
  if not isinstance(answer, dict):
451
643
  continue
644
+ que_id = _coerce_count(answer.get("queId", answer.get("que_id")))
645
+ if que_id is not None and exclude_que_ids is not None and que_id in exclude_que_ids:
646
+ continue
452
647
  open_match = self._answer_to_open_match_value(answer, index)
453
648
  if open_match is None:
454
649
  continue
@@ -507,8 +702,10 @@ class CodeBlockTools(RecordTools):
507
702
  self,
508
703
  context, # type: ignore[no-untyped-def]
509
704
  *,
705
+ profile: str,
510
706
  app_key: str,
511
707
  apply_id: int,
708
+ view_id: str | None,
512
709
  expected_answers: list[JSONObject],
513
710
  index: FieldIndex,
514
711
  role: int,
@@ -526,8 +723,10 @@ class CodeBlockTools(RecordTools):
526
723
  )
527
724
  actual_answers = self._load_record_answers_for_code_block(
528
725
  context,
726
+ profile=profile,
529
727
  app_key=app_key,
530
728
  apply_id=apply_id,
729
+ view_id=view_id,
531
730
  role=role,
532
731
  audit_node_id=audit_node_id,
533
732
  )
@@ -619,6 +818,42 @@ def _normalize_code_block_value_item(value: JSONValue, field: FormField) -> str
619
818
  return text if text is not None else None
620
819
 
621
820
 
821
+ def _code_block_transport_error(error: QingflowApiError) -> JSONObject:
822
+ payload: JSONObject = {
823
+ "category": error.category,
824
+ "message": error.message,
825
+ }
826
+ if error.backend_code is not None:
827
+ payload["backend_code"] = error.backend_code
828
+ if error.http_status is not None:
829
+ payload["http_status"] = error.http_status
830
+ if error.request_id:
831
+ payload["request_id"] = error.request_id
832
+ if error.details:
833
+ payload["details"] = error.details
834
+ return payload
835
+
836
+
837
+ def _code_block_failure_fields(error: JSONObject | None, *, context: str) -> JSONObject:
838
+ default_code = "CODE_BLOCK_RELATION_FAILED" if context == "relation" else "CODE_BLOCK_WRITEBACK_FAILED"
839
+ permission_code = "CODE_BLOCK_RELATION_PERMISSION_DENIED" if context == "relation" else "CODE_BLOCK_WRITEBACK_PERMISSION_DENIED"
840
+ payload: JSONObject = {
841
+ "error_code": default_code,
842
+ }
843
+ if not isinstance(error, dict):
844
+ return payload
845
+ category = str(error.get("category") or "").strip().lower()
846
+ http_status = backend_code_value_int(error.get("http_status"))
847
+ if category == "auth" or http_status == 401 or message_looks_like_invalid_token(error.get("message")):
848
+ payload["error_code"] = "AUTH_REQUIRED"
849
+ elif backend_code_value_int(error.get("backend_code")) in {40002, 40027}:
850
+ payload["error_code"] = permission_code
851
+ for key in ("category", "backend_code", "http_status", "request_id"):
852
+ if key in error:
853
+ payload[key] = error.get(key)
854
+ return payload
855
+
856
+
622
857
  def _selector_numeric_or_text(value: JSONValue, keys: tuple[str, ...], *, allow_text: bool) -> str | None:
623
858
  numeric = _coerce_count(value)
624
859
  if numeric is not None:
@@ -775,3 +1010,26 @@ def _relation_result_errors(items: list[JSONObject]) -> list[JSONObject]:
775
1010
  }
776
1011
  )
777
1012
  return errors
1013
+
1014
+
1015
+ def _is_optional_code_block_record_read_error(error: QingflowApiError) -> bool:
1016
+ if is_auth_like_error(error):
1017
+ return False
1018
+ backend_code = _code_block_backend_code(error)
1019
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
1020
+
1021
+
1022
+ def _is_optional_code_block_schema_error(error: QingflowApiError) -> bool:
1023
+ if is_auth_like_error(error):
1024
+ return False
1025
+ return _code_block_backend_code(error) in _CODE_BLOCK_SCHEMA_PERMISSION_CODES or error.http_status == 404
1026
+
1027
+
1028
+ def _is_code_block_permission_error(error: QingflowApiError) -> bool:
1029
+ if is_auth_like_error(error):
1030
+ return False
1031
+ return _code_block_backend_code(error) in {40002, 40027}
1032
+
1033
+
1034
+ def _code_block_backend_code(error: QingflowApiError) -> int | None:
1035
+ return backend_code_int(error)