@josephyan/qingflow-app-user-mcp 0.2.0-beta.1 → 0.2.0-beta.10
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 +11 -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 +158 -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 +69 -0
- package/skills/qingflow-app-user/references/workflow-usage.md +24 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +40 -0
- package/src/qingflow_mcp/builder_facade/service.py +1102 -183
- package/src/qingflow_mcp/server_app_builder.py +37 -4
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +1 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +9 -2
- package/src/qingflow_mcp/solution/executor.py +34 -7
- package/src/qingflow_mcp/tools/ai_builder_tools.py +642 -30
- package/src/qingflow_mcp/tools/app_tools.py +40 -2
- package/src/qingflow_mcp/tools/auth_tools.py +2 -1
- package/src/qingflow_mcp/tools/workflow_tools.py +78 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +6 -1
|
@@ -4,10 +4,12 @@ from copy import deepcopy
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import re
|
|
7
8
|
import tempfile
|
|
8
9
|
from typing import Any
|
|
9
10
|
from uuid import uuid4
|
|
10
11
|
|
|
12
|
+
from ..backend_client import BackendRequestContext
|
|
11
13
|
from ..errors import QingflowApiError
|
|
12
14
|
from ..json_types import JSONObject
|
|
13
15
|
from ..solution.build_assembly_store import BuildAssemblyStore, default_artifacts, default_manifest
|
|
@@ -134,6 +136,75 @@ class AiBuilderFacade:
|
|
|
134
136
|
"match_mode": "exact",
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
def package_create(self, *, profile: str, package_name: str) -> JSONObject:
|
|
140
|
+
requested = str(package_name or "").strip()
|
|
141
|
+
normalized_args = {"package_name": requested}
|
|
142
|
+
if not requested:
|
|
143
|
+
return _failed(
|
|
144
|
+
"PACKAGE_NAME_REQUIRED",
|
|
145
|
+
"package_name is required",
|
|
146
|
+
normalized_args=normalized_args,
|
|
147
|
+
suggested_next_call=None,
|
|
148
|
+
)
|
|
149
|
+
existing = self.package_resolve(profile=profile, package_name=requested)
|
|
150
|
+
if existing.get("status") == "success":
|
|
151
|
+
return {
|
|
152
|
+
"status": "success",
|
|
153
|
+
"error_code": None,
|
|
154
|
+
"recoverable": False,
|
|
155
|
+
"message": "package already exists",
|
|
156
|
+
"normalized_args": normalized_args,
|
|
157
|
+
"missing_fields": [],
|
|
158
|
+
"allowed_values": {},
|
|
159
|
+
"details": {},
|
|
160
|
+
"request_id": None,
|
|
161
|
+
"suggested_next_call": None,
|
|
162
|
+
"noop": True,
|
|
163
|
+
"verification": {"existing_package_reused": True},
|
|
164
|
+
"tag_id": existing.get("tag_id"),
|
|
165
|
+
"tag_name": existing.get("tag_name"),
|
|
166
|
+
}
|
|
167
|
+
if existing.get("error_code") == "AMBIGUOUS_PACKAGE":
|
|
168
|
+
return existing
|
|
169
|
+
try:
|
|
170
|
+
created = self.packages.package_create(profile=profile, payload={"tagName": requested})
|
|
171
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
172
|
+
api_error = _coerce_api_error(error)
|
|
173
|
+
return _failed_from_api_error(
|
|
174
|
+
"PACKAGE_CREATE_FAILED",
|
|
175
|
+
api_error,
|
|
176
|
+
normalized_args=normalized_args,
|
|
177
|
+
details={"package_name": requested},
|
|
178
|
+
suggested_next_call={"tool_name": "package_create", "arguments": {"profile": profile, "package_name": requested}},
|
|
179
|
+
)
|
|
180
|
+
result = created.get("result") if isinstance(created.get("result"), dict) else {}
|
|
181
|
+
tag_id = _coerce_positive_int(result.get("tagId"))
|
|
182
|
+
tag_name = str(result.get("tagName") or requested).strip() or requested
|
|
183
|
+
if tag_id is None:
|
|
184
|
+
resolved = self.package_resolve(profile=profile, package_name=requested)
|
|
185
|
+
if resolved.get("status") == "success":
|
|
186
|
+
tag_id = _coerce_positive_int(resolved.get("tag_id"))
|
|
187
|
+
tag_name = str(resolved.get("tag_name") or tag_name)
|
|
188
|
+
verified = tag_id is not None
|
|
189
|
+
return {
|
|
190
|
+
"status": "success" if verified else "partial_success",
|
|
191
|
+
"error_code": None,
|
|
192
|
+
"recoverable": False,
|
|
193
|
+
"message": "created package" if verified else "created package but could not verify tag id",
|
|
194
|
+
"normalized_args": normalized_args,
|
|
195
|
+
"missing_fields": [],
|
|
196
|
+
"allowed_values": {},
|
|
197
|
+
"details": {},
|
|
198
|
+
"request_id": None,
|
|
199
|
+
"suggested_next_call": None
|
|
200
|
+
if verified
|
|
201
|
+
else {"tool_name": "package_resolve", "arguments": {"profile": profile, "package_name": requested}},
|
|
202
|
+
"noop": False,
|
|
203
|
+
"verification": {"tag_id_verified": verified},
|
|
204
|
+
"tag_id": tag_id,
|
|
205
|
+
"tag_name": tag_name,
|
|
206
|
+
}
|
|
207
|
+
|
|
137
208
|
def package_list(self, *, profile: str, trial_status: str = "all") -> JSONObject:
|
|
138
209
|
listed = self.packages.package_list(profile=profile, trial_status=trial_status, include_raw=False)
|
|
139
210
|
return {
|
|
@@ -209,6 +280,115 @@ class AiBuilderFacade:
|
|
|
209
280
|
"attached": attached,
|
|
210
281
|
}
|
|
211
282
|
|
|
283
|
+
def app_release_edit_lock_if_mine(
|
|
284
|
+
self,
|
|
285
|
+
*,
|
|
286
|
+
profile: str,
|
|
287
|
+
app_key: str,
|
|
288
|
+
lock_owner_email: str = "",
|
|
289
|
+
lock_owner_name: str = "",
|
|
290
|
+
) -> JSONObject:
|
|
291
|
+
normalized_args = {
|
|
292
|
+
"app_key": app_key,
|
|
293
|
+
"lock_owner_email": lock_owner_email,
|
|
294
|
+
"lock_owner_name": lock_owner_name,
|
|
295
|
+
}
|
|
296
|
+
session_profile = self.apps.sessions.get_profile(profile)
|
|
297
|
+
if session_profile is None:
|
|
298
|
+
return _failed(
|
|
299
|
+
"AUTH_REQUIRED",
|
|
300
|
+
"auth profile is required before releasing an app edit lock",
|
|
301
|
+
normalized_args=normalized_args,
|
|
302
|
+
recoverable=False,
|
|
303
|
+
suggested_next_call={"tool_name": "auth_whoami", "arguments": {"profile": profile}},
|
|
304
|
+
)
|
|
305
|
+
identity = self._resolve_current_user_identity(profile=profile)
|
|
306
|
+
current_email = str(identity.get("email") or "").strip().lower()
|
|
307
|
+
current_name = str(identity.get("nick_name") or "").strip()
|
|
308
|
+
requested_owner_email = str(lock_owner_email or "").strip().lower()
|
|
309
|
+
requested_owner_name = str(lock_owner_name or "").strip()
|
|
310
|
+
if not requested_owner_email and not requested_owner_name:
|
|
311
|
+
return _failed(
|
|
312
|
+
"EDIT_LOCK_OWNER_UNKNOWN",
|
|
313
|
+
"lock owner could not be verified; refuse to release edit lock blindly",
|
|
314
|
+
normalized_args=normalized_args,
|
|
315
|
+
recoverable=False,
|
|
316
|
+
details={
|
|
317
|
+
"current_user_email": identity.get("email"),
|
|
318
|
+
"current_user_name": identity.get("nick_name"),
|
|
319
|
+
},
|
|
320
|
+
suggested_next_call=None,
|
|
321
|
+
)
|
|
322
|
+
owner_matches = True
|
|
323
|
+
if requested_owner_email:
|
|
324
|
+
owner_matches = bool(current_email) and current_email == requested_owner_email
|
|
325
|
+
elif requested_owner_name:
|
|
326
|
+
owner_matches = bool(current_name) and current_name == requested_owner_name
|
|
327
|
+
if not owner_matches:
|
|
328
|
+
return _failed(
|
|
329
|
+
"EDIT_LOCK_HELD_BY_OTHER_USER",
|
|
330
|
+
"edit lock is owned by another user; refusing to release it",
|
|
331
|
+
normalized_args=normalized_args,
|
|
332
|
+
recoverable=False,
|
|
333
|
+
details={
|
|
334
|
+
"lock_owner_email": requested_owner_email or None,
|
|
335
|
+
"lock_owner_name": requested_owner_name or None,
|
|
336
|
+
"current_user_email": identity.get("email"),
|
|
337
|
+
"current_user_name": identity.get("nick_name"),
|
|
338
|
+
},
|
|
339
|
+
suggested_next_call=None,
|
|
340
|
+
)
|
|
341
|
+
try:
|
|
342
|
+
version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
343
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
344
|
+
api_error = _coerce_api_error(error)
|
|
345
|
+
return _failed_from_api_error(
|
|
346
|
+
"EDIT_LOCK_RELEASE_FAILED",
|
|
347
|
+
api_error,
|
|
348
|
+
normalized_args=normalized_args,
|
|
349
|
+
details={"app_key": app_key},
|
|
350
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
351
|
+
)
|
|
352
|
+
edit_version_no = _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or 1
|
|
353
|
+
try:
|
|
354
|
+
self.apps.app_edit_finished(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
355
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
356
|
+
api_error = _coerce_api_error(error)
|
|
357
|
+
return _failed_from_api_error(
|
|
358
|
+
"EDIT_LOCK_RELEASE_FAILED",
|
|
359
|
+
api_error,
|
|
360
|
+
normalized_args=normalized_args,
|
|
361
|
+
details={
|
|
362
|
+
"app_key": app_key,
|
|
363
|
+
"edit_version_no": edit_version_no,
|
|
364
|
+
"lock_owner_email": requested_owner_email or None,
|
|
365
|
+
"lock_owner_name": requested_owner_name or None,
|
|
366
|
+
},
|
|
367
|
+
suggested_next_call={"tool_name": "app_release_edit_lock_if_mine", "arguments": {"profile": profile, **normalized_args}},
|
|
368
|
+
)
|
|
369
|
+
return {
|
|
370
|
+
"status": "success",
|
|
371
|
+
"error_code": None,
|
|
372
|
+
"recoverable": False,
|
|
373
|
+
"message": "released app edit lock owned by current user",
|
|
374
|
+
"normalized_args": normalized_args,
|
|
375
|
+
"missing_fields": [],
|
|
376
|
+
"allowed_values": {},
|
|
377
|
+
"details": {
|
|
378
|
+
"lock_owner_email": requested_owner_email or None,
|
|
379
|
+
"lock_owner_name": requested_owner_name or None,
|
|
380
|
+
"current_user_email": identity.get("email"),
|
|
381
|
+
"current_user_name": identity.get("nick_name"),
|
|
382
|
+
"edit_version_no": edit_version_no,
|
|
383
|
+
},
|
|
384
|
+
"request_id": None,
|
|
385
|
+
"suggested_next_call": {"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
386
|
+
"noop": False,
|
|
387
|
+
"verification": {"released": True},
|
|
388
|
+
"app_key": app_key,
|
|
389
|
+
"released": True,
|
|
390
|
+
}
|
|
391
|
+
|
|
212
392
|
def app_resolve(
|
|
213
393
|
self,
|
|
214
394
|
*,
|
|
@@ -220,8 +400,14 @@ class AiBuilderFacade:
|
|
|
220
400
|
if app_key:
|
|
221
401
|
try:
|
|
222
402
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
223
|
-
except RuntimeError as exc:
|
|
224
|
-
|
|
403
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
404
|
+
api_error = _coerce_api_error(exc)
|
|
405
|
+
return _failed_from_api_error(
|
|
406
|
+
"APP_NOT_FOUND" if api_error.http_status == 404 else "APP_RESOLVE_FAILED",
|
|
407
|
+
api_error,
|
|
408
|
+
details={"app_key": app_key},
|
|
409
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
410
|
+
)
|
|
225
411
|
result = base.get("result") if isinstance(base.get("result"), dict) else {}
|
|
226
412
|
return {
|
|
227
413
|
"status": "success",
|
|
@@ -298,8 +484,30 @@ class AiBuilderFacade:
|
|
|
298
484
|
}
|
|
299
485
|
|
|
300
486
|
def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
301
|
-
|
|
487
|
+
try:
|
|
488
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
489
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
490
|
+
api_error = _coerce_api_error(error)
|
|
491
|
+
return _failed_from_api_error(
|
|
492
|
+
"APP_READ_FAILED",
|
|
493
|
+
api_error,
|
|
494
|
+
normalized_args={"app_key": app_key},
|
|
495
|
+
details={"app_key": app_key},
|
|
496
|
+
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
|
|
497
|
+
)
|
|
498
|
+
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
499
|
+
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
302
500
|
parsed = state["parsed"]
|
|
501
|
+
verification_hints = _build_verification_hints(
|
|
502
|
+
tag_ids=_coerce_int_list(state["base"].get("tagIds")),
|
|
503
|
+
fields=parsed["fields"],
|
|
504
|
+
layout=parsed["layout"],
|
|
505
|
+
views=_summarize_views(views),
|
|
506
|
+
)
|
|
507
|
+
if views_unavailable:
|
|
508
|
+
verification_hints.append("views_read_unavailable")
|
|
509
|
+
if workflow_unavailable:
|
|
510
|
+
verification_hints.append("workflow_read_unavailable")
|
|
303
511
|
response = AppReadSummaryResponse(
|
|
304
512
|
app_key=app_key,
|
|
305
513
|
title=state["base"].get("formTitle"),
|
|
@@ -307,14 +515,9 @@ class AiBuilderFacade:
|
|
|
307
515
|
publish_status=state["base"].get("appPublishStatus"),
|
|
308
516
|
field_count=len(parsed["fields"]),
|
|
309
517
|
layout_section_count=len(parsed["layout"].get("sections", [])),
|
|
310
|
-
view_count=len(_summarize_views(
|
|
311
|
-
workflow_enabled=bool(
|
|
312
|
-
verification_hints=
|
|
313
|
-
tag_ids=_coerce_int_list(state["base"].get("tagIds")),
|
|
314
|
-
fields=parsed["fields"],
|
|
315
|
-
layout=parsed["layout"],
|
|
316
|
-
views=_summarize_views(state["views"]),
|
|
317
|
-
),
|
|
518
|
+
view_count=len(_summarize_views(views)),
|
|
519
|
+
workflow_enabled=bool(workflow),
|
|
520
|
+
verification_hints=verification_hints,
|
|
318
521
|
)
|
|
319
522
|
return {
|
|
320
523
|
"status": "success",
|
|
@@ -333,7 +536,17 @@ class AiBuilderFacade:
|
|
|
333
536
|
}
|
|
334
537
|
|
|
335
538
|
def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
|
|
336
|
-
|
|
539
|
+
try:
|
|
540
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
541
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
542
|
+
api_error = _coerce_api_error(error)
|
|
543
|
+
return _failed_from_api_error(
|
|
544
|
+
"FIELDS_READ_FAILED",
|
|
545
|
+
api_error,
|
|
546
|
+
normalized_args={"app_key": app_key},
|
|
547
|
+
details={"app_key": app_key},
|
|
548
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
549
|
+
)
|
|
337
550
|
parsed = state["parsed"]
|
|
338
551
|
response = AppFieldsReadResponse(
|
|
339
552
|
app_key=app_key,
|
|
@@ -367,7 +580,17 @@ class AiBuilderFacade:
|
|
|
367
580
|
}
|
|
368
581
|
|
|
369
582
|
def app_read_layout_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
370
|
-
|
|
583
|
+
try:
|
|
584
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
585
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
586
|
+
api_error = _coerce_api_error(error)
|
|
587
|
+
return _failed_from_api_error(
|
|
588
|
+
"LAYOUT_READ_FAILED",
|
|
589
|
+
api_error,
|
|
590
|
+
normalized_args={"app_key": app_key},
|
|
591
|
+
details={"app_key": app_key},
|
|
592
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
593
|
+
)
|
|
371
594
|
parsed = state["parsed"]
|
|
372
595
|
layout = parsed["layout"]
|
|
373
596
|
response = AppLayoutReadResponse(
|
|
@@ -393,10 +616,20 @@ class AiBuilderFacade:
|
|
|
393
616
|
}
|
|
394
617
|
|
|
395
618
|
def app_read_views_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
396
|
-
|
|
619
|
+
try:
|
|
620
|
+
views, _ = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
621
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
622
|
+
api_error = _coerce_api_error(error)
|
|
623
|
+
return _failed_from_api_error(
|
|
624
|
+
"VIEWS_READ_FAILED",
|
|
625
|
+
api_error,
|
|
626
|
+
normalized_args={"app_key": app_key},
|
|
627
|
+
details={"app_key": app_key},
|
|
628
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
629
|
+
)
|
|
397
630
|
response = AppViewsReadResponse(
|
|
398
631
|
app_key=app_key,
|
|
399
|
-
views=_summarize_views(
|
|
632
|
+
views=_summarize_views(views),
|
|
400
633
|
)
|
|
401
634
|
return {
|
|
402
635
|
"status": "success",
|
|
@@ -415,11 +648,21 @@ class AiBuilderFacade:
|
|
|
415
648
|
}
|
|
416
649
|
|
|
417
650
|
def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
418
|
-
|
|
651
|
+
try:
|
|
652
|
+
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
653
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
654
|
+
api_error = _coerce_api_error(error)
|
|
655
|
+
return _failed_from_api_error(
|
|
656
|
+
"FLOW_READ_FAILED",
|
|
657
|
+
api_error,
|
|
658
|
+
normalized_args={"app_key": app_key},
|
|
659
|
+
details={"app_key": app_key},
|
|
660
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
661
|
+
)
|
|
419
662
|
response = AppFlowReadResponse(
|
|
420
663
|
app_key=app_key,
|
|
421
|
-
enabled=bool(
|
|
422
|
-
nodes=_summarize_workflow_nodes(
|
|
664
|
+
enabled=bool(workflow),
|
|
665
|
+
nodes=_summarize_workflow_nodes(workflow),
|
|
423
666
|
transitions=[],
|
|
424
667
|
)
|
|
425
668
|
return {
|
|
@@ -434,7 +677,7 @@ class AiBuilderFacade:
|
|
|
434
677
|
"request_id": None,
|
|
435
678
|
"suggested_next_call": None,
|
|
436
679
|
"noop": False,
|
|
437
|
-
"verification": {"app_exists": True},
|
|
680
|
+
"verification": {"app_exists": True, "workflow_read_unavailable": workflow_unavailable},
|
|
438
681
|
**response.model_dump(mode="json"),
|
|
439
682
|
}
|
|
440
683
|
|
|
@@ -452,7 +695,11 @@ class AiBuilderFacade:
|
|
|
452
695
|
return target
|
|
453
696
|
current_fields: list[dict[str, Any]] = []
|
|
454
697
|
if not bool(target.get("would_create")):
|
|
455
|
-
|
|
698
|
+
fields_result = self.app_read_fields(profile=profile, app_key=str(target["app_key"]))
|
|
699
|
+
if fields_result.get("status") == "failed":
|
|
700
|
+
fields_result.setdefault("normalized_args", normalized_args)
|
|
701
|
+
return fields_result
|
|
702
|
+
current_fields = fields_result.get("fields", [])
|
|
456
703
|
current_by_name = {str(field.get("name") or ""): field for field in current_fields}
|
|
457
704
|
blocking_issues: list[dict[str, Any]] = []
|
|
458
705
|
preview_added: list[str] = []
|
|
@@ -510,8 +757,12 @@ class AiBuilderFacade:
|
|
|
510
757
|
|
|
511
758
|
def app_layout_plan(self, *, profile: str, request: LayoutPlanRequest) -> JSONObject:
|
|
512
759
|
read_fields = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
760
|
+
if read_fields.get("status") == "failed":
|
|
761
|
+
return read_fields
|
|
513
762
|
current_names = [str(field.get("name") or "") for field in read_fields.get("fields", []) if field.get("name")]
|
|
514
763
|
current_layout = self.app_read_layout_summary(profile=profile, app_key=request.app_key)
|
|
764
|
+
if current_layout.get("status") == "failed":
|
|
765
|
+
return current_layout
|
|
515
766
|
requested_sections = [section.model_dump(mode="json") for section in request.sections]
|
|
516
767
|
if request.preset is not None:
|
|
517
768
|
requested_sections = _build_layout_preset_sections(preset=request.preset, field_names=current_names)
|
|
@@ -574,7 +825,10 @@ class AiBuilderFacade:
|
|
|
574
825
|
transitions = [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions]
|
|
575
826
|
if request.preset is not None:
|
|
576
827
|
nodes, transitions = _build_flow_preset(request.preset)
|
|
577
|
-
|
|
828
|
+
fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
829
|
+
if fields_result.get("status") == "failed":
|
|
830
|
+
return fields_result
|
|
831
|
+
current_fields = fields_result.get("fields", [])
|
|
578
832
|
status_field_present = _infer_status_field_id(current_fields) is not None
|
|
579
833
|
node_types = {str(node.get("type") or "") for node in nodes}
|
|
580
834
|
if ("approve" in node_types or request.preset in {FlowPreset.basic_approval, FlowPreset.basic_fill_then_approve}) and not status_field_present:
|
|
@@ -644,7 +898,10 @@ class AiBuilderFacade:
|
|
|
644
898
|
}
|
|
645
899
|
|
|
646
900
|
def app_views_plan(self, *, profile: str, request: ViewsPlanRequest) -> JSONObject:
|
|
647
|
-
|
|
901
|
+
fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
902
|
+
if fields_result.get("status") == "failed":
|
|
903
|
+
return fields_result
|
|
904
|
+
current_fields = fields_result.get("fields", [])
|
|
648
905
|
field_names = {str(field.get("name") or "") for field in current_fields}
|
|
649
906
|
upsert_views = [view.model_dump(mode="json") for view in request.upsert_views]
|
|
650
907
|
if request.preset is not None:
|
|
@@ -728,8 +985,9 @@ class AiBuilderFacade:
|
|
|
728
985
|
profile: str,
|
|
729
986
|
app_key: str = "",
|
|
730
987
|
package_tag_id: int | None = None,
|
|
731
|
-
|
|
988
|
+
app_name: str = "",
|
|
732
989
|
create_if_missing: bool = False,
|
|
990
|
+
publish: bool = True,
|
|
733
991
|
add_fields: list[FieldPatch],
|
|
734
992
|
update_fields: list[FieldUpdatePatch],
|
|
735
993
|
remove_fields: list[FieldRemovePatch],
|
|
@@ -737,8 +995,9 @@ class AiBuilderFacade:
|
|
|
737
995
|
normalized_args = {
|
|
738
996
|
"app_key": app_key,
|
|
739
997
|
"package_tag_id": package_tag_id,
|
|
740
|
-
"
|
|
998
|
+
"app_name": app_name,
|
|
741
999
|
"create_if_missing": create_if_missing,
|
|
1000
|
+
"publish": publish,
|
|
742
1001
|
"add_fields": [patch.model_dump(mode="json") for patch in add_fields],
|
|
743
1002
|
"update_fields": [patch.model_dump(mode="json") for patch in update_fields],
|
|
744
1003
|
"remove_fields": [patch.model_dump(mode="json") for patch in remove_fields],
|
|
@@ -746,27 +1005,36 @@ class AiBuilderFacade:
|
|
|
746
1005
|
resolved = self._resolve_or_create_target_app(
|
|
747
1006
|
profile=profile,
|
|
748
1007
|
app_key=app_key,
|
|
749
|
-
|
|
1008
|
+
app_name=app_name,
|
|
750
1009
|
package_tag_id=package_tag_id,
|
|
751
1010
|
create_if_missing=create_if_missing,
|
|
752
1011
|
)
|
|
753
1012
|
if resolved.get("status") == "failed":
|
|
1013
|
+
if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
|
|
1014
|
+
resolved["normalized_args"] = normalized_args
|
|
754
1015
|
return resolved
|
|
755
1016
|
target = ResolvedApp(
|
|
756
1017
|
app_key=str(resolved["app_key"]),
|
|
757
1018
|
app_name=str(resolved["app_name"]),
|
|
758
1019
|
tag_ids=_coerce_int_list(resolved.get("tag_ids")),
|
|
759
1020
|
)
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
app_key=target.app_key
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1021
|
+
schema_readback_delayed = False
|
|
1022
|
+
try:
|
|
1023
|
+
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=target.app_key)
|
|
1024
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1025
|
+
api_error = _coerce_api_error(error)
|
|
1026
|
+
if not bool(resolved.get("created")) or api_error.http_status != 404:
|
|
1027
|
+
return _failed_from_api_error(
|
|
1028
|
+
"SCHEMA_READBACK_FAILED",
|
|
1029
|
+
api_error,
|
|
1030
|
+
normalized_args=normalized_args,
|
|
1031
|
+
allowed_values={"field_types": [item.value for item in PublicFieldType]},
|
|
1032
|
+
details={"app_key": target.app_key},
|
|
1033
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
1034
|
+
)
|
|
1035
|
+
schema_result = _empty_schema_result(target.app_name)
|
|
1036
|
+
_schema_source = "synthetic_new_app"
|
|
1037
|
+
schema_readback_delayed = True
|
|
770
1038
|
parsed = _parse_schema(schema_result)
|
|
771
1039
|
current_fields = parsed["fields"]
|
|
772
1040
|
layout = parsed["layout"]
|
|
@@ -831,7 +1099,7 @@ class AiBuilderFacade:
|
|
|
831
1099
|
if not added and not updated and not removed and not bool(resolved.get("created")):
|
|
832
1100
|
tag_ids_after = _coerce_int_list((self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}).get("tagIds"))
|
|
833
1101
|
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
834
|
-
|
|
1102
|
+
response = {
|
|
835
1103
|
"status": "success",
|
|
836
1104
|
"error_code": None,
|
|
837
1105
|
"recoverable": False,
|
|
@@ -851,6 +1119,7 @@ class AiBuilderFacade:
|
|
|
851
1119
|
"tag_ids_after": tag_ids_after,
|
|
852
1120
|
"package_attached": package_attached,
|
|
853
1121
|
}
|
|
1122
|
+
return self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
|
|
854
1123
|
|
|
855
1124
|
payload = _build_form_payload_from_fields(
|
|
856
1125
|
title=schema_result.get("formTitle") or target.app_name,
|
|
@@ -858,6 +1127,11 @@ class AiBuilderFacade:
|
|
|
858
1127
|
fields=current_fields,
|
|
859
1128
|
layout=layout,
|
|
860
1129
|
)
|
|
1130
|
+
payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
1131
|
+
profile=profile,
|
|
1132
|
+
app_key=target.app_key,
|
|
1133
|
+
current_schema=schema_result,
|
|
1134
|
+
)
|
|
861
1135
|
try:
|
|
862
1136
|
self.apps.app_update_form_schema(profile=profile, app_key=target.app_key, payload=payload)
|
|
863
1137
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -873,13 +1147,8 @@ class AiBuilderFacade:
|
|
|
873
1147
|
},
|
|
874
1148
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
875
1149
|
)
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
tag_ids_after = _coerce_int_list((self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}).get("tagIds"))
|
|
879
|
-
verification_ok = all(name in verified_field_names for name in added + updated) and all(name not in verified_field_names for name in removed)
|
|
880
|
-
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
881
|
-
return {
|
|
882
|
-
"status": "success" if verification_ok and (package_attached is not False) else "partial_success",
|
|
1150
|
+
response = {
|
|
1151
|
+
"status": "success",
|
|
883
1152
|
"error_code": None,
|
|
884
1153
|
"recoverable": False,
|
|
885
1154
|
"message": "applied schema patch",
|
|
@@ -888,21 +1157,11 @@ class AiBuilderFacade:
|
|
|
888
1157
|
"allowed_values": {"field_types": [item.value for item in PublicFieldType]},
|
|
889
1158
|
"details": {},
|
|
890
1159
|
"request_id": None,
|
|
891
|
-
"suggested_next_call": None
|
|
892
|
-
if package_attached is not False
|
|
893
|
-
else {
|
|
894
|
-
"tool_name": "package_attach_app",
|
|
895
|
-
"arguments": {
|
|
896
|
-
"profile": profile,
|
|
897
|
-
"tag_id": package_tag_id,
|
|
898
|
-
"app_key": target.app_key,
|
|
899
|
-
"app_title": app_title or target.app_name,
|
|
900
|
-
},
|
|
901
|
-
},
|
|
1160
|
+
"suggested_next_call": None,
|
|
902
1161
|
"noop": False,
|
|
903
1162
|
"verification": {
|
|
904
|
-
"fields_verified":
|
|
905
|
-
"package_attached":
|
|
1163
|
+
"fields_verified": False,
|
|
1164
|
+
"package_attached": None,
|
|
906
1165
|
},
|
|
907
1166
|
"app_key": target.app_key,
|
|
908
1167
|
"created": bool(resolved.get("created")),
|
|
@@ -911,10 +1170,71 @@ class AiBuilderFacade:
|
|
|
911
1170
|
"updated": updated,
|
|
912
1171
|
"removed": removed,
|
|
913
1172
|
},
|
|
914
|
-
"verified":
|
|
915
|
-
"tag_ids_after":
|
|
916
|
-
"package_attached":
|
|
1173
|
+
"verified": False,
|
|
1174
|
+
"tag_ids_after": [],
|
|
1175
|
+
"package_attached": None,
|
|
917
1176
|
}
|
|
1177
|
+
if schema_readback_delayed:
|
|
1178
|
+
response["verification"]["schema_readback_delayed"] = True
|
|
1179
|
+
response = self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
|
|
1180
|
+
verification_ok = False
|
|
1181
|
+
tag_ids_after: list[int] = []
|
|
1182
|
+
package_attached: bool | None = None
|
|
1183
|
+
verification_error: QingflowApiError | None = None
|
|
1184
|
+
try:
|
|
1185
|
+
verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
|
|
1186
|
+
verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
|
|
1187
|
+
verification_ok = all(name in verified_field_names for name in added + updated) and all(name not in verified_field_names for name in removed)
|
|
1188
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1189
|
+
verification_error = _coerce_api_error(error)
|
|
1190
|
+
verification_ok = False
|
|
1191
|
+
try:
|
|
1192
|
+
base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
|
|
1193
|
+
tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
|
|
1194
|
+
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
1195
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1196
|
+
base_error = _coerce_api_error(error)
|
|
1197
|
+
if verification_error is None:
|
|
1198
|
+
verification_error = base_error
|
|
1199
|
+
tag_ids_after = []
|
|
1200
|
+
package_attached = None if package_tag_id is None else False
|
|
1201
|
+
response["verification"]["fields_verified"] = verification_ok
|
|
1202
|
+
response["verification"]["package_attached"] = package_attached
|
|
1203
|
+
response["verified"] = verification_ok
|
|
1204
|
+
response["tag_ids_after"] = tag_ids_after
|
|
1205
|
+
response["package_attached"] = package_attached
|
|
1206
|
+
if package_attached is False:
|
|
1207
|
+
response["suggested_next_call"] = {
|
|
1208
|
+
"tool_name": "package_attach_app",
|
|
1209
|
+
"arguments": {
|
|
1210
|
+
"profile": profile,
|
|
1211
|
+
"tag_id": package_tag_id,
|
|
1212
|
+
"app_key": target.app_key,
|
|
1213
|
+
"app_title": app_name or target.app_name,
|
|
1214
|
+
},
|
|
1215
|
+
}
|
|
1216
|
+
publish_failed = bool(response.get("publish_requested")) and not bool(response.get("published"))
|
|
1217
|
+
if verification_ok and package_attached is not False and not publish_failed:
|
|
1218
|
+
response["status"] = "success"
|
|
1219
|
+
else:
|
|
1220
|
+
response["status"] = "partial_success"
|
|
1221
|
+
if verification_error is not None:
|
|
1222
|
+
response["recoverable"] = True
|
|
1223
|
+
response["error_code"] = response.get("error_code") or (
|
|
1224
|
+
"READBACK_PENDING" if verification_error.http_status == 404 else "READBACK_FAILED"
|
|
1225
|
+
)
|
|
1226
|
+
response["message"] = f"{response.get('message') or 'apply succeeded'}; readback pending"
|
|
1227
|
+
response["request_id"] = response.get("request_id") or verification_error.request_id
|
|
1228
|
+
details = response.get("details")
|
|
1229
|
+
if not isinstance(details, dict):
|
|
1230
|
+
details = {}
|
|
1231
|
+
response["details"] = details
|
|
1232
|
+
details["verification_error"] = {
|
|
1233
|
+
"message": verification_error.message,
|
|
1234
|
+
"http_status": verification_error.http_status,
|
|
1235
|
+
"backend_code": verification_error.backend_code,
|
|
1236
|
+
}
|
|
1237
|
+
return response
|
|
918
1238
|
|
|
919
1239
|
def app_layout_apply(
|
|
920
1240
|
self,
|
|
@@ -923,22 +1243,25 @@ class AiBuilderFacade:
|
|
|
923
1243
|
app_key: str,
|
|
924
1244
|
mode: LayoutApplyMode = LayoutApplyMode.merge,
|
|
925
1245
|
sections: list[LayoutSectionPatch],
|
|
1246
|
+
publish: bool = True,
|
|
926
1247
|
) -> JSONObject:
|
|
927
1248
|
normalized_args = {
|
|
928
1249
|
"app_key": app_key,
|
|
929
1250
|
"mode": mode.value,
|
|
930
1251
|
"sections": [section.model_dump(mode="json") for section in sections],
|
|
1252
|
+
"publish": publish,
|
|
931
1253
|
}
|
|
932
|
-
|
|
933
|
-
profile=profile,
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1254
|
+
try:
|
|
1255
|
+
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1256
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1257
|
+
api_error = _coerce_api_error(error)
|
|
1258
|
+
return _failed_from_api_error(
|
|
1259
|
+
"LAYOUT_READ_FAILED",
|
|
1260
|
+
api_error,
|
|
1261
|
+
normalized_args=normalized_args,
|
|
1262
|
+
details={"app_key": app_key},
|
|
1263
|
+
suggested_next_call={"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1264
|
+
)
|
|
942
1265
|
parsed = _parse_schema(schema_result)
|
|
943
1266
|
current_fields = parsed["fields"]
|
|
944
1267
|
fields_by_name = {field["name"]: field for field in current_fields}
|
|
@@ -996,7 +1319,7 @@ class AiBuilderFacade:
|
|
|
996
1319
|
else merged["layout"]
|
|
997
1320
|
)
|
|
998
1321
|
if _layouts_equal(parsed["layout"], target_layout):
|
|
999
|
-
|
|
1322
|
+
response = {
|
|
1000
1323
|
"status": "success",
|
|
1001
1324
|
"error_code": None,
|
|
1002
1325
|
"recoverable": False,
|
|
@@ -1019,10 +1342,16 @@ class AiBuilderFacade:
|
|
|
1019
1342
|
},
|
|
1020
1343
|
"verified": True,
|
|
1021
1344
|
}
|
|
1345
|
+
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1022
1346
|
payload = _build_form_payload_from_existing_schema(
|
|
1023
1347
|
current_schema=schema_result,
|
|
1024
1348
|
layout=target_layout,
|
|
1025
1349
|
)
|
|
1350
|
+
payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
1351
|
+
profile=profile,
|
|
1352
|
+
app_key=app_key,
|
|
1353
|
+
current_schema=schema_result,
|
|
1354
|
+
)
|
|
1026
1355
|
applied_layout = target_layout
|
|
1027
1356
|
fallback_applied = None
|
|
1028
1357
|
try:
|
|
@@ -1035,6 +1364,11 @@ class AiBuilderFacade:
|
|
|
1035
1364
|
current_schema=schema_result,
|
|
1036
1365
|
layout=flattened_layout,
|
|
1037
1366
|
)
|
|
1367
|
+
fallback_payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
1368
|
+
profile=profile,
|
|
1369
|
+
app_key=app_key,
|
|
1370
|
+
current_schema=schema_result,
|
|
1371
|
+
)
|
|
1038
1372
|
try:
|
|
1039
1373
|
self.apps.app_update_form_schema(profile=profile, app_key=app_key, payload=fallback_payload)
|
|
1040
1374
|
applied_layout = flattened_layout
|
|
@@ -1068,8 +1402,33 @@ class AiBuilderFacade:
|
|
|
1068
1402
|
},
|
|
1069
1403
|
suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_args}},
|
|
1070
1404
|
)
|
|
1071
|
-
verified = self.
|
|
1072
|
-
|
|
1405
|
+
verified = self.app_read_layout_summary(profile=profile, app_key=app_key)
|
|
1406
|
+
if verified.get("status") == "failed":
|
|
1407
|
+
response = {
|
|
1408
|
+
"status": "partial_success",
|
|
1409
|
+
"error_code": "LAYOUT_READBACK_PENDING",
|
|
1410
|
+
"recoverable": True,
|
|
1411
|
+
"message": "applied app layout; layout readback pending",
|
|
1412
|
+
"normalized_args": normalized_args,
|
|
1413
|
+
"missing_fields": [],
|
|
1414
|
+
"allowed_values": {"modes": ["merge", "replace"]},
|
|
1415
|
+
"details": {},
|
|
1416
|
+
"request_id": verified.get("request_id"),
|
|
1417
|
+
"suggested_next_call": {"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1418
|
+
"noop": False,
|
|
1419
|
+
"verification": {"layout_verified": False, "layout_read_unavailable": True},
|
|
1420
|
+
"app_key": app_key,
|
|
1421
|
+
"layout_diff": {
|
|
1422
|
+
"mode": mode.value,
|
|
1423
|
+
"replaced": mode == LayoutApplyMode.replace,
|
|
1424
|
+
"merged": mode == LayoutApplyMode.merge,
|
|
1425
|
+
"auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
|
|
1426
|
+
"fallback_applied": fallback_applied,
|
|
1427
|
+
},
|
|
1428
|
+
"verified": False,
|
|
1429
|
+
}
|
|
1430
|
+
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1431
|
+
response = {
|
|
1073
1432
|
"status": "partial_success" if fallback_applied else "success",
|
|
1074
1433
|
"error_code": None,
|
|
1075
1434
|
"recoverable": False,
|
|
@@ -1081,7 +1440,7 @@ class AiBuilderFacade:
|
|
|
1081
1440
|
"request_id": None,
|
|
1082
1441
|
"suggested_next_call": None,
|
|
1083
1442
|
"noop": False,
|
|
1084
|
-
"verification": {"layout_verified": verified["
|
|
1443
|
+
"verification": {"layout_verified": verified["sections"] == applied_layout.get("sections", [])},
|
|
1085
1444
|
"app_key": app_key,
|
|
1086
1445
|
"layout_diff": {
|
|
1087
1446
|
"mode": mode.value,
|
|
@@ -1090,8 +1449,9 @@ class AiBuilderFacade:
|
|
|
1090
1449
|
"auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
|
|
1091
1450
|
"fallback_applied": fallback_applied,
|
|
1092
1451
|
},
|
|
1093
|
-
"verified": verified["
|
|
1452
|
+
"verified": verified["sections"] == applied_layout.get("sections", []),
|
|
1094
1453
|
}
|
|
1454
|
+
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1095
1455
|
|
|
1096
1456
|
def app_flow_apply(
|
|
1097
1457
|
self,
|
|
@@ -1101,12 +1461,14 @@ class AiBuilderFacade:
|
|
|
1101
1461
|
nodes: list[dict[str, Any]],
|
|
1102
1462
|
transitions: list[dict[str, Any]],
|
|
1103
1463
|
mode: str = "replace",
|
|
1464
|
+
publish: bool = True,
|
|
1104
1465
|
) -> JSONObject:
|
|
1105
1466
|
normalized_args = {
|
|
1106
1467
|
"app_key": app_key,
|
|
1107
1468
|
"mode": mode,
|
|
1108
1469
|
"nodes": nodes,
|
|
1109
1470
|
"transitions": transitions,
|
|
1471
|
+
"publish": publish,
|
|
1110
1472
|
}
|
|
1111
1473
|
if mode != "replace":
|
|
1112
1474
|
return _failed(
|
|
@@ -1116,16 +1478,18 @@ class AiBuilderFacade:
|
|
|
1116
1478
|
allowed_values={"modes": ["replace"]},
|
|
1117
1479
|
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}},
|
|
1118
1480
|
)
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
profile=profile,
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1481
|
+
try:
|
|
1482
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1483
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1484
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1485
|
+
api_error = _coerce_api_error(error)
|
|
1486
|
+
return _failed_from_api_error(
|
|
1487
|
+
"FLOW_READ_FAILED",
|
|
1488
|
+
api_error,
|
|
1489
|
+
normalized_args=normalized_args,
|
|
1490
|
+
details={"app_key": app_key},
|
|
1491
|
+
suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1492
|
+
)
|
|
1129
1493
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
1130
1494
|
workflow_spec = _build_public_workflow_spec(nodes=nodes, transitions=transitions)
|
|
1131
1495
|
if workflow_spec.get("status") == "failed":
|
|
@@ -1134,7 +1498,8 @@ class AiBuilderFacade:
|
|
|
1134
1498
|
workflow_spec["suggested_next_call"] = {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
|
|
1135
1499
|
return workflow_spec
|
|
1136
1500
|
desired_node_count = len([node for node in nodes if node.get("type") != "end"])
|
|
1137
|
-
|
|
1501
|
+
current_workflow, _workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1502
|
+
current_node_count = len(_summarize_workflow_nodes(current_workflow))
|
|
1138
1503
|
if current_node_count == desired_node_count and desired_node_count > 0:
|
|
1139
1504
|
# Lightweight idempotency check for repeat submissions of same simple graph.
|
|
1140
1505
|
pass
|
|
@@ -1146,42 +1511,57 @@ class AiBuilderFacade:
|
|
|
1146
1511
|
os.environ["QINGFLOW_MCP_BUILD_HOME"] = temporary_build_home
|
|
1147
1512
|
try:
|
|
1148
1513
|
assembly = BuildAssemblyStore.open(build_id=build_id, create=True)
|
|
1514
|
+
manifest = default_manifest()
|
|
1515
|
+
manifest["solution_name"] = base.get("formTitle") or app_key
|
|
1516
|
+
manifest["preferences"]["create_package"] = False
|
|
1517
|
+
manifest["preferences"]["create_portal"] = False
|
|
1518
|
+
manifest["preferences"]["create_navigation"] = False
|
|
1519
|
+
manifest["entities"] = [entity]
|
|
1520
|
+
assembly.set_manifest(manifest)
|
|
1521
|
+
artifacts = default_artifacts()
|
|
1522
|
+
artifacts["apps"][entity["entity_id"]] = {"app_key": app_key}
|
|
1523
|
+
assembly.set_artifacts(artifacts)
|
|
1524
|
+
flow_stage_spec = {
|
|
1525
|
+
"solution_name": manifest["solution_name"],
|
|
1526
|
+
"entities": [{"entity_id": entity["entity_id"], "workflow": workflow_spec["workflow"]}],
|
|
1527
|
+
}
|
|
1528
|
+
assembly.set_stage_spec("app_flow", flow_stage_spec)
|
|
1529
|
+
stage = self.solutions.solution_build_flow(
|
|
1530
|
+
profile=profile,
|
|
1531
|
+
mode="apply",
|
|
1532
|
+
build_id=build_id,
|
|
1533
|
+
flow_spec=flow_stage_spec,
|
|
1534
|
+
publish=False,
|
|
1535
|
+
run_label=None,
|
|
1536
|
+
repair_patch={},
|
|
1537
|
+
)
|
|
1149
1538
|
finally:
|
|
1150
1539
|
if previous_build_home is None:
|
|
1151
1540
|
os.environ.pop("QINGFLOW_MCP_BUILD_HOME", None)
|
|
1152
|
-
manifest = default_manifest()
|
|
1153
|
-
manifest["solution_name"] = base.get("formTitle") or app_key
|
|
1154
|
-
manifest["preferences"]["create_package"] = False
|
|
1155
|
-
manifest["preferences"]["create_portal"] = False
|
|
1156
|
-
manifest["preferences"]["create_navigation"] = False
|
|
1157
|
-
manifest["entities"] = [entity]
|
|
1158
|
-
assembly.set_manifest(manifest)
|
|
1159
|
-
artifacts = default_artifacts()
|
|
1160
|
-
artifacts["apps"][entity["entity_id"]] = {"app_key": app_key}
|
|
1161
|
-
assembly.set_artifacts(artifacts)
|
|
1162
|
-
stage = self.solutions.solution_build_flow(
|
|
1163
|
-
profile=profile,
|
|
1164
|
-
mode="apply",
|
|
1165
|
-
build_id=build_id,
|
|
1166
|
-
flow_spec={
|
|
1167
|
-
"solution_name": manifest["solution_name"],
|
|
1168
|
-
"entities": [{"entity_id": entity["entity_id"], "workflow": workflow_spec["workflow"]}],
|
|
1169
|
-
},
|
|
1170
|
-
publish=False,
|
|
1171
|
-
run_label=None,
|
|
1172
|
-
repair_patch={},
|
|
1173
|
-
)
|
|
1174
1541
|
if stage.get("status") != "success":
|
|
1175
1542
|
failed = _normalize_flow_stage_failure(stage, profile=profile, app_key=app_key, entity=entity)
|
|
1176
1543
|
failed["normalized_args"] = normalized_args
|
|
1177
|
-
|
|
1544
|
+
suggested_next_call = failed.get("suggested_next_call")
|
|
1545
|
+
if not isinstance(suggested_next_call, dict):
|
|
1546
|
+
suggested_next_call = {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
|
|
1547
|
+
elif suggested_next_call.get("tool_name") == "app_flow_plan":
|
|
1548
|
+
arguments = suggested_next_call.get("arguments")
|
|
1549
|
+
if not isinstance(arguments, dict):
|
|
1550
|
+
arguments = {}
|
|
1551
|
+
arguments.setdefault("profile", profile)
|
|
1552
|
+
arguments.setdefault("app_key", app_key)
|
|
1553
|
+
arguments.setdefault("mode", mode)
|
|
1554
|
+
arguments.setdefault("nodes", nodes)
|
|
1555
|
+
arguments.setdefault("transitions", transitions)
|
|
1556
|
+
suggested_next_call["arguments"] = arguments
|
|
1557
|
+
failed["suggested_next_call"] = suggested_next_call
|
|
1178
1558
|
return failed
|
|
1179
|
-
verified_nodes = self.
|
|
1180
|
-
|
|
1181
|
-
"status": "success",
|
|
1559
|
+
verified_nodes, verified_nodes_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1560
|
+
response = {
|
|
1561
|
+
"status": "success" if bool(verified_nodes) or not verified_nodes_unavailable else "partial_success",
|
|
1182
1562
|
"error_code": None,
|
|
1183
|
-
"recoverable":
|
|
1184
|
-
"message": "applied workflow patch",
|
|
1563
|
+
"recoverable": bool(verified_nodes_unavailable),
|
|
1564
|
+
"message": "applied workflow patch" if not verified_nodes_unavailable else "applied workflow patch; flow readback pending",
|
|
1185
1565
|
"normalized_args": normalized_args,
|
|
1186
1566
|
"missing_fields": [],
|
|
1187
1567
|
"allowed_values": {"modes": ["replace"]},
|
|
@@ -1189,11 +1569,15 @@ class AiBuilderFacade:
|
|
|
1189
1569
|
"request_id": None,
|
|
1190
1570
|
"suggested_next_call": None,
|
|
1191
1571
|
"noop": False,
|
|
1192
|
-
"verification": {"workflow_verified": bool(verified_nodes)},
|
|
1572
|
+
"verification": {"workflow_verified": bool(verified_nodes), "workflow_read_unavailable": verified_nodes_unavailable},
|
|
1193
1573
|
"app_key": app_key,
|
|
1194
1574
|
"flow_diff": {"mode": "replace", "node_count": desired_node_count},
|
|
1195
1575
|
"verified": bool(verified_nodes),
|
|
1196
1576
|
}
|
|
1577
|
+
if verified_nodes_unavailable:
|
|
1578
|
+
response["error_code"] = "FLOW_READBACK_PENDING"
|
|
1579
|
+
response["suggested_next_call"] = {"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}}
|
|
1580
|
+
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1197
1581
|
|
|
1198
1582
|
def app_views_apply(
|
|
1199
1583
|
self,
|
|
@@ -1202,14 +1586,16 @@ class AiBuilderFacade:
|
|
|
1202
1586
|
app_key: str,
|
|
1203
1587
|
upsert_views: list[ViewUpsertPatch],
|
|
1204
1588
|
remove_views: list[str],
|
|
1589
|
+
publish: bool = True,
|
|
1205
1590
|
) -> JSONObject:
|
|
1206
1591
|
normalized_args = {
|
|
1207
1592
|
"app_key": app_key,
|
|
1208
1593
|
"upsert_views": [patch.model_dump(mode="json") for patch in upsert_views],
|
|
1209
1594
|
"remove_views": list(remove_views),
|
|
1595
|
+
"publish": publish,
|
|
1210
1596
|
}
|
|
1211
1597
|
if not upsert_views and not remove_views:
|
|
1212
|
-
|
|
1598
|
+
response = {
|
|
1213
1599
|
"status": "success",
|
|
1214
1600
|
"error_code": None,
|
|
1215
1601
|
"recoverable": False,
|
|
@@ -1226,17 +1612,21 @@ class AiBuilderFacade:
|
|
|
1226
1612
|
"views_diff": {"created": [], "updated": [], "removed": []},
|
|
1227
1613
|
"verified": True,
|
|
1228
1614
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
profile=profile,
|
|
1232
|
-
app_key=app_key
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1615
|
+
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1616
|
+
try:
|
|
1617
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1618
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1619
|
+
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
1620
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1621
|
+
api_error = _coerce_api_error(error)
|
|
1622
|
+
return _failed_from_api_error(
|
|
1623
|
+
"VIEWS_READ_FAILED",
|
|
1624
|
+
api_error,
|
|
1625
|
+
normalized_args=normalized_args,
|
|
1626
|
+
details={"app_key": app_key},
|
|
1627
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1628
|
+
)
|
|
1629
|
+
existing_views = existing_views or []
|
|
1240
1630
|
existing_by_name = {}
|
|
1241
1631
|
for view in existing_views if isinstance(existing_views, list) else []:
|
|
1242
1632
|
if not isinstance(view, dict):
|
|
@@ -1328,6 +1718,33 @@ class AiBuilderFacade:
|
|
|
1328
1718
|
created.append(patch.name)
|
|
1329
1719
|
except (QingflowApiError, RuntimeError) as error:
|
|
1330
1720
|
api_error = _coerce_api_error(error)
|
|
1721
|
+
if api_error.backend_code == 48104:
|
|
1722
|
+
try:
|
|
1723
|
+
if existing_key or created_key:
|
|
1724
|
+
target_key = created_key or existing_key or ""
|
|
1725
|
+
fallback_payload = _build_minimal_view_payload(
|
|
1726
|
+
app_key=app_key,
|
|
1727
|
+
schema=schema,
|
|
1728
|
+
patch=patch,
|
|
1729
|
+
ordinal=ordinal,
|
|
1730
|
+
)
|
|
1731
|
+
self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
|
|
1732
|
+
if existing_key:
|
|
1733
|
+
updated.append(patch.name)
|
|
1734
|
+
else:
|
|
1735
|
+
created.append(patch.name)
|
|
1736
|
+
continue
|
|
1737
|
+
fallback_payload = _build_minimal_view_payload(
|
|
1738
|
+
app_key=app_key,
|
|
1739
|
+
schema=schema,
|
|
1740
|
+
patch=patch,
|
|
1741
|
+
ordinal=ordinal,
|
|
1742
|
+
)
|
|
1743
|
+
self.views.view_create(profile=profile, payload=fallback_payload)
|
|
1744
|
+
created.append(patch.name)
|
|
1745
|
+
continue
|
|
1746
|
+
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
1747
|
+
api_error = _coerce_api_error(fallback_error)
|
|
1331
1748
|
if created_key:
|
|
1332
1749
|
try:
|
|
1333
1750
|
self.views.view_delete(profile=profile, viewgraph_key=created_key)
|
|
@@ -1343,32 +1760,49 @@ class AiBuilderFacade:
|
|
|
1343
1760
|
"view_type": patch.type.value,
|
|
1344
1761
|
"columns": patch.columns,
|
|
1345
1762
|
"group_by": patch.group_by,
|
|
1763
|
+
"operation": "update" if existing_key or created_key else "create",
|
|
1346
1764
|
},
|
|
1347
1765
|
suggested_next_call={
|
|
1348
1766
|
"tool_name": "app_views_plan",
|
|
1349
1767
|
"arguments": {"profile": profile, "app_key": app_key},
|
|
1350
1768
|
},
|
|
1351
1769
|
)
|
|
1352
|
-
|
|
1353
|
-
|
|
1770
|
+
try:
|
|
1771
|
+
verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1772
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1773
|
+
api_error = _coerce_api_error(error)
|
|
1774
|
+
return _failed_from_api_error(
|
|
1775
|
+
"VIEWS_READ_FAILED",
|
|
1776
|
+
api_error,
|
|
1777
|
+
normalized_args=normalized_args,
|
|
1778
|
+
details={"app_key": app_key},
|
|
1779
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1780
|
+
)
|
|
1781
|
+
verified_names = {
|
|
1782
|
+
item.get("viewgraphName") or item.get("viewName") or item.get("title")
|
|
1783
|
+
for item in (verified_view_result or [])
|
|
1784
|
+
if isinstance(item, dict)
|
|
1785
|
+
}
|
|
1786
|
+
verified = (not verified_views_unavailable) and all(name in verified_names for name in created + updated) and all(name not in verified_names for name in removed)
|
|
1354
1787
|
noop = not created and not updated and not removed
|
|
1355
|
-
|
|
1788
|
+
response = {
|
|
1356
1789
|
"status": "success" if verified else "partial_success",
|
|
1357
|
-
"error_code": None,
|
|
1358
|
-
"recoverable":
|
|
1359
|
-
"message": "applied view patch",
|
|
1790
|
+
"error_code": None if not verified_views_unavailable else "VIEWS_READBACK_PENDING",
|
|
1791
|
+
"recoverable": bool(verified_views_unavailable),
|
|
1792
|
+
"message": "applied view patch" if not verified_views_unavailable else "applied view patch; views readback pending",
|
|
1360
1793
|
"normalized_args": normalized_args,
|
|
1361
1794
|
"missing_fields": [],
|
|
1362
1795
|
"allowed_values": {"view_types": ["table", "card", "board"]},
|
|
1363
1796
|
"details": {},
|
|
1364
1797
|
"request_id": None,
|
|
1365
|
-
"suggested_next_call": None,
|
|
1798
|
+
"suggested_next_call": None if not verified_views_unavailable else {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1366
1799
|
"noop": noop,
|
|
1367
|
-
"verification": {"views_verified": verified},
|
|
1800
|
+
"verification": {"views_verified": verified, "views_read_unavailable": verified_views_unavailable},
|
|
1368
1801
|
"app_key": app_key,
|
|
1369
1802
|
"views_diff": {"created": created, "updated": updated, "removed": removed},
|
|
1370
1803
|
"verified": verified,
|
|
1371
1804
|
}
|
|
1805
|
+
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1372
1806
|
|
|
1373
1807
|
def app_publish_verify(
|
|
1374
1808
|
self,
|
|
@@ -1378,12 +1812,33 @@ class AiBuilderFacade:
|
|
|
1378
1812
|
expected_package_tag_id: int | None = None,
|
|
1379
1813
|
) -> JSONObject:
|
|
1380
1814
|
normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
|
|
1381
|
-
|
|
1815
|
+
try:
|
|
1816
|
+
base_before = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1817
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1818
|
+
api_error = _coerce_api_error(error)
|
|
1819
|
+
return _failed_from_api_error(
|
|
1820
|
+
"APP_READ_FAILED",
|
|
1821
|
+
api_error,
|
|
1822
|
+
normalized_args=normalized_args,
|
|
1823
|
+
details={"app_key": app_key},
|
|
1824
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1825
|
+
)
|
|
1382
1826
|
tag_ids_before = _coerce_int_list(base_before.get("tagIds"))
|
|
1383
1827
|
already_published = bool(base_before.get("appPublishStatus") in {1, 2})
|
|
1384
1828
|
package_already_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_before
|
|
1385
|
-
|
|
1386
|
-
|
|
1829
|
+
try:
|
|
1830
|
+
views_before, views_before_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1831
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1832
|
+
api_error = _coerce_api_error(error)
|
|
1833
|
+
return _failed_from_api_error(
|
|
1834
|
+
"VIEWS_READ_FAILED",
|
|
1835
|
+
api_error,
|
|
1836
|
+
normalized_args=normalized_args,
|
|
1837
|
+
details={"app_key": app_key},
|
|
1838
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1839
|
+
)
|
|
1840
|
+
views_before = views_before or []
|
|
1841
|
+
if already_published and package_already_attached is not False and isinstance(views_before, list) and not views_before_unavailable:
|
|
1387
1842
|
return {
|
|
1388
1843
|
"status": "success",
|
|
1389
1844
|
"error_code": None,
|
|
@@ -1418,17 +1873,38 @@ class AiBuilderFacade:
|
|
|
1418
1873
|
details={"app_key": app_key, "edit_version_no": edit_version_no},
|
|
1419
1874
|
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1420
1875
|
)
|
|
1421
|
-
|
|
1876
|
+
try:
|
|
1877
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1878
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1879
|
+
api_error = _coerce_api_error(error)
|
|
1880
|
+
return _failed_from_api_error(
|
|
1881
|
+
"APP_READ_FAILED",
|
|
1882
|
+
api_error,
|
|
1883
|
+
normalized_args=normalized_args,
|
|
1884
|
+
details={"app_key": app_key},
|
|
1885
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1886
|
+
)
|
|
1422
1887
|
tag_ids_after = _coerce_int_list(base.get("tagIds"))
|
|
1423
1888
|
package_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_after
|
|
1424
|
-
|
|
1425
|
-
|
|
1889
|
+
try:
|
|
1890
|
+
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1891
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1892
|
+
api_error = _coerce_api_error(error)
|
|
1893
|
+
return _failed_from_api_error(
|
|
1894
|
+
"VIEWS_READ_FAILED",
|
|
1895
|
+
api_error,
|
|
1896
|
+
normalized_args=normalized_args,
|
|
1897
|
+
details={"app_key": app_key},
|
|
1898
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1899
|
+
)
|
|
1900
|
+
views = views or []
|
|
1901
|
+
views_ok = isinstance(views, list) and not views_unavailable
|
|
1426
1902
|
verified = bool(base.get("appPublishStatus") in {1, 2}) and (package_attached is not False) and views_ok
|
|
1427
1903
|
return {
|
|
1428
1904
|
"status": "success" if verified else "partial_success",
|
|
1429
|
-
"error_code": None,
|
|
1430
|
-
"recoverable":
|
|
1431
|
-
"message": "published and verified app",
|
|
1905
|
+
"error_code": None if not views_unavailable else "VIEWS_READBACK_PENDING",
|
|
1906
|
+
"recoverable": bool(views_unavailable),
|
|
1907
|
+
"message": "published and verified app" if not views_unavailable else "published app; views readback pending",
|
|
1432
1908
|
"normalized_args": normalized_args,
|
|
1433
1909
|
"missing_fields": [],
|
|
1434
1910
|
"allowed_values": {},
|
|
@@ -1441,7 +1917,7 @@ class AiBuilderFacade:
|
|
|
1441
1917
|
"arguments": {"profile": profile, "tag_id": expected_package_tag_id, "app_key": app_key},
|
|
1442
1918
|
},
|
|
1443
1919
|
"noop": False,
|
|
1444
|
-
"verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok},
|
|
1920
|
+
"verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok, "views_read_unavailable": views_unavailable},
|
|
1445
1921
|
"app_key": app_key,
|
|
1446
1922
|
"published": bool(base.get("appPublishStatus") in {1, 2}),
|
|
1447
1923
|
"package_attached": package_attached,
|
|
@@ -1450,29 +1926,151 @@ class AiBuilderFacade:
|
|
|
1450
1926
|
"verified": verified,
|
|
1451
1927
|
}
|
|
1452
1928
|
|
|
1453
|
-
def
|
|
1929
|
+
def _publish_current_edit_version(self, *, profile: str, app_key: str) -> JSONObject:
|
|
1930
|
+
normalized_args = {"app_key": app_key}
|
|
1931
|
+
version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
1932
|
+
edit_version_no = _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or 1
|
|
1933
|
+
try:
|
|
1934
|
+
self.apps.app_publish(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
1935
|
+
self.apps.app_edit_finished(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
1936
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1937
|
+
api_error = _coerce_api_error(error)
|
|
1938
|
+
return _failed_from_api_error(
|
|
1939
|
+
"PUBLISH_FAILED",
|
|
1940
|
+
api_error,
|
|
1941
|
+
normalized_args=normalized_args,
|
|
1942
|
+
details={"app_key": app_key, "edit_version_no": edit_version_no},
|
|
1943
|
+
suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1944
|
+
)
|
|
1945
|
+
return {
|
|
1946
|
+
"status": "success",
|
|
1947
|
+
"error_code": None,
|
|
1948
|
+
"recoverable": False,
|
|
1949
|
+
"message": "published current app draft",
|
|
1950
|
+
"normalized_args": normalized_args,
|
|
1951
|
+
"missing_fields": [],
|
|
1952
|
+
"allowed_values": {},
|
|
1953
|
+
"details": {"app_key": app_key, "edit_version_no": edit_version_no},
|
|
1954
|
+
"request_id": None,
|
|
1955
|
+
"suggested_next_call": None,
|
|
1956
|
+
"noop": False,
|
|
1957
|
+
"verification": {"published": True},
|
|
1958
|
+
"app_key": app_key,
|
|
1959
|
+
"published": True,
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
def _resolve_form_edit_version(self, *, profile: str, app_key: str, current_schema: dict[str, Any]) -> int:
|
|
1963
|
+
try:
|
|
1964
|
+
version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
1965
|
+
except (QingflowApiError, RuntimeError):
|
|
1966
|
+
version_result = {}
|
|
1967
|
+
return _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or int(current_schema.get("editVersionNo") or 1)
|
|
1968
|
+
|
|
1969
|
+
def _append_publish_result(self, *, profile: str, app_key: str, publish: bool, response: JSONObject) -> JSONObject:
|
|
1970
|
+
response["publish_requested"] = publish
|
|
1971
|
+
if not publish:
|
|
1972
|
+
response["published"] = False
|
|
1973
|
+
return response
|
|
1974
|
+
publish_result = self._publish_current_edit_version(profile=profile, app_key=app_key)
|
|
1975
|
+
response["publish_result"] = publish_result
|
|
1976
|
+
response["published"] = bool(publish_result.get("published"))
|
|
1977
|
+
verification = response.get("verification")
|
|
1978
|
+
if not isinstance(verification, dict):
|
|
1979
|
+
verification = {}
|
|
1980
|
+
response["verification"] = verification
|
|
1981
|
+
verification["published"] = bool(publish_result.get("published"))
|
|
1982
|
+
if publish_result.get("status") == "failed":
|
|
1983
|
+
response["status"] = "partial_success"
|
|
1984
|
+
response["error_code"] = response.get("error_code") or publish_result.get("error_code")
|
|
1985
|
+
response["recoverable"] = True
|
|
1986
|
+
response["message"] = f"{response.get('message') or 'apply succeeded'}; publish failed"
|
|
1987
|
+
if not response.get("suggested_next_call"):
|
|
1988
|
+
response["suggested_next_call"] = publish_result.get("suggested_next_call")
|
|
1989
|
+
return response
|
|
1990
|
+
|
|
1991
|
+
def _load_base_schema_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
|
|
1454
1992
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
1455
|
-
|
|
1456
|
-
profile=profile,
|
|
1457
|
-
app_key=app_key,
|
|
1458
|
-
form_type=1,
|
|
1459
|
-
being_draft=True,
|
|
1460
|
-
being_apply=None,
|
|
1461
|
-
audit_node_id=None,
|
|
1462
|
-
include_raw=True,
|
|
1463
|
-
)
|
|
1464
|
-
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
1465
|
-
workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
1993
|
+
schema_result, schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1466
1994
|
base_result = base.get("result") if isinstance(base.get("result"), dict) else {}
|
|
1467
|
-
schema_result = schema.get("result") if isinstance(schema.get("result"), dict) else {}
|
|
1468
1995
|
return {
|
|
1469
1996
|
"base": base_result,
|
|
1470
1997
|
"schema": schema_result,
|
|
1471
1998
|
"parsed": _parse_schema(schema_result),
|
|
1472
|
-
"
|
|
1473
|
-
"workflow": workflow.get("result"),
|
|
1999
|
+
"schema_source": schema_source,
|
|
1474
2000
|
}
|
|
1475
2001
|
|
|
2002
|
+
def _load_views_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
|
|
2003
|
+
try:
|
|
2004
|
+
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
2005
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2006
|
+
api_error = _coerce_api_error(error)
|
|
2007
|
+
if api_error.http_status == 404:
|
|
2008
|
+
try:
|
|
2009
|
+
legacy_views = self.views.view_list(profile=profile, app_key=app_key)
|
|
2010
|
+
except (QingflowApiError, RuntimeError) as legacy_error:
|
|
2011
|
+
legacy_api_error = _coerce_api_error(legacy_error)
|
|
2012
|
+
if tolerate_404 and legacy_api_error.http_status == 404:
|
|
2013
|
+
return [], True
|
|
2014
|
+
raise
|
|
2015
|
+
legacy_result = legacy_views.get("result")
|
|
2016
|
+
if _is_view_collection_shape(legacy_result):
|
|
2017
|
+
return _normalize_view_collection(legacy_result), False
|
|
2018
|
+
if tolerate_404:
|
|
2019
|
+
return [], True
|
|
2020
|
+
raise error
|
|
2021
|
+
raise
|
|
2022
|
+
return _normalize_view_collection(views.get("result")), False
|
|
2023
|
+
|
|
2024
|
+
def _load_workflow_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
|
|
2025
|
+
try:
|
|
2026
|
+
workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
2027
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2028
|
+
api_error = _coerce_api_error(error)
|
|
2029
|
+
if tolerate_404 and api_error.http_status == 404:
|
|
2030
|
+
return [], True
|
|
2031
|
+
raise
|
|
2032
|
+
return workflow.get("result"), False
|
|
2033
|
+
|
|
2034
|
+
def _load_app_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
|
|
2035
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
2036
|
+
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
2037
|
+
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
2038
|
+
state["views"] = views
|
|
2039
|
+
state["workflow"] = workflow
|
|
2040
|
+
state["views_unavailable"] = views_unavailable
|
|
2041
|
+
state["workflow_unavailable"] = workflow_unavailable
|
|
2042
|
+
return state
|
|
2043
|
+
|
|
2044
|
+
def _read_schema_with_fallback(self, *, profile: str, app_key: str) -> tuple[dict[str, Any], str]:
|
|
2045
|
+
attempts = (
|
|
2046
|
+
("draft", True),
|
|
2047
|
+
("current", None),
|
|
2048
|
+
("published", False),
|
|
2049
|
+
)
|
|
2050
|
+
last_error: Exception | None = None
|
|
2051
|
+
for label, being_draft in attempts:
|
|
2052
|
+
try:
|
|
2053
|
+
schema = self.apps.app_get_form_schema(
|
|
2054
|
+
profile=profile,
|
|
2055
|
+
app_key=app_key,
|
|
2056
|
+
form_type=1,
|
|
2057
|
+
being_draft=being_draft,
|
|
2058
|
+
being_apply=None,
|
|
2059
|
+
audit_node_id=None,
|
|
2060
|
+
include_raw=True,
|
|
2061
|
+
)
|
|
2062
|
+
result = schema.get("result")
|
|
2063
|
+
return (result if isinstance(result, dict) else {}), label
|
|
2064
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2065
|
+
api_error = _coerce_api_error(error)
|
|
2066
|
+
last_error = error
|
|
2067
|
+
if api_error.http_status == 404:
|
|
2068
|
+
continue
|
|
2069
|
+
raise
|
|
2070
|
+
if last_error is not None:
|
|
2071
|
+
raise last_error
|
|
2072
|
+
return {}, "unknown"
|
|
2073
|
+
|
|
1476
2074
|
def _preview_target_app(
|
|
1477
2075
|
self,
|
|
1478
2076
|
*,
|
|
@@ -1516,33 +2114,72 @@ class AiBuilderFacade:
|
|
|
1516
2114
|
*,
|
|
1517
2115
|
profile: str,
|
|
1518
2116
|
app_key: str,
|
|
1519
|
-
|
|
2117
|
+
app_name: str,
|
|
1520
2118
|
package_tag_id: int | None,
|
|
1521
2119
|
create_if_missing: bool,
|
|
1522
2120
|
) -> JSONObject:
|
|
1523
2121
|
if app_key:
|
|
1524
2122
|
return self.app_resolve(profile=profile, app_key=app_key)
|
|
1525
|
-
if
|
|
1526
|
-
resolved = self.app_resolve(profile=profile, app_name=
|
|
2123
|
+
if app_name:
|
|
2124
|
+
resolved = self.app_resolve(profile=profile, app_name=app_name, package_tag_id=package_tag_id)
|
|
1527
2125
|
if resolved.get("status") == "success":
|
|
1528
2126
|
return resolved
|
|
1529
2127
|
if not create_if_missing:
|
|
1530
2128
|
return resolved
|
|
1531
2129
|
elif not create_if_missing:
|
|
1532
|
-
return _failed("APP_NAME_REQUIRED", "
|
|
2130
|
+
return _failed("APP_NAME_REQUIRED", "app_name or app_key is required", suggested_next_call=None)
|
|
1533
2131
|
if not create_if_missing:
|
|
1534
2132
|
return _failed("APP_NOT_FOUND", "target app was not found", suggested_next_call=None)
|
|
1535
2133
|
payload: JSONObject = {
|
|
1536
|
-
"appName":
|
|
2134
|
+
"appName": app_name or "未命名应用",
|
|
1537
2135
|
"auth": default_member_auth(),
|
|
1538
2136
|
"tagIds": [],
|
|
1539
2137
|
}
|
|
1540
|
-
|
|
2138
|
+
try:
|
|
2139
|
+
created = self.apps.app_create(profile=profile, payload=payload)
|
|
2140
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2141
|
+
api_error = _coerce_api_error(error)
|
|
2142
|
+
request_route = self._current_request_route(profile)
|
|
2143
|
+
return _failed_from_api_error(
|
|
2144
|
+
"CREATE_APP_ROUTE_NOT_FOUND" if api_error.http_status == 404 else "APP_CREATE_FAILED",
|
|
2145
|
+
api_error,
|
|
2146
|
+
details={
|
|
2147
|
+
"app_name": app_name,
|
|
2148
|
+
"package_tag_id": package_tag_id,
|
|
2149
|
+
"request_route": request_route,
|
|
2150
|
+
},
|
|
2151
|
+
suggested_next_call=(
|
|
2152
|
+
{"tool_name": "workspace_select", "arguments": {"profile": profile, "ws_id": request_route.get("ws_id")}}
|
|
2153
|
+
if request_route.get("ws_id")
|
|
2154
|
+
else None
|
|
2155
|
+
),
|
|
2156
|
+
)
|
|
1541
2157
|
result = created.get("result") if isinstance(created.get("result"), dict) else {}
|
|
1542
2158
|
new_app_key = str(result.get("appKey") or (result.get("appKeys")[0] if isinstance(result.get("appKeys"), list) and result.get("appKeys") else ""))
|
|
1543
2159
|
if not new_app_key:
|
|
1544
2160
|
return _failed("APP_CREATE_FAILED", "failed to create app shell", details={"result": result}, suggested_next_call=None)
|
|
1545
|
-
|
|
2161
|
+
try:
|
|
2162
|
+
base = self.apps.app_get_base(profile=profile, app_key=new_app_key, include_raw=True).get("result") or {}
|
|
2163
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2164
|
+
api_error = _coerce_api_error(error)
|
|
2165
|
+
if api_error.http_status != 404:
|
|
2166
|
+
return _failed_from_api_error(
|
|
2167
|
+
"APP_CREATE_READBACK_FAILED",
|
|
2168
|
+
api_error,
|
|
2169
|
+
details={"app_key": new_app_key, "app_name": app_name, "package_tag_id": package_tag_id},
|
|
2170
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": new_app_key}},
|
|
2171
|
+
)
|
|
2172
|
+
return {
|
|
2173
|
+
"status": "success",
|
|
2174
|
+
"error_code": None,
|
|
2175
|
+
"recoverable": False,
|
|
2176
|
+
"message": "created app; base readback pending",
|
|
2177
|
+
"suggested_next_call": None,
|
|
2178
|
+
"app_key": new_app_key,
|
|
2179
|
+
"app_name": app_name or "未命名应用",
|
|
2180
|
+
"tag_ids": [],
|
|
2181
|
+
"created": True,
|
|
2182
|
+
}
|
|
1546
2183
|
return {
|
|
1547
2184
|
"status": "success",
|
|
1548
2185
|
"error_code": None,
|
|
@@ -1550,11 +2187,56 @@ class AiBuilderFacade:
|
|
|
1550
2187
|
"message": "created app",
|
|
1551
2188
|
"suggested_next_call": None,
|
|
1552
2189
|
"app_key": new_app_key,
|
|
1553
|
-
"app_name": base.get("formTitle") or
|
|
2190
|
+
"app_name": base.get("formTitle") or app_name or "未命名应用",
|
|
1554
2191
|
"tag_ids": _coerce_int_list(base.get("tagIds")),
|
|
1555
2192
|
"created": True,
|
|
1556
2193
|
}
|
|
1557
2194
|
|
|
2195
|
+
def _current_request_route(self, profile: str) -> JSONObject:
|
|
2196
|
+
session_profile = self.apps.sessions.get_profile(profile)
|
|
2197
|
+
backend_session = self.apps.sessions.get_backend_session(profile)
|
|
2198
|
+
if session_profile is None or backend_session is None:
|
|
2199
|
+
return {}
|
|
2200
|
+
context = BackendRequestContext(
|
|
2201
|
+
base_url=backend_session.base_url,
|
|
2202
|
+
token=backend_session.token,
|
|
2203
|
+
ws_id=session_profile.selected_ws_id,
|
|
2204
|
+
qf_version=backend_session.qf_version,
|
|
2205
|
+
qf_version_source=backend_session.qf_version_source,
|
|
2206
|
+
)
|
|
2207
|
+
describe_route = getattr(self.apps.backend, "describe_route", None)
|
|
2208
|
+
route = describe_route(context) if callable(describe_route) else None
|
|
2209
|
+
payload = dict(route) if isinstance(route, dict) else {}
|
|
2210
|
+
payload.setdefault("ws_id", session_profile.selected_ws_id)
|
|
2211
|
+
return payload
|
|
2212
|
+
|
|
2213
|
+
def _resolve_current_user_identity(self, *, profile: str) -> JSONObject:
|
|
2214
|
+
session_profile = self.apps.sessions.get_profile(profile)
|
|
2215
|
+
backend_session = self.apps.sessions.get_backend_session(profile)
|
|
2216
|
+
current_email = str((session_profile.email if session_profile else None) or "").strip()
|
|
2217
|
+
current_name = str((session_profile.nick_name if session_profile else None) or "").strip()
|
|
2218
|
+
if current_email or current_name or session_profile is None or backend_session is None:
|
|
2219
|
+
return {"email": current_email or None, "nick_name": current_name or None}
|
|
2220
|
+
try:
|
|
2221
|
+
user_info = self.apps.backend.request(
|
|
2222
|
+
"GET",
|
|
2223
|
+
BackendRequestContext(
|
|
2224
|
+
base_url=backend_session.base_url,
|
|
2225
|
+
token=backend_session.token,
|
|
2226
|
+
ws_id=session_profile.selected_ws_id,
|
|
2227
|
+
qf_version=backend_session.qf_version,
|
|
2228
|
+
qf_version_source=backend_session.qf_version_source,
|
|
2229
|
+
),
|
|
2230
|
+
"/user",
|
|
2231
|
+
)
|
|
2232
|
+
except (QingflowApiError, RuntimeError):
|
|
2233
|
+
return {"email": current_email or None, "nick_name": current_name or None}
|
|
2234
|
+
if not isinstance(user_info, dict):
|
|
2235
|
+
return {"email": current_email or None, "nick_name": current_name or None}
|
|
2236
|
+
resolved_email = str(user_info.get("email") or "").strip() or None
|
|
2237
|
+
resolved_name = str(user_info.get("nickName") or user_info.get("displayName") or user_info.get("name") or "").strip() or None
|
|
2238
|
+
return {"email": resolved_email, "nick_name": resolved_name}
|
|
2239
|
+
|
|
1558
2240
|
def _attach_app_to_package(self, *, profile: str, app_key: str, app_title: str, package_tag_id: int) -> None:
|
|
1559
2241
|
detail = self.packages.package_get(profile=profile, tag_id=package_tag_id, include_raw=True)
|
|
1560
2242
|
result = detail.get("result") if isinstance(detail.get("result"), dict) else {}
|
|
@@ -1619,18 +2301,49 @@ def _failed_from_api_error(
|
|
|
1619
2301
|
suggested_next_call: JSONObject | None = None,
|
|
1620
2302
|
recoverable: bool = True,
|
|
1621
2303
|
) -> JSONObject:
|
|
2304
|
+
effective_error_code = "APP_EDIT_LOCKED" if error.backend_code == 40074 else error_code
|
|
2305
|
+
public_message = _public_error_message(effective_error_code, error)
|
|
2306
|
+
public_http_status = None if error.http_status == 404 else error.http_status
|
|
2307
|
+
merged_details = dict(details or {})
|
|
2308
|
+
if error.backend_code == 40074:
|
|
2309
|
+
owner = _extract_edit_lock_owner(error.message)
|
|
2310
|
+
merged_details.setdefault("lock_owner_name", owner.get("lock_owner_name"))
|
|
2311
|
+
merged_details.setdefault("lock_owner_email", owner.get("lock_owner_email"))
|
|
2312
|
+
app_key = None
|
|
2313
|
+
if isinstance(normalized_args, dict):
|
|
2314
|
+
app_key = normalized_args.get("app_key")
|
|
2315
|
+
if not app_key and isinstance(details, dict):
|
|
2316
|
+
app_key = details.get("app_key")
|
|
2317
|
+
if isinstance(app_key, str) and app_key.strip():
|
|
2318
|
+
suggested_next_call = {
|
|
2319
|
+
"tool_name": "app_release_edit_lock_if_mine",
|
|
2320
|
+
"arguments": {
|
|
2321
|
+
"app_key": app_key,
|
|
2322
|
+
"lock_owner_name": owner.get("lock_owner_name") or "",
|
|
2323
|
+
"lock_owner_email": owner.get("lock_owner_email") or "",
|
|
2324
|
+
},
|
|
2325
|
+
}
|
|
2326
|
+
if error.http_status is not None or error.backend_code is not None:
|
|
2327
|
+
merged_details.setdefault(
|
|
2328
|
+
"transport_error",
|
|
2329
|
+
{
|
|
2330
|
+
"http_status": error.http_status,
|
|
2331
|
+
"backend_code": error.backend_code,
|
|
2332
|
+
"category": error.category,
|
|
2333
|
+
},
|
|
2334
|
+
)
|
|
1622
2335
|
return _failed(
|
|
1623
|
-
|
|
1624
|
-
|
|
2336
|
+
effective_error_code,
|
|
2337
|
+
public_message,
|
|
1625
2338
|
recoverable=recoverable,
|
|
1626
2339
|
normalized_args=normalized_args,
|
|
1627
2340
|
missing_fields=missing_fields,
|
|
1628
2341
|
allowed_values=allowed_values,
|
|
1629
|
-
details=
|
|
2342
|
+
details=merged_details,
|
|
1630
2343
|
suggested_next_call=suggested_next_call,
|
|
1631
2344
|
request_id=error.request_id,
|
|
1632
2345
|
backend_code=error.backend_code,
|
|
1633
|
-
http_status=
|
|
2346
|
+
http_status=public_http_status,
|
|
1634
2347
|
)
|
|
1635
2348
|
|
|
1636
2349
|
|
|
@@ -1669,6 +2382,52 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
|
1669
2382
|
return QingflowApiError(category="runtime", message=str(error))
|
|
1670
2383
|
|
|
1671
2384
|
|
|
2385
|
+
def _public_error_message(error_code: str, error: QingflowApiError) -> str:
|
|
2386
|
+
if error.backend_code == 40074 or error_code == "APP_EDIT_LOCKED":
|
|
2387
|
+
owner = _extract_edit_lock_owner(error.message)
|
|
2388
|
+
owner_label = owner.get("lock_owner_email") or owner.get("lock_owner_name")
|
|
2389
|
+
if owner_label:
|
|
2390
|
+
return f"app is currently locked by active editor {owner_label}"
|
|
2391
|
+
return "app is currently locked by another active editor session"
|
|
2392
|
+
if error.http_status != 404:
|
|
2393
|
+
return error.message
|
|
2394
|
+
mapping = {
|
|
2395
|
+
"APP_READ_FAILED": "app base or schema is unavailable in the current route",
|
|
2396
|
+
"FIELDS_READ_FAILED": "app fields are unavailable in the current route",
|
|
2397
|
+
"LAYOUT_READ_FAILED": "layout resource is unavailable for this app in the current route",
|
|
2398
|
+
"VIEWS_READ_FAILED": "views resource is unavailable for this app in the current route",
|
|
2399
|
+
"FLOW_READ_FAILED": "workflow resource is unavailable for this app in the current route",
|
|
2400
|
+
"SCHEMA_READBACK_FAILED": "schema was written but schema readback is unavailable in the current route",
|
|
2401
|
+
"CREATE_APP_ROUTE_NOT_FOUND": "create app route is unavailable in the current workspace route",
|
|
2402
|
+
"APP_CREATE_READBACK_FAILED": "app was created but base readback is unavailable in the current route",
|
|
2403
|
+
"PACKAGE_ATTACH_FAILED": "package attachment could not be verified in the current route",
|
|
2404
|
+
"PUBLISH_FAILED": "publish route is unavailable in the current route",
|
|
2405
|
+
"VIEW_APPLY_FAILED": "view resource rejected the operation or is unavailable in the current route",
|
|
2406
|
+
"LAYOUT_APPLY_FAILED": "layout resource rejected the operation or is unavailable in the current route",
|
|
2407
|
+
"SCHEMA_APPLY_FAILED": "schema resource rejected the operation or is unavailable in the current route",
|
|
2408
|
+
"EDIT_LOCK_RELEASE_FAILED": "edit lock release route is unavailable in the current route",
|
|
2409
|
+
}
|
|
2410
|
+
return mapping.get(error_code, "requested builder resource is unavailable in the current route")
|
|
2411
|
+
|
|
2412
|
+
|
|
2413
|
+
def _extract_edit_lock_owner(message: str) -> JSONObject:
|
|
2414
|
+
text = str(message or "").strip()
|
|
2415
|
+
if not text:
|
|
2416
|
+
return {"lock_owner_name": None, "lock_owner_email": None}
|
|
2417
|
+
patterns = [
|
|
2418
|
+
r"应用已被\s*(?P<name>[^((]+?)\s*[((](?P<email>[^))]+)[))]\s*编辑",
|
|
2419
|
+
r"edited by\s*(?P<name>[^<(]+?)\s*<(?P<email>[^>]+)>",
|
|
2420
|
+
]
|
|
2421
|
+
for pattern in patterns:
|
|
2422
|
+
match = re.search(pattern, text)
|
|
2423
|
+
if match:
|
|
2424
|
+
return {
|
|
2425
|
+
"lock_owner_name": match.groupdict().get("name", "").strip() or None,
|
|
2426
|
+
"lock_owner_email": match.groupdict().get("email", "").strip() or None,
|
|
2427
|
+
}
|
|
2428
|
+
return {"lock_owner_name": None, "lock_owner_email": None}
|
|
2429
|
+
|
|
2430
|
+
|
|
1672
2431
|
def _coerce_positive_int(value: Any) -> int | None:
|
|
1673
2432
|
if isinstance(value, bool) or value is None:
|
|
1674
2433
|
return None
|
|
@@ -1696,6 +2455,34 @@ def _coerce_int_list(values: Any) -> list[int]:
|
|
|
1696
2455
|
return result
|
|
1697
2456
|
|
|
1698
2457
|
|
|
2458
|
+
def _normalize_view_collection(values: Any) -> list[dict[str, Any]]:
|
|
2459
|
+
if isinstance(values, list):
|
|
2460
|
+
return [item for item in values if isinstance(item, dict)]
|
|
2461
|
+
if isinstance(values, dict):
|
|
2462
|
+
for key in ("list", "viewList", "views", "result"):
|
|
2463
|
+
candidate = values.get(key)
|
|
2464
|
+
if isinstance(candidate, list):
|
|
2465
|
+
return [item for item in candidate if isinstance(item, dict)]
|
|
2466
|
+
return []
|
|
2467
|
+
|
|
2468
|
+
|
|
2469
|
+
def _is_view_collection_shape(values: Any) -> bool:
|
|
2470
|
+
if isinstance(values, list):
|
|
2471
|
+
return True
|
|
2472
|
+
if isinstance(values, dict):
|
|
2473
|
+
return any(isinstance(values.get(key), list) for key in ("list", "viewList", "views", "result"))
|
|
2474
|
+
return False
|
|
2475
|
+
|
|
2476
|
+
|
|
2477
|
+
def _empty_schema_result(title: str) -> dict[str, Any]:
|
|
2478
|
+
return {
|
|
2479
|
+
"formTitle": title,
|
|
2480
|
+
"editVersionNo": 1,
|
|
2481
|
+
"formQues": [],
|
|
2482
|
+
"questionRelations": [],
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
|
|
1699
2486
|
def _slugify(text: str, *, default: str) -> str:
|
|
1700
2487
|
normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(text or ""))
|
|
1701
2488
|
collapsed = "_".join(part for part in normalized.split("_") if part)
|
|
@@ -2377,19 +3164,17 @@ def _build_view_create_payload(
|
|
|
2377
3164
|
payload["auth"] = default_member_auth()
|
|
2378
3165
|
payload.setdefault("sortType", "defaultSort")
|
|
2379
3166
|
payload.setdefault("viewgraphSorts", [{"queId": 0, "beingSortAscend": True, "queType": 8}])
|
|
2380
|
-
payload.setdefault("beingPinNavigate", True)
|
|
2381
|
-
payload.setdefault("beingShowCover", False)
|
|
2382
|
-
payload.setdefault("defaultRowHigh", "compact")
|
|
2383
|
-
payload.setdefault("asosChartVisible", False)
|
|
2384
|
-
payload.setdefault("viewgraphLimitType", 1)
|
|
2385
|
-
payload.setdefault("viewgraphLimit", [])
|
|
2386
|
-
payload.setdefault("buttonConfigDTOList", [])
|
|
2387
3167
|
if patch.type.value in {"card", "board"}:
|
|
2388
3168
|
payload["beingShowTitleQue"] = True
|
|
2389
3169
|
payload["titleQue"] = visible_que_ids[0] if visible_que_ids else None
|
|
2390
3170
|
if patch.type.value == "board":
|
|
2391
3171
|
payload["groupQueId"] = field_map.get(patch.group_by or "")
|
|
2392
|
-
return
|
|
3172
|
+
return _hydrate_view_backend_payload(
|
|
3173
|
+
payload=payload,
|
|
3174
|
+
view_type=patch.type.value,
|
|
3175
|
+
visible_que_id_values=visible_que_ids,
|
|
3176
|
+
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
3177
|
+
)
|
|
2393
3178
|
|
|
2394
3179
|
|
|
2395
3180
|
def _build_form_payload_from_existing_schema(
|
|
@@ -2581,7 +3366,109 @@ def _build_view_update_payload(
|
|
|
2581
3366
|
payload["beingShowTitleQue"] = True
|
|
2582
3367
|
payload["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
|
|
2583
3368
|
payload["groupQueId"] = field_map.get(patch.group_by or "")
|
|
2584
|
-
return
|
|
3369
|
+
return _hydrate_view_backend_payload(
|
|
3370
|
+
payload=payload,
|
|
3371
|
+
view_type=normalized_type,
|
|
3372
|
+
visible_que_id_values=visible_que_id_values,
|
|
3373
|
+
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
3374
|
+
)
|
|
3375
|
+
|
|
3376
|
+
|
|
3377
|
+
def _build_minimal_view_payload(
|
|
3378
|
+
*,
|
|
3379
|
+
app_key: str,
|
|
3380
|
+
schema: dict[str, Any],
|
|
3381
|
+
patch: ViewUpsertPatch,
|
|
3382
|
+
ordinal: int,
|
|
3383
|
+
) -> JSONObject:
|
|
3384
|
+
field_map = extract_field_map(schema)
|
|
3385
|
+
visible_que_id_values = [field_map[name] for name in patch.columns if name in field_map]
|
|
3386
|
+
payload: JSONObject = {
|
|
3387
|
+
"appKey": app_key,
|
|
3388
|
+
"viewgraphName": patch.name,
|
|
3389
|
+
"viewgraphType": {
|
|
3390
|
+
"table": "tableView",
|
|
3391
|
+
"card": "cardView",
|
|
3392
|
+
"board": "boardView",
|
|
3393
|
+
}[patch.type.value],
|
|
3394
|
+
"ordinal": ordinal,
|
|
3395
|
+
"viewgraphQueIds": visible_que_id_values,
|
|
3396
|
+
"viewgraphQuestions": _build_viewgraph_questions(schema, visible_que_id_values),
|
|
3397
|
+
"auth": default_member_auth(),
|
|
3398
|
+
}
|
|
3399
|
+
if patch.type.value in {"card", "board"}:
|
|
3400
|
+
payload["beingShowTitleQue"] = True
|
|
3401
|
+
payload["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
|
|
3402
|
+
if patch.type.value == "board":
|
|
3403
|
+
payload["groupQueId"] = field_map.get(patch.group_by or "")
|
|
3404
|
+
return _hydrate_view_backend_payload(
|
|
3405
|
+
payload=payload,
|
|
3406
|
+
view_type=patch.type.value,
|
|
3407
|
+
visible_que_id_values=visible_que_id_values,
|
|
3408
|
+
group_que_id=field_map.get(patch.group_by or "") if patch.group_by else None,
|
|
3409
|
+
)
|
|
3410
|
+
|
|
3411
|
+
|
|
3412
|
+
def _hydrate_view_backend_payload(
|
|
3413
|
+
*,
|
|
3414
|
+
payload: JSONObject,
|
|
3415
|
+
view_type: str,
|
|
3416
|
+
visible_que_id_values: list[int],
|
|
3417
|
+
group_que_id: int | None,
|
|
3418
|
+
) -> JSONObject:
|
|
3419
|
+
data = deepcopy(payload)
|
|
3420
|
+
data.setdefault("beingPinNavigate", True)
|
|
3421
|
+
data.setdefault("beingNeedPass", False)
|
|
3422
|
+
data.setdefault("beingShowTitleQue", view_type in {"card", "board"})
|
|
3423
|
+
data.setdefault("beingShowCover", False)
|
|
3424
|
+
data.setdefault("defaultRowHigh", "compact")
|
|
3425
|
+
data.setdefault("asosChartVisible", False)
|
|
3426
|
+
data.setdefault("viewgraphPass", "")
|
|
3427
|
+
data.setdefault("beingGroupColor", False)
|
|
3428
|
+
data.setdefault("beingShowQueTitle", True)
|
|
3429
|
+
data.setdefault("beingImageAdaption", False)
|
|
3430
|
+
data.setdefault("clippingMode", "default")
|
|
3431
|
+
data.setdefault("frontCoverQueId", None)
|
|
3432
|
+
data.setdefault("viewgraphLimitType", 1)
|
|
3433
|
+
data.setdefault("viewgraphLimit", [])
|
|
3434
|
+
data.setdefault("viewgraphLimitFormula", "")
|
|
3435
|
+
data.setdefault("sortType", "defaultSort")
|
|
3436
|
+
if not data.get("viewgraphSorts"):
|
|
3437
|
+
sort_que_id = visible_que_id_values[0] if visible_que_id_values else 1
|
|
3438
|
+
data["viewgraphSorts"] = [{"queId": sort_que_id, "beingSortAscend": True}]
|
|
3439
|
+
data.setdefault("beingAuditRecordVisible", True)
|
|
3440
|
+
data.setdefault("beingQrobotRecordVisible", False)
|
|
3441
|
+
data.setdefault("beingPrintStatus", False)
|
|
3442
|
+
data.setdefault("beingDefaultPrintTplStatus", False)
|
|
3443
|
+
data.setdefault("printTpls", [])
|
|
3444
|
+
data.setdefault("beingCommentStatus", False)
|
|
3445
|
+
data.setdefault("usages", [])
|
|
3446
|
+
data.setdefault("dataPermissionType", "CUSTOM")
|
|
3447
|
+
data.setdefault("dataScope", "ALL")
|
|
3448
|
+
data.setdefault("needPass", False)
|
|
3449
|
+
data.setdefault("beingWorkflowNodeFutureListVisible", True)
|
|
3450
|
+
data.setdefault("asosChartConfig", {"limitType": 1, "asosChartIdList": []})
|
|
3451
|
+
data.setdefault("viewgraphGanttConfigVO", None)
|
|
3452
|
+
data.setdefault("viewgraphHierarchyConfigVO", None)
|
|
3453
|
+
data.setdefault("buttonConfigDTOList", [])
|
|
3454
|
+
if "buttonConfig" not in data:
|
|
3455
|
+
data["buttonConfig"] = {"topButtonList": [], "mainButtonDetailList": [], "moreButtonDetailList": []}
|
|
3456
|
+
if view_type == "table":
|
|
3457
|
+
data["viewgraphType"] = "tableView"
|
|
3458
|
+
data["beingShowTitleQue"] = False
|
|
3459
|
+
data["titleQue"] = None
|
|
3460
|
+
data["groupQueId"] = None
|
|
3461
|
+
elif view_type == "card":
|
|
3462
|
+
data["viewgraphType"] = "cardView"
|
|
3463
|
+
data["beingShowTitleQue"] = True
|
|
3464
|
+
data["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
|
|
3465
|
+
data["groupQueId"] = None
|
|
3466
|
+
elif view_type == "board":
|
|
3467
|
+
data["viewgraphType"] = "boardView"
|
|
3468
|
+
data["beingShowTitleQue"] = True
|
|
3469
|
+
data["titleQue"] = visible_que_id_values[0] if visible_que_id_values else None
|
|
3470
|
+
data["groupQueId"] = group_que_id
|
|
3471
|
+
return data
|
|
2585
3472
|
|
|
2586
3473
|
|
|
2587
3474
|
def _infer_status_field_id(fields: list[dict[str, Any]]) -> str | None:
|
|
@@ -2615,6 +3502,7 @@ def _normalize_flow_stage_failure(stage: JSONObject, *, profile: str, app_key: s
|
|
|
2615
3502
|
request_id = first_error.get("request_id")
|
|
2616
3503
|
backend_code = first_error.get("backend_code")
|
|
2617
3504
|
http_status = first_error.get("http_status")
|
|
3505
|
+
lowered_detail = detail_text.lower()
|
|
2618
3506
|
if "must declare status field" in detail_text:
|
|
2619
3507
|
return _failed(
|
|
2620
3508
|
"STATUS_FIELD_REQUIRED",
|
|
@@ -2647,14 +3535,45 @@ def _normalize_flow_stage_failure(stage: JSONObject, *, profile: str, app_key: s
|
|
|
2647
3535
|
backend_code=backend_code,
|
|
2648
3536
|
http_status=http_status,
|
|
2649
3537
|
)
|
|
3538
|
+
if (
|
|
3539
|
+
"run solution_build_app first" in lowered_detail
|
|
3540
|
+
or "run solution_build_app_flow first" in lowered_detail
|
|
3541
|
+
or ("is not defined yet" in lowered_detail and "solution_build_" in lowered_detail)
|
|
3542
|
+
):
|
|
3543
|
+
return _failed(
|
|
3544
|
+
"FLOW_STAGE_CONTEXT_MISSING",
|
|
3545
|
+
"workflow apply lost the app context required by the internal flow builder",
|
|
3546
|
+
details={
|
|
3547
|
+
"app_key": app_key,
|
|
3548
|
+
"entity_id": entity.get("entity_id"),
|
|
3549
|
+
"existing_fields": entity.get("fields") or [],
|
|
3550
|
+
"internal_detail": detail_text,
|
|
3551
|
+
"stage_result": public_stage_result,
|
|
3552
|
+
},
|
|
3553
|
+
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3554
|
+
request_id=request_id,
|
|
3555
|
+
backend_code=backend_code,
|
|
3556
|
+
http_status=http_status,
|
|
3557
|
+
)
|
|
3558
|
+
message = detail_text or "failed to apply workflow patch"
|
|
3559
|
+
details = {"app_key": app_key, "entity_id": entity.get("entity_id"), "stage_result": public_stage_result}
|
|
3560
|
+
public_http_status = http_status
|
|
3561
|
+
if http_status == 404:
|
|
3562
|
+
message = "workflow write route is unavailable for this app in the current route"
|
|
3563
|
+
details["transport_error"] = {
|
|
3564
|
+
"http_status": http_status,
|
|
3565
|
+
"backend_code": backend_code,
|
|
3566
|
+
"category": "http",
|
|
3567
|
+
}
|
|
3568
|
+
public_http_status = None
|
|
2650
3569
|
return _failed(
|
|
2651
3570
|
stage_error_code,
|
|
2652
|
-
|
|
2653
|
-
details=
|
|
3571
|
+
message,
|
|
3572
|
+
details=details,
|
|
2654
3573
|
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2655
3574
|
request_id=request_id,
|
|
2656
3575
|
backend_code=backend_code,
|
|
2657
|
-
http_status=
|
|
3576
|
+
http_status=public_http_status,
|
|
2658
3577
|
)
|
|
2659
3578
|
|
|
2660
3579
|
|