@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.
- package/README.md +7 -3
- package/docs/local-agent-install.md +57 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/bin/qingflow.mjs +1 -34
- package/npm/lib/runtime.mjs +21 -101
- package/npm/scripts/postinstall.mjs +1 -10
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +58 -44
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
- package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
- package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
- package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
- package/skills/qingflow-cli/reference/builder/README.md +41 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
- package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
- package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
- package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
- package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
- package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
- package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
- package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
- package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
- package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
- package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
- package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
- package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
- package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
- package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
- package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
- package/skills/qingflow-mcp-setup/SKILL.md +115 -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/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +282 -102
- package/src/qingflow_mcp/builder_facade/service.py +4192 -935
- package/src/qingflow_mcp/cli/commands/builder.py +316 -298
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- 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 +101 -27
- package/src/qingflow_mcp/cli/commands/task.py +28 -47
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +784 -16
- package/src/qingflow_mcp/cli/main.py +117 -33
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +26 -17
- package/src/qingflow_mcp/response_trim.py +81 -17
- package/src/qingflow_mcp/server.py +14 -12
- package/src/qingflow_mcp/server_app_builder.py +65 -21
- package/src/qingflow_mcp/server_app_user.py +22 -16
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/executor.py +245 -18
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +197 -35
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- 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 +244 -34
- package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
- package/src/qingflow_mcp/tools/file_tools.py +9 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- 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 +1141 -356
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +59 -45
- package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
- 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 +48 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
- /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
- /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=
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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)
|