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