@josephyan/qingflow-app-user-mcp 0.2.0-beta.2 → 0.2.0-beta.21
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 +12 -2
- package/npm/lib/runtime.mjs +37 -0
- package/npm/scripts/postinstall.mjs +5 -1
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +230 -0
- package/skills/qingflow-app-user/agents/openai.yaml +4 -0
- package/skills/qingflow-app-user/references/data-gotchas.md +49 -0
- package/skills/qingflow-app-user/references/environments.md +63 -0
- package/skills/qingflow-app-user/references/record-patterns.md +110 -0
- package/skills/qingflow-app-user/references/workflow-usage.md +26 -0
- package/skills/qingflow-record-analysis/SKILL.md +253 -0
- package/skills/qingflow-record-analysis/agents/openai.yaml +4 -0
- package/skills/qingflow-record-analysis/references/analysis-gotchas.md +141 -0
- package/skills/qingflow-record-analysis/references/analysis-patterns.md +113 -0
- package/skills/qingflow-record-analysis/references/confidence-reporting.md +92 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +294 -1
- package/src/qingflow_mcp/builder_facade/service.py +2727 -235
- package/src/qingflow_mcp/server.py +7 -5
- package/src/qingflow_mcp/server_app_builder.py +80 -4
- package/src/qingflow_mcp/server_app_user.py +8 -182
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +1 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +21 -2
- package/src/qingflow_mcp/solution/executor.py +34 -7
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1038 -30
- package/src/qingflow_mcp/tools/app_tools.py +1 -2
- package/src/qingflow_mcp/tools/approval_tools.py +357 -75
- package/src/qingflow_mcp/tools/directory_tools.py +158 -28
- package/src/qingflow_mcp/tools/record_tools.py +1954 -973
- package/src/qingflow_mcp/tools/task_tools.py +376 -225
- package/src/qingflow_mcp/tools/workflow_tools.py +78 -4
|
@@ -28,14 +28,16 @@ def build_server() -> FastMCP:
|
|
|
28
28
|
instructions=(
|
|
29
29
|
"Use auth_login first, then workspace_list and workspace_select. "
|
|
30
30
|
"All resource tools operate with the logged-in user's Qingflow permissions.\n\n"
|
|
31
|
+
"For analytics, use record_schema_get first, let the model build field_id-based DSL, "
|
|
32
|
+
"then call record_analyze. For operational record reads, use record_schema_get first, then record_list or record_get. "
|
|
33
|
+
"For writes, use record_schema_get and then record_write with mode=plan or apply.\n\n"
|
|
31
34
|
"Task Center (待办/已办) handling:\n"
|
|
32
|
-
"- Use
|
|
33
|
-
"- Use task_list
|
|
34
|
-
"- Use
|
|
35
|
+
"- Use task_summary to get headline counts.\n"
|
|
36
|
+
"- Use task_list for flat task browsing with task_box and flow_status.\n"
|
|
37
|
+
"- Use task_facets when worksheet or workflow-node buckets matter.\n"
|
|
35
38
|
"- Use task_mark_read to mark a specific task as read.\n"
|
|
36
39
|
"- Use task_urge to send an urgent reminder for a pending task.\n"
|
|
37
|
-
"-
|
|
38
|
-
"- After identifying the exact task node and record, use record_approve, record_reject, record_rollback, record_transfer, record_reassign, or record_countersign as needed."
|
|
40
|
+
"- After identifying the exact task node and record, use task_approve, task_reject, task_rollback, or task_transfer as needed."
|
|
39
41
|
),
|
|
40
42
|
)
|
|
41
43
|
sessions = SessionStore()
|
|
@@ -17,11 +17,15 @@ def build_builder_server() -> FastMCP:
|
|
|
17
17
|
instructions=(
|
|
18
18
|
"Use this server for AI-native Qingflow builder workflows. "
|
|
19
19
|
"Follow the resource path resolve -> summary read -> plan -> apply -> attach -> publish_verify. "
|
|
20
|
-
"Use
|
|
20
|
+
"Use builder_tool_contract when you need a machine-readable contract, aliases, allowed enums, or a minimal valid example for a public builder tool. "
|
|
21
|
+
"If creating a new package may be appropriate, ask the user to confirm package creation before calling package_create; otherwise use package_resolve/package_list and app_resolve to locate resources, "
|
|
21
22
|
"app_read_summary/app_read_fields/app_read_layout_summary/app_read_views_summary/app_read_flow_summary for compact reads, "
|
|
23
|
+
"member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
|
|
22
24
|
"app_schema_plan/app_layout_plan/app_flow_plan/app_views_plan before writes when the target patch is non-trivial, "
|
|
23
|
-
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply to execute normalized patches
|
|
24
|
-
"package_attach_app to attach apps to packages, and app_publish_verify
|
|
25
|
+
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply to execute normalized patches; these apply tools publish by default unless publish=false. "
|
|
26
|
+
"Use package_attach_app to attach apps to packages, and app_publish_verify for explicit final publish verification. "
|
|
27
|
+
"For workflow edits, prefer preset plus explicit patching over generating a full custom graph from scratch, and declare node assignees and editable fields explicitly. "
|
|
28
|
+
"If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
|
|
25
29
|
"Do not handcraft internal solution payloads or rely on build_id/stage/repair."
|
|
26
30
|
),
|
|
27
31
|
)
|
|
@@ -124,6 +128,57 @@ def build_builder_server() -> FastMCP:
|
|
|
124
128
|
def package_resolve(profile: str = DEFAULT_PROFILE, package_name: str = "") -> dict:
|
|
125
129
|
return ai_builder.package_resolve(profile=profile, package_name=package_name)
|
|
126
130
|
|
|
131
|
+
@server.tool()
|
|
132
|
+
def builder_tool_contract(tool_name: str = "") -> dict:
|
|
133
|
+
return ai_builder.builder_tool_contract(tool_name=tool_name)
|
|
134
|
+
|
|
135
|
+
@server.tool()
|
|
136
|
+
def package_create(profile: str = DEFAULT_PROFILE, package_name: str = "") -> dict:
|
|
137
|
+
return ai_builder.package_create(profile=profile, package_name=package_name)
|
|
138
|
+
|
|
139
|
+
@server.tool()
|
|
140
|
+
def member_search(
|
|
141
|
+
profile: str = DEFAULT_PROFILE,
|
|
142
|
+
query: str = "",
|
|
143
|
+
page_num: int = 1,
|
|
144
|
+
page_size: int = 20,
|
|
145
|
+
contain_disable: bool = False,
|
|
146
|
+
) -> dict:
|
|
147
|
+
return ai_builder.member_search(
|
|
148
|
+
profile=profile,
|
|
149
|
+
query=query,
|
|
150
|
+
page_num=page_num,
|
|
151
|
+
page_size=page_size,
|
|
152
|
+
contain_disable=contain_disable,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@server.tool()
|
|
156
|
+
def role_search(
|
|
157
|
+
profile: str = DEFAULT_PROFILE,
|
|
158
|
+
keyword: str = "",
|
|
159
|
+
page_num: int = 1,
|
|
160
|
+
page_size: int = 20,
|
|
161
|
+
) -> dict:
|
|
162
|
+
return ai_builder.role_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
|
|
163
|
+
|
|
164
|
+
@server.tool()
|
|
165
|
+
def role_create(
|
|
166
|
+
profile: str = DEFAULT_PROFILE,
|
|
167
|
+
role_name: str = "",
|
|
168
|
+
member_uids: list[int] | None = None,
|
|
169
|
+
member_emails: list[str] | None = None,
|
|
170
|
+
member_names: list[str] | None = None,
|
|
171
|
+
role_icon: str = "ex-user-outlined",
|
|
172
|
+
) -> dict:
|
|
173
|
+
return ai_builder.role_create(
|
|
174
|
+
profile=profile,
|
|
175
|
+
role_name=role_name,
|
|
176
|
+
member_uids=member_uids or [],
|
|
177
|
+
member_emails=member_emails or [],
|
|
178
|
+
member_names=member_names or [],
|
|
179
|
+
role_icon=role_icon,
|
|
180
|
+
)
|
|
181
|
+
|
|
127
182
|
@server.tool()
|
|
128
183
|
def package_attach_app(
|
|
129
184
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -133,6 +188,20 @@ def build_builder_server() -> FastMCP:
|
|
|
133
188
|
) -> dict:
|
|
134
189
|
return ai_builder.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title)
|
|
135
190
|
|
|
191
|
+
@server.tool()
|
|
192
|
+
def app_release_edit_lock_if_mine(
|
|
193
|
+
profile: str = DEFAULT_PROFILE,
|
|
194
|
+
app_key: str = "",
|
|
195
|
+
lock_owner_email: str = "",
|
|
196
|
+
lock_owner_name: str = "",
|
|
197
|
+
) -> dict:
|
|
198
|
+
return ai_builder.app_release_edit_lock_if_mine(
|
|
199
|
+
profile=profile,
|
|
200
|
+
app_key=app_key,
|
|
201
|
+
lock_owner_email=lock_owner_email,
|
|
202
|
+
lock_owner_name=lock_owner_name,
|
|
203
|
+
)
|
|
204
|
+
|
|
136
205
|
@server.tool()
|
|
137
206
|
def app_resolve(
|
|
138
207
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -242,6 +311,7 @@ def build_builder_server() -> FastMCP:
|
|
|
242
311
|
app_name: str = "",
|
|
243
312
|
app_title: str = "",
|
|
244
313
|
create_if_missing: bool = False,
|
|
314
|
+
publish: bool = True,
|
|
245
315
|
add_fields: list[dict] | None = None,
|
|
246
316
|
update_fields: list[dict] | None = None,
|
|
247
317
|
remove_fields: list[dict] | None = None,
|
|
@@ -253,6 +323,7 @@ def build_builder_server() -> FastMCP:
|
|
|
253
323
|
app_name=app_name,
|
|
254
324
|
app_title=app_title,
|
|
255
325
|
create_if_missing=create_if_missing,
|
|
326
|
+
publish=publish,
|
|
256
327
|
add_fields=add_fields or [],
|
|
257
328
|
update_fields=update_fields or [],
|
|
258
329
|
remove_fields=remove_fields or [],
|
|
@@ -263,15 +334,17 @@ def build_builder_server() -> FastMCP:
|
|
|
263
334
|
profile: str = DEFAULT_PROFILE,
|
|
264
335
|
app_key: str = "",
|
|
265
336
|
mode: str = "merge",
|
|
337
|
+
publish: bool = True,
|
|
266
338
|
sections: list[dict] | None = None,
|
|
267
339
|
) -> dict:
|
|
268
|
-
return ai_builder.app_layout_apply(profile=profile, app_key=app_key, mode=mode, sections=sections or [])
|
|
340
|
+
return ai_builder.app_layout_apply(profile=profile, app_key=app_key, mode=mode, publish=publish, sections=sections or [])
|
|
269
341
|
|
|
270
342
|
@server.tool()
|
|
271
343
|
def app_flow_apply(
|
|
272
344
|
profile: str = DEFAULT_PROFILE,
|
|
273
345
|
app_key: str = "",
|
|
274
346
|
mode: str = "replace",
|
|
347
|
+
publish: bool = True,
|
|
275
348
|
nodes: list[dict] | None = None,
|
|
276
349
|
transitions: list[dict] | None = None,
|
|
277
350
|
) -> dict:
|
|
@@ -279,6 +352,7 @@ def build_builder_server() -> FastMCP:
|
|
|
279
352
|
profile=profile,
|
|
280
353
|
app_key=app_key,
|
|
281
354
|
mode=mode,
|
|
355
|
+
publish=publish,
|
|
282
356
|
nodes=nodes or [],
|
|
283
357
|
transitions=transitions or [],
|
|
284
358
|
)
|
|
@@ -287,12 +361,14 @@ def build_builder_server() -> FastMCP:
|
|
|
287
361
|
def app_views_apply(
|
|
288
362
|
profile: str = DEFAULT_PROFILE,
|
|
289
363
|
app_key: str = "",
|
|
364
|
+
publish: bool = True,
|
|
290
365
|
upsert_views: list[dict] | None = None,
|
|
291
366
|
remove_views: list[str] | None = None,
|
|
292
367
|
) -> dict:
|
|
293
368
|
return ai_builder.app_views_apply(
|
|
294
369
|
profile=profile,
|
|
295
370
|
app_key=app_key,
|
|
371
|
+
publish=publish,
|
|
296
372
|
upsert_views=upsert_views or [],
|
|
297
373
|
remove_views=remove_views or [],
|
|
298
374
|
)
|
|
@@ -18,8 +18,11 @@ def build_user_server() -> FastMCP:
|
|
|
18
18
|
server = FastMCP(
|
|
19
19
|
"Qingflow App User MCP",
|
|
20
20
|
instructions=(
|
|
21
|
-
"Use this server for Qingflow
|
|
22
|
-
"
|
|
21
|
+
"Use this server for Qingflow operational workflows with a schema-first path. "
|
|
22
|
+
"For records, start with record_schema_get, then choose record_list, record_get, or record_write. "
|
|
23
|
+
"For analytics, switch to record_schema_get and record_analyze. "
|
|
24
|
+
"For task center, use task_summary, task_list, and task_facets before any explicit task action. "
|
|
25
|
+
"Avoid builder-side app or schema changes here."
|
|
23
26
|
),
|
|
24
27
|
)
|
|
25
28
|
sessions = SessionStore()
|
|
@@ -136,189 +139,12 @@ def build_user_server() -> FastMCP:
|
|
|
136
139
|
bucket_type=bucket_type,
|
|
137
140
|
path_id=path_id,
|
|
138
141
|
file_related_url=file_related_url,
|
|
139
|
-
|
|
142
|
+
)
|
|
140
143
|
|
|
141
144
|
RecordTools(sessions, backend).register(server)
|
|
142
145
|
DirectoryTools(sessions, backend).register(server)
|
|
143
|
-
|
|
144
|
-
|
|
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 {})
|
|
146
|
+
approvals.register(server)
|
|
147
|
+
tasks.register(server)
|
|
322
148
|
|
|
323
149
|
return server
|
|
324
150
|
|
|
@@ -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
|
|
|
@@ -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)
|
|
@@ -69,7 +74,7 @@ def compile_workflow(entity: EntitySpec) -> dict[str, Any] | None:
|
|
|
69
74
|
"auditNodeName": node.name,
|
|
70
75
|
"type": WORKFLOW_TYPE_MAP[node.node_type]["type"],
|
|
71
76
|
"dealType": WORKFLOW_TYPE_MAP[node.node_type]["dealType"],
|
|
72
|
-
"prevNodeRef": _prev_node_ref(node, branch_lane_ref),
|
|
77
|
+
"prevNodeRef": _prev_node_ref(node, branch_lane_ref, start_node_ids),
|
|
73
78
|
"auditUserInfos": _build_audit_user_infos(node)
|
|
74
79
|
if node.node_type in {WorkflowNodeType.audit, WorkflowNodeType.fill, WorkflowNodeType.copy}
|
|
75
80
|
else None,
|
|
@@ -100,17 +105,31 @@ def _build_audit_user_infos(node) -> dict[str, Any]:
|
|
|
100
105
|
role_refs = assignees.get("role_refs") or []
|
|
101
106
|
if role_refs:
|
|
102
107
|
audit_user_infos["role_refs"] = [role_ref for role_ref in role_refs if role_ref]
|
|
108
|
+
role_entries = assignees.get("role_entries") or []
|
|
109
|
+
if role_entries:
|
|
110
|
+
audit_user_infos["role"] = [
|
|
111
|
+
{
|
|
112
|
+
"roleId": int(entry.get("roleId") or entry.get("role_id")),
|
|
113
|
+
"roleName": entry.get("roleName") or entry.get("role_name") or str(entry.get("roleId") or entry.get("role_id")),
|
|
114
|
+
"roleIcon": entry.get("roleIcon") or entry.get("role_icon") or "ex-user-outlined",
|
|
115
|
+
"beingFrontendConfig": True,
|
|
116
|
+
}
|
|
117
|
+
for entry in role_entries
|
|
118
|
+
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
|
|
119
|
+
]
|
|
103
120
|
include_sub_departs = assignees.get("include_sub_departs")
|
|
104
121
|
if include_sub_departs is not None:
|
|
105
122
|
audit_user_infos["includeSubDeparts"] = bool(include_sub_departs)
|
|
106
123
|
return audit_user_infos
|
|
107
124
|
|
|
108
125
|
|
|
109
|
-
def _prev_node_ref(node, branch_lane_ref: str | None) -> str:
|
|
126
|
+
def _prev_node_ref(node, branch_lane_ref: str | None, start_node_ids: set[str]) -> str:
|
|
110
127
|
if branch_lane_ref:
|
|
111
128
|
if node.parent_node_id and node.parent_node_id != node.branch_parent_id:
|
|
112
129
|
return node.parent_node_id
|
|
113
130
|
return branch_lane_ref
|
|
131
|
+
if node.parent_node_id in start_node_ids:
|
|
132
|
+
return "__applicant__"
|
|
114
133
|
return node.parent_node_id or "__applicant__"
|
|
115
134
|
|
|
116
135
|
|
|
@@ -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
|
|
@@ -2046,6 +2059,20 @@ def _find_created_sub_branch_lane_id(
|
|
|
2046
2059
|
return candidates[0] if candidates else None
|
|
2047
2060
|
|
|
2048
2061
|
|
|
2062
|
+
def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | None) -> bool:
|
|
2063
|
+
if not isinstance(global_settings, dict):
|
|
2064
|
+
return False
|
|
2065
|
+
for key, value in global_settings.items():
|
|
2066
|
+
if key == "editVersionNo":
|
|
2067
|
+
continue
|
|
2068
|
+
if value is None:
|
|
2069
|
+
continue
|
|
2070
|
+
if isinstance(value, (list, dict)) and not value:
|
|
2071
|
+
continue
|
|
2072
|
+
return True
|
|
2073
|
+
return False
|
|
2074
|
+
|
|
2075
|
+
|
|
2049
2076
|
def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
|
|
2050
2077
|
try:
|
|
2051
2078
|
backend_code = int(error.backend_code)
|