@josephyan/qingflow-app-builder-mcp 1.1.24 → 1.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/docs/local-agent-install.md +6 -57
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
- package/npm/lib/runtime.mjs +101 -21
- package/npm/scripts/postinstall.mjs +10 -1
- package/package.json +2 -3
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +42 -104
- package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
- package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
- package/skills/qingflow-app-builder/references/create-app.md +11 -30
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +9 -31
- package/skills/qingflow-app-builder/references/match-rules.md +0 -15
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
- package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
- package/skills/qingflow-app-builder/references/update-flow.md +38 -22
- package/skills/qingflow-app-builder/references/update-schema.md +0 -14
- package/skills/qingflow-app-builder/references/update-views.md +14 -83
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +111 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +61 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-workflow-builder/SKILL.md +96 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +2 -6
- package/src/qingflow_mcp/builder_facade/models.py +108 -485
- package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
- package/src/qingflow_mcp/cli/commands/builder.py +364 -322
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +3 -12
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +27 -101
- package/src/qingflow_mcp/cli/commands/task.py +47 -28
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +16 -784
- package/src/qingflow_mcp/cli/main.py +33 -117
- package/src/qingflow_mcp/errors.py +2 -43
- package/src/qingflow_mcp/public_surface.py +17 -26
- package/src/qingflow_mcp/response_trim.py +17 -81
- package/src/qingflow_mcp/server.py +12 -14
- package/src/qingflow_mcp/server_app_builder.py +36 -79
- package/src/qingflow_mcp/server_app_user.py +16 -22
- package/src/qingflow_mcp/session_store.py +7 -11
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/executor.py +18 -245
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3100
- package/src/qingflow_mcp/tools/app_tools.py +43 -184
- package/src/qingflow_mcp/tools/approval_tools.py +35 -197
- package/src/qingflow_mcp/tools/auth_tools.py +16 -92
- package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
- package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
- package/src/qingflow_mcp/tools/directory_tools.py +72 -236
- package/src/qingflow_mcp/tools/export_tools.py +34 -244
- package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
- package/src/qingflow_mcp/tools/file_tools.py +3 -9
- package/src/qingflow_mcp/tools/import_tools.py +49 -336
- package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
- package/src/qingflow_mcp/tools/package_tools.py +6 -118
- package/src/qingflow_mcp/tools/portal_tools.py +3 -39
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
- package/src/qingflow_mcp/tools/record_tools.py +360 -1145
- package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
- package/src/qingflow_mcp/tools/role_tools.py +9 -80
- package/src/qingflow_mcp/tools/solution_tools.py +45 -59
- package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
- package/src/qingflow_mcp/tools/task_tools.py +29 -113
- package/src/qingflow_mcp/tools/view_tools.py +3 -106
- package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
- package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
- package/npm/bin/qingflow-skills.mjs +0 -5
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
- package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
|
@@ -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,
|
|
8
|
+
from ..errors import QingflowApiError, 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,11 +19,9 @@ from .record_tools import (
|
|
|
19
19
|
FieldIndex,
|
|
20
20
|
FormField,
|
|
21
21
|
RecordTools,
|
|
22
|
-
_build_answer_backed_field_index,
|
|
23
22
|
_coerce_count,
|
|
24
23
|
_collect_question_relations,
|
|
25
24
|
_field_ref_payload,
|
|
26
|
-
_merge_field_indexes,
|
|
27
25
|
_normalize_optional_text,
|
|
28
26
|
_relation_ids_from_answer,
|
|
29
27
|
_stringify_json,
|
|
@@ -33,7 +31,6 @@ from .record_tools import (
|
|
|
33
31
|
CODE_BLOCK_QUE_TYPE = 26
|
|
34
32
|
CODE_BLOCK_RELATION_TYPE = 3
|
|
35
33
|
SUPPORTED_CODE_BLOCK_ROLES = {1, 2, 3, 5}
|
|
36
|
-
_CODE_BLOCK_SCHEMA_PERMISSION_CODES = {40002, 40027, 404}
|
|
37
34
|
|
|
38
35
|
|
|
39
36
|
class CodeBlockTools(RecordTools):
|
|
@@ -68,48 +65,17 @@ class CodeBlockTools(RecordTools):
|
|
|
68
65
|
self._form_cache[cache_key] = normalized
|
|
69
66
|
return normalized
|
|
70
67
|
|
|
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
|
-
|
|
101
68
|
def register(self, mcp: FastMCP) -> None:
|
|
102
69
|
"""注册当前工具到 MCP 服务。"""
|
|
103
70
|
super().register(mcp)
|
|
104
71
|
|
|
105
72
|
@mcp.tool()
|
|
106
73
|
def record_code_block_schema_get(
|
|
107
|
-
profile: str = DEFAULT_PROFILE,
|
|
108
74
|
app_key: str = "",
|
|
109
75
|
output_profile: str = "normal",
|
|
110
76
|
) -> JSONObject:
|
|
111
77
|
return self.record_code_block_schema_get_public(
|
|
112
|
-
profile=
|
|
78
|
+
profile=DEFAULT_PROFILE,
|
|
113
79
|
app_key=app_key,
|
|
114
80
|
output_profile=output_profile,
|
|
115
81
|
)
|
|
@@ -118,8 +84,7 @@ class CodeBlockTools(RecordTools):
|
|
|
118
84
|
description=(
|
|
119
85
|
"Run a form code-block field against the current record data, parse alias results, and optionally "
|
|
120
86
|
"reuse Qingflow's existing relation-calculation chain to compute bound outputs and write them back. "
|
|
121
|
-
"Use record_code_block_schema_get
|
|
122
|
-
"if the exact code-block field id is known from record/task detail, run directly. "
|
|
87
|
+
"Use record_code_block_schema_get first and choose an exact code-block field selector. "
|
|
123
88
|
"For safe debugging, pass apply_writeback=false to inspect parsed results without writing back."
|
|
124
89
|
)
|
|
125
90
|
)
|
|
@@ -128,7 +93,6 @@ class CodeBlockTools(RecordTools):
|
|
|
128
93
|
app_key: str = "",
|
|
129
94
|
record_id: str = "",
|
|
130
95
|
code_block_field: str = "",
|
|
131
|
-
view_id: str | None = None,
|
|
132
96
|
role: int = 1,
|
|
133
97
|
workflow_node_id: int | None = None,
|
|
134
98
|
answers: list[JSONObject] | None = None,
|
|
@@ -144,7 +108,6 @@ class CodeBlockTools(RecordTools):
|
|
|
144
108
|
app_key=app_key,
|
|
145
109
|
record_id=record_id,
|
|
146
110
|
code_block_field=code_block_field,
|
|
147
|
-
view_id=view_id,
|
|
148
111
|
role=role,
|
|
149
112
|
workflow_node_id=workflow_node_id,
|
|
150
113
|
answers=answers or [],
|
|
@@ -170,45 +133,8 @@ class CodeBlockTools(RecordTools):
|
|
|
170
133
|
normalized_output_profile = self._normalize_public_output_profile(output_profile)
|
|
171
134
|
|
|
172
135
|
def runner(session_profile, context):
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
}
|
|
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)
|
|
212
138
|
input_fields = [
|
|
213
139
|
self._ready_schema_field_payload(
|
|
214
140
|
profile,
|
|
@@ -273,7 +199,6 @@ class CodeBlockTools(RecordTools):
|
|
|
273
199
|
app_key: str,
|
|
274
200
|
record_id: int | str,
|
|
275
201
|
code_block_field: str,
|
|
276
|
-
view_id: str | None = None,
|
|
277
202
|
role: int = 1,
|
|
278
203
|
workflow_node_id: int | None = None,
|
|
279
204
|
answers: list[JSONObject] | None = None,
|
|
@@ -295,42 +220,14 @@ class CodeBlockTools(RecordTools):
|
|
|
295
220
|
raise_tool_error(QingflowApiError.config_error("code_block_field is required"))
|
|
296
221
|
|
|
297
222
|
def runner(session_profile, context):
|
|
298
|
-
|
|
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(
|
|
223
|
+
relation_schema = self._get_code_block_relation_schema(
|
|
311
224
|
profile,
|
|
312
225
|
context,
|
|
313
226
|
app_key,
|
|
314
227
|
force_refresh=force_refresh_form,
|
|
315
|
-
warnings=warnings,
|
|
316
228
|
)
|
|
317
|
-
|
|
318
|
-
|
|
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)
|
|
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")
|
|
334
231
|
if code_block.que_type != CODE_BLOCK_QUE_TYPE:
|
|
335
232
|
raise_tool_error(
|
|
336
233
|
QingflowApiError(
|
|
@@ -344,6 +241,14 @@ class CodeBlockTools(RecordTools):
|
|
|
344
241
|
},
|
|
345
242
|
)
|
|
346
243
|
)
|
|
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
|
+
)
|
|
347
252
|
override_answers = (
|
|
348
253
|
self._resolve_answers(
|
|
349
254
|
profile,
|
|
@@ -352,13 +257,12 @@ class CodeBlockTools(RecordTools):
|
|
|
352
257
|
answers=answers or [],
|
|
353
258
|
fields=fields or {},
|
|
354
259
|
force_refresh_form=force_refresh_form,
|
|
355
|
-
field_index_override=index,
|
|
356
260
|
)
|
|
357
261
|
if answers or fields
|
|
358
262
|
else []
|
|
359
263
|
)
|
|
360
264
|
merged_answers = self._merge_record_answers(current_answers, override_answers) if override_answers else current_answers
|
|
361
|
-
key_que_values = self._answers_to_open_match_values(merged_answers, index
|
|
265
|
+
key_que_values = self._answers_to_open_match_values(merged_answers, index)
|
|
362
266
|
run_body: JSONObject = {
|
|
363
267
|
"role": role,
|
|
364
268
|
"manual": bool(manual),
|
|
@@ -385,7 +289,6 @@ class CodeBlockTools(RecordTools):
|
|
|
385
289
|
relation_items: list[JSONObject] = []
|
|
386
290
|
calculated_answers: list[JSONObject] = []
|
|
387
291
|
relation_result: JSONObject | None = None
|
|
388
|
-
relation_transport_error: JSONObject | None = None
|
|
389
292
|
if relation_target_fields:
|
|
390
293
|
relation_body: JSONObject = {
|
|
391
294
|
"role": role,
|
|
@@ -403,38 +306,26 @@ class CodeBlockTools(RecordTools):
|
|
|
403
306
|
relation_result = self.backend.request("POST", context, relation_route, json_body=relation_body)
|
|
404
307
|
relation_items = _relation_result_items(relation_result)
|
|
405
308
|
except QingflowApiError as exc:
|
|
406
|
-
if exc.http_status
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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:
|
|
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:
|
|
416
315
|
# Keep compatibility with legacy runtime deployments and lightweight test doubles
|
|
417
316
|
# that still stub the older relation-calculation route only.
|
|
418
317
|
relation_route = "/que/actuator"
|
|
419
|
-
|
|
420
|
-
|
|
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)
|
|
318
|
+
relation_result = self.backend.request("POST", context, relation_route, json_body=relation_body)
|
|
319
|
+
relation_items = _relation_result_items(relation_result)
|
|
425
320
|
relation_errors = _relation_result_errors(relation_items)
|
|
426
321
|
calculated_answers = _relation_result_answers(relation_items)
|
|
427
322
|
write_result: JSONObject | None = None
|
|
428
|
-
write_error: JSONObject | None = None
|
|
429
323
|
verification: JSONObject | None = None
|
|
430
324
|
writeback_attempted = False
|
|
431
325
|
writeback_applied = False
|
|
432
326
|
status = "completed"
|
|
433
327
|
ok = True
|
|
434
|
-
if
|
|
435
|
-
status = "relation_failed"
|
|
436
|
-
ok = False
|
|
437
|
-
elif relation_errors:
|
|
328
|
+
if relation_errors:
|
|
438
329
|
status = "relation_failed"
|
|
439
330
|
ok = False
|
|
440
331
|
elif not apply_writeback:
|
|
@@ -444,28 +335,21 @@ class CodeBlockTools(RecordTools):
|
|
|
444
335
|
if workflow_node_id is not None:
|
|
445
336
|
write_body["auditNodeId"] = workflow_node_id
|
|
446
337
|
writeback_attempted = True
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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:
|
|
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:
|
|
463
349
|
verification = self._verify_code_block_writeback_result(
|
|
464
350
|
context,
|
|
465
|
-
profile=profile,
|
|
466
351
|
app_key=app_key,
|
|
467
352
|
apply_id=normalized_record_id,
|
|
468
|
-
view_id=view_id,
|
|
469
353
|
expected_answers=calculated_answers,
|
|
470
354
|
index=index,
|
|
471
355
|
role=role,
|
|
@@ -473,16 +357,7 @@ class CodeBlockTools(RecordTools):
|
|
|
473
357
|
)
|
|
474
358
|
if not bool(verification.get("verified")):
|
|
475
359
|
status = "verification_failed"
|
|
476
|
-
ok =
|
|
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
|
-
)
|
|
360
|
+
ok = False
|
|
486
361
|
else:
|
|
487
362
|
status = "no_writeback"
|
|
488
363
|
response: JSONObject = {
|
|
@@ -494,14 +369,9 @@ class CodeBlockTools(RecordTools):
|
|
|
494
369
|
"apply_id": normalized_record_id,
|
|
495
370
|
"status": status,
|
|
496
371
|
"ok": ok,
|
|
497
|
-
"write_executed": writeback_applied,
|
|
498
|
-
"write_succeeded": writeback_applied,
|
|
499
|
-
"safe_to_retry": not writeback_applied,
|
|
500
|
-
"warnings": warnings,
|
|
501
372
|
"code_block_field": _field_ref_payload(code_block),
|
|
502
373
|
"execution": {
|
|
503
374
|
"executed": True,
|
|
504
|
-
"view_id": _normalize_optional_text(view_id),
|
|
505
375
|
"role": role,
|
|
506
376
|
"workflow_node_id": workflow_node_id,
|
|
507
377
|
"manual": bool(manual),
|
|
@@ -519,7 +389,6 @@ class CodeBlockTools(RecordTools):
|
|
|
519
389
|
"calculated_answer_count": len(calculated_answers),
|
|
520
390
|
"calculated_answers_preview": calculated_answers,
|
|
521
391
|
"errors": relation_errors,
|
|
522
|
-
"transport_error": relation_transport_error,
|
|
523
392
|
},
|
|
524
393
|
"writeback": {
|
|
525
394
|
"enabled": bool(apply_writeback),
|
|
@@ -529,15 +398,10 @@ class CodeBlockTools(RecordTools):
|
|
|
529
398
|
"verify_writeback": verify_writeback,
|
|
530
399
|
"write_verified": bool(verification.get("verified")) if verification is not None else None,
|
|
531
400
|
"result": write_result,
|
|
532
|
-
"error": write_error,
|
|
533
401
|
"verification": verification,
|
|
534
402
|
},
|
|
535
403
|
"resource": {"apply_id": normalized_record_id},
|
|
536
404
|
}
|
|
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))
|
|
541
405
|
if normalized_output_profile == "verbose":
|
|
542
406
|
response["debug"] = {
|
|
543
407
|
"run_body": run_body,
|
|
@@ -551,66 +415,16 @@ class CodeBlockTools(RecordTools):
|
|
|
551
415
|
|
|
552
416
|
return self._run_record_tool(profile, runner)
|
|
553
417
|
|
|
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
|
-
|
|
576
418
|
def _load_record_answers_for_code_block(
|
|
577
419
|
self,
|
|
578
420
|
context, # type: ignore[no-untyped-def]
|
|
579
421
|
*,
|
|
580
|
-
profile: str,
|
|
581
422
|
app_key: str,
|
|
582
423
|
apply_id: int,
|
|
583
|
-
view_id: str | None,
|
|
584
424
|
role: int,
|
|
585
425
|
audit_node_id: int | None,
|
|
586
426
|
) -> list[JSONObject]:
|
|
587
427
|
"""执行内部辅助逻辑。"""
|
|
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
|
-
|
|
614
428
|
last_error: QingflowApiError | None = None
|
|
615
429
|
for list_type in self._INTERNAL_GET_LIST_TYPE_FALLBACKS:
|
|
616
430
|
params: JSONObject = {"role": role, "listType": list_type}
|
|
@@ -622,28 +436,19 @@ class CodeBlockTools(RecordTools):
|
|
|
622
436
|
return [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
|
|
623
437
|
except QingflowApiError as exc:
|
|
624
438
|
last_error = exc
|
|
625
|
-
if
|
|
439
|
+
if exc.backend_code == 40002:
|
|
626
440
|
continue
|
|
627
441
|
raise
|
|
628
442
|
if last_error is not None:
|
|
629
443
|
raise last_error
|
|
630
444
|
raise_tool_error(QingflowApiError.config_error("record answers could not be loaded for code-block execution"))
|
|
631
445
|
|
|
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]:
|
|
446
|
+
def _answers_to_open_match_values(self, answers: list[JSONObject], index: FieldIndex) -> list[JSONObject]:
|
|
639
447
|
"""执行内部辅助逻辑。"""
|
|
640
448
|
values: list[JSONObject] = []
|
|
641
449
|
for answer in answers:
|
|
642
450
|
if not isinstance(answer, dict):
|
|
643
451
|
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
|
|
647
452
|
open_match = self._answer_to_open_match_value(answer, index)
|
|
648
453
|
if open_match is None:
|
|
649
454
|
continue
|
|
@@ -702,10 +507,8 @@ class CodeBlockTools(RecordTools):
|
|
|
702
507
|
self,
|
|
703
508
|
context, # type: ignore[no-untyped-def]
|
|
704
509
|
*,
|
|
705
|
-
profile: str,
|
|
706
510
|
app_key: str,
|
|
707
511
|
apply_id: int,
|
|
708
|
-
view_id: str | None,
|
|
709
512
|
expected_answers: list[JSONObject],
|
|
710
513
|
index: FieldIndex,
|
|
711
514
|
role: int,
|
|
@@ -723,10 +526,8 @@ class CodeBlockTools(RecordTools):
|
|
|
723
526
|
)
|
|
724
527
|
actual_answers = self._load_record_answers_for_code_block(
|
|
725
528
|
context,
|
|
726
|
-
profile=profile,
|
|
727
529
|
app_key=app_key,
|
|
728
530
|
apply_id=apply_id,
|
|
729
|
-
view_id=view_id,
|
|
730
531
|
role=role,
|
|
731
532
|
audit_node_id=audit_node_id,
|
|
732
533
|
)
|
|
@@ -818,42 +619,6 @@ def _normalize_code_block_value_item(value: JSONValue, field: FormField) -> str
|
|
|
818
619
|
return text if text is not None else None
|
|
819
620
|
|
|
820
621
|
|
|
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
|
-
|
|
857
622
|
def _selector_numeric_or_text(value: JSONValue, keys: tuple[str, ...], *, allow_text: bool) -> str | None:
|
|
858
623
|
numeric = _coerce_count(value)
|
|
859
624
|
if numeric is not None:
|
|
@@ -1010,26 +775,3 @@ def _relation_result_errors(items: list[JSONObject]) -> list[JSONObject]:
|
|
|
1010
775
|
}
|
|
1011
776
|
)
|
|
1012
777
|
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)
|