@josephyan/qingflow-app-builder-mcp 0.2.0-beta.4 → 0.2.0-beta.41
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 +3 -3
- package/package.json +1 -1
- package/pyproject.toml +3 -1
- package/skills/qingflow-app-builder/SKILL.md +154 -22
- package/skills/qingflow-app-builder/references/create-app.md +51 -21
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +28 -1
- package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
- package/skills/qingflow-app-builder/references/tool-selection.md +45 -17
- package/skills/qingflow-app-builder/references/update-flow.md +112 -25
- package/skills/qingflow-app-builder/references/update-layout.md +11 -24
- package/skills/qingflow-app-builder/references/update-schema.md +1 -23
- package/skills/qingflow-app-builder/references/update-views.md +87 -21
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +189 -0
- package/src/qingflow_mcp/builder_facade/models.py +584 -1
- package/src/qingflow_mcp/builder_facade/service.py +4698 -262
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/list_type_labels.py +24 -0
- package/src/qingflow_mcp/server.py +131 -16
- package/src/qingflow_mcp/server_app_builder.py +132 -72
- package/src/qingflow_mcp/server_app_user.py +143 -187
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +14 -4
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
- package/src/qingflow_mcp/solution/executor.py +44 -7
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1567 -144
- package/src/qingflow_mcp/tools/app_tools.py +243 -14
- package/src/qingflow_mcp/tools/approval_tools.py +411 -76
- package/src/qingflow_mcp/tools/directory_tools.py +203 -31
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +1 -0
- package/src/qingflow_mcp/tools/import_tools.py +1164 -0
- package/src/qingflow_mcp/tools/portal_tools.py +31 -0
- package/src/qingflow_mcp/tools/record_tools.py +4943 -1025
- package/src/qingflow_mcp/tools/task_context_tools.py +1335 -0
- package/src/qingflow_mcp/tools/task_tools.py +376 -225
- package/src/qingflow_mcp/tools/workflow_tools.py +78 -4
|
@@ -1,34 +1,153 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
3
5
|
from mcp.server.fastmcp import FastMCP
|
|
4
6
|
|
|
5
7
|
from .backend_client import BackendClient
|
|
6
8
|
from .config import DEFAULT_PROFILE
|
|
7
9
|
from .session_store import SessionStore
|
|
8
|
-
from .tools.
|
|
10
|
+
from .tools.app_tools import AppTools
|
|
9
11
|
from .tools.auth_tools import AuthTools
|
|
10
12
|
from .tools.directory_tools import DirectoryTools
|
|
13
|
+
from .tools.feedback_tools import FeedbackTools
|
|
11
14
|
from .tools.file_tools import FileTools
|
|
15
|
+
from .tools.import_tools import ImportTools
|
|
12
16
|
from .tools.record_tools import RecordTools
|
|
13
|
-
from .tools.
|
|
17
|
+
from .tools.task_context_tools import TaskContextTools
|
|
14
18
|
from .tools.workspace_tools import WorkspaceTools
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
def build_user_server() -> FastMCP:
|
|
22
|
+
today = date.today()
|
|
23
|
+
current_year = today.year
|
|
18
24
|
server = FastMCP(
|
|
19
25
|
"Qingflow App User MCP",
|
|
20
|
-
instructions=(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.
|
|
27
|
+
|
|
28
|
+
## App Discovery
|
|
29
|
+
|
|
30
|
+
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
31
|
+
If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
|
|
32
|
+
If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
|
|
33
|
+
|
|
34
|
+
## Shared Helper
|
|
35
|
+
|
|
36
|
+
`feedback_submit` is always available as a cross-cutting helper.
|
|
37
|
+
|
|
38
|
+
- Use it when the current MCP capability is unsupported, awkward, or still cannot satisfy the user's need after reasonable use.
|
|
39
|
+
- It does not require Qingflow login or workspace selection.
|
|
40
|
+
- Call it only after the user explicitly confirms submission.
|
|
41
|
+
|
|
42
|
+
## Schema-First Rule
|
|
43
|
+
|
|
44
|
+
Call `record_schema_get(schema_mode="applicant")` before `record_write`.
|
|
45
|
+
Call `app_get` first when the data range is unclear, then use `record_schema_get(schema_mode="browse", view_id=...)` before `record_list`, `record_get`, or `record_analyze`.
|
|
46
|
+
|
|
47
|
+
- All `field_id` values must come from the schema response.
|
|
48
|
+
- Never guess field names or ids.
|
|
49
|
+
|
|
50
|
+
## Schema Scope
|
|
51
|
+
|
|
52
|
+
`record_schema_get(schema_mode="applicant")` returns the current user's applicant-node visible fields for write/create.
|
|
53
|
+
`record_schema_get(schema_mode="browse", view_id=...)` returns browse-schema fields for the selected accessible view.
|
|
54
|
+
|
|
55
|
+
- Hidden fields are omitted.
|
|
56
|
+
- Missing fields mean the field is not visible in the current permission scope.
|
|
57
|
+
- Read `fields` and `suggested_*` from the top level of the schema response.
|
|
58
|
+
|
|
59
|
+
## Analytics Path
|
|
60
|
+
|
|
61
|
+
`app_get -> record_schema_get(schema_mode="browse", view_id=...) -> record_analyze`
|
|
62
|
+
|
|
63
|
+
Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
|
|
64
|
+
|
|
65
|
+
Use this DSL shape:
|
|
66
|
+
|
|
67
|
+
- `dimensions`: `{{field_id, alias, bucket}}`
|
|
68
|
+
- `metrics`: `{{op, field_id, alias}}`
|
|
69
|
+
- `filters`: `{{field_id, op, value}}`
|
|
70
|
+
- `sort`: `{{by, order}}`
|
|
71
|
+
|
|
72
|
+
Important key rules:
|
|
73
|
+
|
|
74
|
+
- Use `op`
|
|
75
|
+
- Do **not** use `type`
|
|
76
|
+
- Do **not** use `agg`
|
|
77
|
+
- Do **not** use `aggregation`
|
|
78
|
+
- Do **not** use `operator`
|
|
79
|
+
|
|
80
|
+
Analysis answers must include concrete numbers. When applicable, include percentages based on the returned totals.
|
|
81
|
+
|
|
82
|
+
## Record CRUD Path
|
|
83
|
+
|
|
84
|
+
`app_get -> record_schema_get(schema_mode="browse", view_id=...) -> record_list / record_get`
|
|
85
|
+
`record_schema_get(schema_mode="applicant") -> record_write`
|
|
86
|
+
|
|
87
|
+
- Use `columns` as `[{{field_id}}]`
|
|
88
|
+
- Use `where` items as `{{field_id, op, value}}`
|
|
89
|
+
- Use `order_by` items as `{{field_id, direction}}`
|
|
90
|
+
- Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
|
|
91
|
+
|
|
92
|
+
`record_write` uses SQL-like JSON clauses:
|
|
93
|
+
|
|
94
|
+
- `insert` -> `values`
|
|
95
|
+
- `update` -> `record_id + set`
|
|
96
|
+
- `delete` -> `record_id` or `record_ids`
|
|
97
|
+
|
|
98
|
+
- Read relation targets from `record_schema_get.target_app_key` / `target_app_name` before preparing relation writes.
|
|
99
|
+
- If a member or department field id is known but candidate ids are not, use `record_member_candidates` or `record_department_candidates` before `record_write`.
|
|
100
|
+
- For default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
|
|
101
|
+
|
|
102
|
+
## Import Path
|
|
103
|
+
|
|
104
|
+
`record_import_template_get -> record_import_verify -> (optional authorized record_import_repair_local) -> record_import_start -> record_import_status_get`
|
|
105
|
+
|
|
106
|
+
- Import must go through `verify -> start`; do not start directly from a raw file path.
|
|
107
|
+
- `record_import_start` requires an explicit `being_enter_auditing` choice. Do not assume a default.
|
|
108
|
+
- Do not modify user-uploaded files unless the user explicitly authorizes repair.
|
|
109
|
+
- If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.
|
|
110
|
+
|
|
111
|
+
## Task Workflow Path
|
|
112
|
+
|
|
113
|
+
`task_list -> task_get -> task_action_execute`
|
|
114
|
+
|
|
115
|
+
- Use `task_associated_report_detail_get` for associated view or report details.
|
|
116
|
+
- Use `task_workflow_log_get` for full workflow log history.
|
|
117
|
+
- Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
|
|
118
|
+
|
|
119
|
+
## Time Handling
|
|
120
|
+
|
|
121
|
+
Normalize relative dates before building DSL.
|
|
122
|
+
|
|
123
|
+
- If the user says `3月` without a year, use the current year: `{current_year}`
|
|
124
|
+
- Convert month-only phrases into explicit legal date ranges
|
|
125
|
+
- Never send impossible dates such as `2026-02-29`
|
|
126
|
+
|
|
127
|
+
## Environment
|
|
128
|
+
|
|
129
|
+
Default to `prod` unless the user explicitly specifies `test`.
|
|
130
|
+
|
|
131
|
+
## Constraints
|
|
132
|
+
|
|
133
|
+
Avoid builder-side app or schema changes here.
|
|
134
|
+
|
|
135
|
+
## Feedback Path
|
|
136
|
+
|
|
137
|
+
If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, offer to submit product feedback.
|
|
138
|
+
|
|
139
|
+
- First summarize what is still not working
|
|
140
|
+
- Ask the user whether to submit feedback
|
|
141
|
+
- Call `feedback_submit` only after explicit user confirmation""",
|
|
24
142
|
)
|
|
25
143
|
sessions = SessionStore()
|
|
26
144
|
backend = BackendClient()
|
|
27
145
|
auth = AuthTools(sessions, backend)
|
|
146
|
+
apps = AppTools(sessions, backend)
|
|
28
147
|
workspace = WorkspaceTools(sessions, backend)
|
|
29
148
|
files = FileTools(sessions, backend)
|
|
30
|
-
|
|
31
|
-
|
|
149
|
+
imports = ImportTools(sessions, backend)
|
|
150
|
+
feedback = FeedbackTools(backend, mcp_side="App User MCP")
|
|
32
151
|
|
|
33
152
|
@server.tool()
|
|
34
153
|
def auth_login(
|
|
@@ -92,6 +211,18 @@ def build_user_server() -> FastMCP:
|
|
|
92
211
|
def workspace_select(profile: str = DEFAULT_PROFILE, ws_id: int = 0) -> dict:
|
|
93
212
|
return workspace.workspace_select(profile=profile, ws_id=ws_id)
|
|
94
213
|
|
|
214
|
+
@server.tool()
|
|
215
|
+
def app_list(profile: str = DEFAULT_PROFILE) -> dict:
|
|
216
|
+
return apps.app_list(profile=profile)
|
|
217
|
+
|
|
218
|
+
@server.tool()
|
|
219
|
+
def app_search(profile: str = DEFAULT_PROFILE, keyword: str = "", page_num: int = 1, page_size: int = 50) -> dict:
|
|
220
|
+
return apps.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
|
|
221
|
+
|
|
222
|
+
@server.tool()
|
|
223
|
+
def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
|
|
224
|
+
return apps.app_get(profile=profile, app_key=app_key)
|
|
225
|
+
|
|
95
226
|
@server.tool()
|
|
96
227
|
def file_get_upload_info(
|
|
97
228
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -138,188 +269,13 @@ def build_user_server() -> FastMCP:
|
|
|
138
269
|
file_related_url=file_related_url,
|
|
139
270
|
)
|
|
140
271
|
|
|
272
|
+
imports.register(server)
|
|
273
|
+
|
|
274
|
+
feedback.register(server)
|
|
141
275
|
RecordTools(sessions, backend).register(server)
|
|
276
|
+
TaskContextTools(sessions, backend).register(server)
|
|
142
277
|
DirectoryTools(sessions, backend).register(server)
|
|
143
278
|
|
|
144
|
-
@server.tool()
|
|
145
|
-
def record_comment_add(
|
|
146
|
-
profile: str = DEFAULT_PROFILE,
|
|
147
|
-
app_key: str = "",
|
|
148
|
-
apply_id: int = 0,
|
|
149
|
-
payload: dict | None = None,
|
|
150
|
-
) -> dict:
|
|
151
|
-
return approvals.record_comment_add(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
152
|
-
|
|
153
|
-
@server.tool()
|
|
154
|
-
def record_comment_list(
|
|
155
|
-
profile: str = DEFAULT_PROFILE,
|
|
156
|
-
app_key: str = "",
|
|
157
|
-
apply_id: int = 0,
|
|
158
|
-
page_size: int = 20,
|
|
159
|
-
list_type: int | None = None,
|
|
160
|
-
page_num: int | None = 1,
|
|
161
|
-
) -> dict:
|
|
162
|
-
return approvals.record_comment_list(
|
|
163
|
-
profile=profile,
|
|
164
|
-
app_key=app_key,
|
|
165
|
-
apply_id=apply_id,
|
|
166
|
-
page_size=page_size,
|
|
167
|
-
list_type=list_type,
|
|
168
|
-
page_num=page_num,
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
@server.tool()
|
|
172
|
-
def record_comment_mention_candidates(
|
|
173
|
-
profile: str = DEFAULT_PROFILE,
|
|
174
|
-
app_key: str = "",
|
|
175
|
-
apply_id: int = 0,
|
|
176
|
-
page_size: int = 20,
|
|
177
|
-
page_num: int = 1,
|
|
178
|
-
list_type: int | None = None,
|
|
179
|
-
keyword: str | None = None,
|
|
180
|
-
) -> dict:
|
|
181
|
-
return approvals.record_comment_mention_candidates(
|
|
182
|
-
profile=profile,
|
|
183
|
-
app_key=app_key,
|
|
184
|
-
apply_id=apply_id,
|
|
185
|
-
page_size=page_size,
|
|
186
|
-
page_num=page_num,
|
|
187
|
-
list_type=list_type,
|
|
188
|
-
keyword=keyword,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
@server.tool()
|
|
192
|
-
def record_comment_mark_read(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> dict:
|
|
193
|
-
return approvals.record_comment_mark_read(profile=profile, app_key=app_key, apply_id=apply_id)
|
|
194
|
-
|
|
195
|
-
@server.tool()
|
|
196
|
-
def record_comment_stats(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> dict:
|
|
197
|
-
return approvals.record_comment_stats(profile=profile, app_key=app_key, apply_id=apply_id)
|
|
198
|
-
|
|
199
|
-
@server.tool()
|
|
200
|
-
def task_list(
|
|
201
|
-
profile: str = DEFAULT_PROFILE,
|
|
202
|
-
type: int = 1,
|
|
203
|
-
process_status: int = 1,
|
|
204
|
-
app_key: str | None = None,
|
|
205
|
-
node_id: int | None = None,
|
|
206
|
-
search_key: str | None = None,
|
|
207
|
-
page_num: int = 1,
|
|
208
|
-
page_size: int = 20,
|
|
209
|
-
create_time_asc: bool | None = None,
|
|
210
|
-
) -> dict:
|
|
211
|
-
return tasks.task_list(
|
|
212
|
-
profile=profile,
|
|
213
|
-
type=type,
|
|
214
|
-
process_status=process_status,
|
|
215
|
-
app_key=app_key,
|
|
216
|
-
node_id=node_id,
|
|
217
|
-
search_key=search_key,
|
|
218
|
-
page_num=page_num,
|
|
219
|
-
page_size=page_size,
|
|
220
|
-
create_time_asc=create_time_asc,
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
@server.tool()
|
|
224
|
-
def task_list_grouped(
|
|
225
|
-
profile: str = DEFAULT_PROFILE,
|
|
226
|
-
type: int = 1,
|
|
227
|
-
process_status: int = 1,
|
|
228
|
-
app_key: str | None = None,
|
|
229
|
-
node_id: int | None = None,
|
|
230
|
-
search_key: str | None = None,
|
|
231
|
-
page_num: int = 1,
|
|
232
|
-
page_size: int = 20,
|
|
233
|
-
) -> dict:
|
|
234
|
-
return tasks.task_list_grouped(
|
|
235
|
-
profile=profile,
|
|
236
|
-
type=type,
|
|
237
|
-
process_status=process_status,
|
|
238
|
-
app_key=app_key,
|
|
239
|
-
node_id=node_id,
|
|
240
|
-
search_key=search_key,
|
|
241
|
-
page_num=page_num,
|
|
242
|
-
page_size=page_size,
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
@server.tool()
|
|
246
|
-
def task_statistics(profile: str = DEFAULT_PROFILE, app_key: str | None = None) -> dict:
|
|
247
|
-
return tasks.task_statistics(profile=profile, app_key=app_key)
|
|
248
|
-
|
|
249
|
-
@server.tool()
|
|
250
|
-
def task_urge(profile: str = DEFAULT_PROFILE, app_key: str = "", row_record_id: int = 0) -> dict:
|
|
251
|
-
return tasks.task_urge(profile=profile, app_key=app_key, row_record_id=row_record_id)
|
|
252
|
-
|
|
253
|
-
@server.tool(description=approvals._high_risk_tool_description(operation="approve", target="workflow task"))
|
|
254
|
-
def task_approve(
|
|
255
|
-
profile: str = DEFAULT_PROFILE,
|
|
256
|
-
app_key: str = "",
|
|
257
|
-
apply_id: int = 0,
|
|
258
|
-
payload: dict | None = None,
|
|
259
|
-
) -> dict:
|
|
260
|
-
return approvals.record_approve(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
261
|
-
|
|
262
|
-
@server.tool(description=approvals._high_risk_tool_description(operation="reject", target="workflow task"))
|
|
263
|
-
def task_reject(
|
|
264
|
-
profile: str = DEFAULT_PROFILE,
|
|
265
|
-
app_key: str = "",
|
|
266
|
-
apply_id: int = 0,
|
|
267
|
-
payload: dict | None = None,
|
|
268
|
-
) -> dict:
|
|
269
|
-
return approvals.record_reject(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
270
|
-
|
|
271
|
-
@server.tool()
|
|
272
|
-
def task_rollback_candidates(
|
|
273
|
-
profile: str = DEFAULT_PROFILE,
|
|
274
|
-
app_key: str = "",
|
|
275
|
-
apply_id: int = 0,
|
|
276
|
-
audit_node_id: int = 0,
|
|
277
|
-
) -> dict:
|
|
278
|
-
return approvals.record_rollback_candidates(
|
|
279
|
-
profile=profile,
|
|
280
|
-
app_key=app_key,
|
|
281
|
-
apply_id=apply_id,
|
|
282
|
-
audit_node_id=audit_node_id,
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
@server.tool()
|
|
286
|
-
def task_rollback(
|
|
287
|
-
profile: str = DEFAULT_PROFILE,
|
|
288
|
-
app_key: str = "",
|
|
289
|
-
apply_id: int = 0,
|
|
290
|
-
payload: dict | None = None,
|
|
291
|
-
) -> dict:
|
|
292
|
-
return approvals.record_rollback(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
293
|
-
|
|
294
|
-
@server.tool()
|
|
295
|
-
def task_transfer_candidates(
|
|
296
|
-
profile: str = DEFAULT_PROFILE,
|
|
297
|
-
app_key: str = "",
|
|
298
|
-
apply_id: int = 0,
|
|
299
|
-
page_size: int = 20,
|
|
300
|
-
page_num: int = 1,
|
|
301
|
-
audit_node_id: int = 0,
|
|
302
|
-
keyword: str | None = None,
|
|
303
|
-
) -> dict:
|
|
304
|
-
return approvals.record_transfer_candidates(
|
|
305
|
-
profile=profile,
|
|
306
|
-
app_key=app_key,
|
|
307
|
-
apply_id=apply_id,
|
|
308
|
-
page_size=page_size,
|
|
309
|
-
page_num=page_num,
|
|
310
|
-
audit_node_id=audit_node_id,
|
|
311
|
-
keyword=keyword,
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
@server.tool()
|
|
315
|
-
def task_transfer(
|
|
316
|
-
profile: str = DEFAULT_PROFILE,
|
|
317
|
-
app_key: str = "",
|
|
318
|
-
apply_id: int = 0,
|
|
319
|
-
payload: dict | None = None,
|
|
320
|
-
) -> dict:
|
|
321
|
-
return approvals.record_transfer(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
|
|
322
|
-
|
|
323
279
|
return server
|
|
324
280
|
|
|
325
281
|
|
|
@@ -249,7 +249,7 @@ def build_question(field: dict[str, Any], temp_id: int) -> tuple[dict[str, Any],
|
|
|
249
249
|
sub_question, next_temp_id = build_question(subfield, next_temp_id)
|
|
250
250
|
sub_questions.append(sub_question)
|
|
251
251
|
question["subQuestions"] = sub_questions
|
|
252
|
-
question["innerQuestions"] = deepcopy(sub_questions)
|
|
252
|
+
question["innerQuestions"] = [deepcopy(sub_questions)]
|
|
253
253
|
question["queDefaultValues"] = {"queId": temp_id, "queTitle": field["label"], "queType": que_type, "values": [], "tableValues": []}
|
|
254
254
|
return question, next_temp_id
|
|
255
255
|
|
|
@@ -257,13 +257,16 @@ def build_question(field: dict[str, Any], temp_id: int) -> tuple[dict[str, Any],
|
|
|
257
257
|
def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any]:
|
|
258
258
|
config = field.get("config") or {}
|
|
259
259
|
display_field_id = field.get("target_field_id") or "title"
|
|
260
|
+
display_field_que_id = field.get("target_field_que_id") or config.get("target_field_que_id") or 0
|
|
260
261
|
display_field_label = config.get("target_field_label") or "__TARGET_FIELD_LABEL__"
|
|
261
262
|
refer_field_ids = list(config.get("refer_field_ids") or [display_field_id])
|
|
263
|
+
refer_field_que_ids = list(config.get("refer_field_que_ids") or [])
|
|
262
264
|
refer_field_labels = config.get("refer_field_labels") or [display_field_label]
|
|
263
265
|
refer_field_types = config.get("refer_field_types")
|
|
264
266
|
refer_questions = []
|
|
265
267
|
for ordinal, field_id in enumerate(refer_field_ids, start=1):
|
|
266
268
|
label = refer_field_labels[ordinal - 1] if ordinal - 1 < len(refer_field_labels) else field_id
|
|
269
|
+
que_id = refer_field_que_ids[ordinal - 1] if ordinal - 1 < len(refer_field_que_ids) else 0
|
|
267
270
|
raw_type = _reference_field_type_value(
|
|
268
271
|
refer_field_types,
|
|
269
272
|
field_id=field_id,
|
|
@@ -272,7 +275,7 @@ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any
|
|
|
272
275
|
)
|
|
273
276
|
refer_questions.append(
|
|
274
277
|
{
|
|
275
|
-
"queId":
|
|
278
|
+
"queId": que_id,
|
|
276
279
|
"queTitle": label,
|
|
277
280
|
"queType": _normalize_reference_que_type(raw_type) or "2",
|
|
278
281
|
"queAuth": 1,
|
|
@@ -282,15 +285,22 @@ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any
|
|
|
282
285
|
}
|
|
283
286
|
)
|
|
284
287
|
auth_field_ids = list(config.get("auth_field_ids") or refer_field_ids)
|
|
288
|
+
auth_field_que_ids = list(config.get("auth_field_que_ids") or [])
|
|
289
|
+
auth_ques = deepcopy(config.get("refer_auth_ques") or [])
|
|
290
|
+
if not auth_ques:
|
|
291
|
+
auth_ques = []
|
|
292
|
+
for ordinal, field_id in enumerate(auth_field_ids, start=1):
|
|
293
|
+
que_id = auth_field_que_ids[ordinal - 1] if ordinal - 1 < len(auth_field_que_ids) else 0
|
|
294
|
+
auth_ques.append({"queId": que_id, "queAuth": 1, "_field_id": field_id})
|
|
285
295
|
return {
|
|
286
296
|
"referAppKey": "__TARGET_APP_KEY__",
|
|
287
|
-
"referQueId":
|
|
297
|
+
"referQueId": display_field_que_id,
|
|
288
298
|
"customButtonText": config.get("custom_button_text") or "选择数据",
|
|
289
299
|
"beingTableSource": False,
|
|
290
300
|
"referQuestions": refer_questions,
|
|
291
301
|
"referMatchRules": deepcopy(config.get("refer_match_rules") or []),
|
|
292
302
|
"referFillRules": deepcopy(config.get("refer_fill_rules") or []),
|
|
293
|
-
"referAuthQues":
|
|
303
|
+
"referAuthQues": auth_ques,
|
|
294
304
|
"canAddData": bool(config.get("can_add_data", False)),
|
|
295
305
|
"dataAdditionButtonText": config.get("data_addition_button_text") or "新增数据",
|
|
296
306
|
"canViewProcessLog": bool(config.get("can_view_process_log", True)),
|
|
@@ -36,6 +36,11 @@ def compile_workflow(entity: EntitySpec) -> dict[str, Any] | None:
|
|
|
36
36
|
actions: list[dict[str, Any]] = []
|
|
37
37
|
seen_node_ids: set[str] = set()
|
|
38
38
|
created_extra_branch_lanes: set[str] = set()
|
|
39
|
+
start_node_ids = {
|
|
40
|
+
node.node_id
|
|
41
|
+
for node in workflow.nodes
|
|
42
|
+
if node.node_type == WorkflowNodeType.start
|
|
43
|
+
}
|
|
39
44
|
for node in workflow.nodes:
|
|
40
45
|
if node.node_type == WorkflowNodeType.start:
|
|
41
46
|
seen_node_ids.add(node.node_id)
|
|
@@ -58,6 +63,26 @@ def compile_workflow(entity: EntitySpec) -> dict[str, Any] | None:
|
|
|
58
63
|
}
|
|
59
64
|
)
|
|
60
65
|
created_extra_branch_lanes.add(branch_lane_ref)
|
|
66
|
+
lane_only = bool((node.config or {}).get("__lane_only__")) and node.node_type == WorkflowNodeType.condition and branch_lane_ref
|
|
67
|
+
if lane_only:
|
|
68
|
+
lane_payload = {key: value for key, value in node.config.items() if key != "__lane_only__"}
|
|
69
|
+
actions.append(
|
|
70
|
+
{
|
|
71
|
+
"action": "update_node",
|
|
72
|
+
"node_id": branch_lane_ref,
|
|
73
|
+
"node_name": node.name,
|
|
74
|
+
"node_type": node.node_type.value,
|
|
75
|
+
"payload": {
|
|
76
|
+
"editVersionNo": 1,
|
|
77
|
+
"auditNodeName": node.name,
|
|
78
|
+
"type": WORKFLOW_TYPE_MAP[node.node_type]["type"],
|
|
79
|
+
"dealType": WORKFLOW_TYPE_MAP[node.node_type]["dealType"],
|
|
80
|
+
**lane_payload,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
seen_node_ids.add(node.node_id)
|
|
85
|
+
continue
|
|
61
86
|
actions.append(
|
|
62
87
|
{
|
|
63
88
|
"action": "add_node",
|
|
@@ -69,7 +94,7 @@ def compile_workflow(entity: EntitySpec) -> dict[str, Any] | None:
|
|
|
69
94
|
"auditNodeName": node.name,
|
|
70
95
|
"type": WORKFLOW_TYPE_MAP[node.node_type]["type"],
|
|
71
96
|
"dealType": WORKFLOW_TYPE_MAP[node.node_type]["dealType"],
|
|
72
|
-
"prevNodeRef": _prev_node_ref(node, branch_lane_ref),
|
|
97
|
+
"prevNodeRef": _prev_node_ref(node, branch_lane_ref, start_node_ids),
|
|
73
98
|
"auditUserInfos": _build_audit_user_infos(node)
|
|
74
99
|
if node.node_type in {WorkflowNodeType.audit, WorkflowNodeType.fill, WorkflowNodeType.copy}
|
|
75
100
|
else None,
|
|
@@ -100,17 +125,31 @@ def _build_audit_user_infos(node) -> dict[str, Any]:
|
|
|
100
125
|
role_refs = assignees.get("role_refs") or []
|
|
101
126
|
if role_refs:
|
|
102
127
|
audit_user_infos["role_refs"] = [role_ref for role_ref in role_refs if role_ref]
|
|
128
|
+
role_entries = assignees.get("role_entries") or []
|
|
129
|
+
if role_entries:
|
|
130
|
+
audit_user_infos["role"] = [
|
|
131
|
+
{
|
|
132
|
+
"roleId": int(entry.get("roleId") or entry.get("role_id")),
|
|
133
|
+
"roleName": entry.get("roleName") or entry.get("role_name") or str(entry.get("roleId") or entry.get("role_id")),
|
|
134
|
+
"roleIcon": entry.get("roleIcon") or entry.get("role_icon") or "ex-user-outlined",
|
|
135
|
+
"beingFrontendConfig": True,
|
|
136
|
+
}
|
|
137
|
+
for entry in role_entries
|
|
138
|
+
if isinstance(entry, dict) and isinstance(entry.get("roleId") or entry.get("role_id"), int) and int(entry.get("roleId") or entry.get("role_id")) > 0
|
|
139
|
+
]
|
|
103
140
|
include_sub_departs = assignees.get("include_sub_departs")
|
|
104
141
|
if include_sub_departs is not None:
|
|
105
142
|
audit_user_infos["includeSubDeparts"] = bool(include_sub_departs)
|
|
106
143
|
return audit_user_infos
|
|
107
144
|
|
|
108
145
|
|
|
109
|
-
def _prev_node_ref(node, branch_lane_ref: str | None) -> str:
|
|
146
|
+
def _prev_node_ref(node, branch_lane_ref: str | None, start_node_ids: set[str]) -> str:
|
|
110
147
|
if branch_lane_ref:
|
|
111
148
|
if node.parent_node_id and node.parent_node_id != node.branch_parent_id:
|
|
112
149
|
return node.parent_node_id
|
|
113
150
|
return branch_lane_ref
|
|
151
|
+
if node.parent_node_id in start_node_ids:
|
|
152
|
+
return "__applicant__"
|
|
114
153
|
return node.parent_node_id or "__applicant__"
|
|
115
154
|
|
|
116
155
|
|
|
@@ -415,13 +415,13 @@ class SolutionExecutor:
|
|
|
415
415
|
current_nodes = _coerce_workflow_nodes(existing_nodes)
|
|
416
416
|
existing_nodes_by_name = {
|
|
417
417
|
node.get("auditNodeName"): int(node_id)
|
|
418
|
-
for node_id, node in
|
|
418
|
+
for node_id, node in current_nodes.items()
|
|
419
419
|
if isinstance(node, dict) and node.get("auditNodeName")
|
|
420
420
|
}
|
|
421
421
|
applicant_node_id = next(
|
|
422
422
|
(
|
|
423
423
|
int(node_id)
|
|
424
|
-
for node_id, node in
|
|
424
|
+
for node_id, node in current_nodes.items()
|
|
425
425
|
if isinstance(node, dict) and node.get("type") == 0 and node.get("dealType") == 3
|
|
426
426
|
),
|
|
427
427
|
None,
|
|
@@ -429,11 +429,24 @@ class SolutionExecutor:
|
|
|
429
429
|
if applicant_node_id is not None:
|
|
430
430
|
node_artifacts.setdefault("__applicant__", applicant_node_id)
|
|
431
431
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
432
|
+
desired_global_settings = deepcopy(entity.workflow_plan["global_settings"])
|
|
433
|
+
explicit_global_settings = _has_explicit_workflow_global_settings(desired_global_settings)
|
|
434
|
+
current_global_settings: dict[str, Any] = {}
|
|
435
|
+
if explicit_global_settings:
|
|
436
|
+
current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
|
|
437
|
+
else:
|
|
438
|
+
try:
|
|
439
|
+
current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
|
|
440
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
441
|
+
api_error = QingflowApiError(**_coerce_nested_error_payload(error))
|
|
442
|
+
if api_error.http_status != 404:
|
|
443
|
+
raise
|
|
444
|
+
current_global_settings = {}
|
|
445
|
+
if explicit_global_settings:
|
|
446
|
+
global_settings = deepcopy(current_global_settings if isinstance(current_global_settings, dict) else {})
|
|
447
|
+
global_settings.update(desired_global_settings)
|
|
448
|
+
global_settings["editVersionNo"] = workflow_edit_version_no or global_settings.get("editVersionNo") or 1
|
|
449
|
+
self.workflow_tools.workflow_update_global_settings(profile=profile, app_key=app_key, payload=global_settings)
|
|
437
450
|
for action in entity.workflow_plan["actions"]:
|
|
438
451
|
if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is not None:
|
|
439
452
|
continue
|
|
@@ -462,6 +475,16 @@ class SolutionExecutor:
|
|
|
462
475
|
payload["editVersionNo"] = int(workflow_edit_version_no)
|
|
463
476
|
if action["action"] == "create_sub_branch":
|
|
464
477
|
result = self.workflow_tools.workflow_create_sub_branch(profile=profile, app_key=app_key, payload=payload)
|
|
478
|
+
elif action["action"] == "update_node":
|
|
479
|
+
target_node_id = node_artifacts.get(action["node_id"])
|
|
480
|
+
if target_node_id is None:
|
|
481
|
+
raise RuntimeError(f"workflow lane '{action['node_id']}' could not be resolved before update")
|
|
482
|
+
result = self.workflow_tools.workflow_update_node(
|
|
483
|
+
profile=profile,
|
|
484
|
+
app_key=app_key,
|
|
485
|
+
audit_node_id=target_node_id,
|
|
486
|
+
payload=payload,
|
|
487
|
+
)
|
|
465
488
|
else:
|
|
466
489
|
result = self.workflow_tools.workflow_add_node(profile=profile, app_key=app_key, payload=payload)
|
|
467
490
|
expected_type = 1 if action.get("node_type") == "branch" else None
|
|
@@ -2046,6 +2069,20 @@ def _find_created_sub_branch_lane_id(
|
|
|
2046
2069
|
return candidates[0] if candidates else None
|
|
2047
2070
|
|
|
2048
2071
|
|
|
2072
|
+
def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | None) -> bool:
|
|
2073
|
+
if not isinstance(global_settings, dict):
|
|
2074
|
+
return False
|
|
2075
|
+
for key, value in global_settings.items():
|
|
2076
|
+
if key == "editVersionNo":
|
|
2077
|
+
continue
|
|
2078
|
+
if value is None:
|
|
2079
|
+
continue
|
|
2080
|
+
if isinstance(value, (list, dict)) and not value:
|
|
2081
|
+
continue
|
|
2082
|
+
return True
|
|
2083
|
+
return False
|
|
2084
|
+
|
|
2085
|
+
|
|
2049
2086
|
def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
|
|
2050
2087
|
try:
|
|
2051
2088
|
backend_code = int(error.backend_code)
|