@qingflow-tech/qingflow-app-user-mcp 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +255 -0
- package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder/references/create-app.md +149 -0
- package/skills/qingflow-app-builder/references/environments.md +63 -0
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +107 -0
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
- package/skills/qingflow-app-builder/references/update-flow.md +158 -0
- package/skills/qingflow-app-builder/references/update-layout.md +68 -0
- package/skills/qingflow-app-builder/references/update-schema.md +72 -0
- package/skills/qingflow-app-builder/references/update-views.md +284 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/skills/qingflow-app-user/SKILL.md +12 -11
- package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
- package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
- package/skills/qingflow-app-user/references/record-patterns.md +5 -5
- package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
- package/skills/qingflow-mcp-setup/SKILL.md +113 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-record-analysis/SKILL.md +6 -7
- package/skills/qingflow-record-analysis/manifest.yaml +10 -0
- package/skills/qingflow-record-delete/SKILL.md +5 -3
- package/skills/qingflow-record-import/SKILL.md +6 -2
- package/skills/qingflow-record-insert/SKILL.md +48 -4
- package/skills/qingflow-record-insert/manifest.yaml +6 -0
- package/skills/qingflow-record-update/SKILL.md +36 -24
- package/skills/qingflow-task-ops/SKILL.md +25 -25
- package/skills/qingflow-task-ops/references/environments.md +0 -1
- package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +41 -2
- package/src/qingflow_mcp/builder_facade/service.py +2743 -423
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +30 -4
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +54 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +238 -8
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +24 -16
- package/src/qingflow_mcp/response_trim.py +119 -12
- package/src/qingflow_mcp/server.py +17 -14
- package/src/qingflow_mcp/server_app_builder.py +29 -7
- package/src/qingflow_mcp/server_app_user.py +23 -24
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
- package/src/qingflow_mcp/tools/app_tools.py +237 -51
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +230 -33
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +293 -40
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +134 -8
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +2305 -442
- package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -5,6 +5,215 @@ import re
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
DEFAULT_ICON_COLOR = "qing-orange"
|
|
8
|
+
WORKSPACE_ICON_COLORS: tuple[str, ...] = (
|
|
9
|
+
"qing-orange",
|
|
10
|
+
"yellow",
|
|
11
|
+
"green",
|
|
12
|
+
"emerald",
|
|
13
|
+
"blue",
|
|
14
|
+
"azure",
|
|
15
|
+
"indigo",
|
|
16
|
+
"qing-purple",
|
|
17
|
+
"purple",
|
|
18
|
+
"pink",
|
|
19
|
+
"red",
|
|
20
|
+
"orange",
|
|
21
|
+
)
|
|
22
|
+
WORKSPACE_ICON_NAMES: tuple[str, ...] = (
|
|
23
|
+
"user",
|
|
24
|
+
"user-group",
|
|
25
|
+
"user-remove",
|
|
26
|
+
"user-add",
|
|
27
|
+
"user-circle",
|
|
28
|
+
"base-camera",
|
|
29
|
+
"view-grid",
|
|
30
|
+
"inbox",
|
|
31
|
+
"inbox-in",
|
|
32
|
+
"share",
|
|
33
|
+
"sitemap",
|
|
34
|
+
"airplane",
|
|
35
|
+
"template",
|
|
36
|
+
"music-note",
|
|
37
|
+
"movie-play",
|
|
38
|
+
"clock",
|
|
39
|
+
"document",
|
|
40
|
+
"document-search",
|
|
41
|
+
"clipboard-check",
|
|
42
|
+
"document-download",
|
|
43
|
+
"document-text",
|
|
44
|
+
"clipboard-copy",
|
|
45
|
+
"presentation-chart-bar",
|
|
46
|
+
"chart-square-bar",
|
|
47
|
+
"database",
|
|
48
|
+
"server",
|
|
49
|
+
"calendar",
|
|
50
|
+
"mail",
|
|
51
|
+
"annotation",
|
|
52
|
+
"chat",
|
|
53
|
+
"bell",
|
|
54
|
+
"key",
|
|
55
|
+
"shopping-bag",
|
|
56
|
+
"download",
|
|
57
|
+
"eye",
|
|
58
|
+
"eye-off",
|
|
59
|
+
"emoji-happy",
|
|
60
|
+
"emoji-sad",
|
|
61
|
+
"sun",
|
|
62
|
+
"moon",
|
|
63
|
+
"cloud",
|
|
64
|
+
"lightning-bolt",
|
|
65
|
+
"fire",
|
|
66
|
+
"star",
|
|
67
|
+
"sparkles",
|
|
68
|
+
"heart",
|
|
69
|
+
"cake",
|
|
70
|
+
"gift",
|
|
71
|
+
"light-bulb",
|
|
72
|
+
"exclamation",
|
|
73
|
+
"cog",
|
|
74
|
+
"thumb-up",
|
|
75
|
+
"thumb-down",
|
|
76
|
+
"cloud-download",
|
|
77
|
+
"cloud-upload",
|
|
78
|
+
"printer",
|
|
79
|
+
"phone-incoming",
|
|
80
|
+
"phone-missed-call",
|
|
81
|
+
"terminal",
|
|
82
|
+
"search-circle",
|
|
83
|
+
"x-circle",
|
|
84
|
+
"check-circle",
|
|
85
|
+
"exclamation-circle",
|
|
86
|
+
"question-mark-circle",
|
|
87
|
+
"information-circle",
|
|
88
|
+
"academic-cap",
|
|
89
|
+
"briefcase",
|
|
90
|
+
"home",
|
|
91
|
+
"phone",
|
|
92
|
+
"photograph",
|
|
93
|
+
"puzzle",
|
|
94
|
+
"color-swatch",
|
|
95
|
+
"lock-open",
|
|
96
|
+
"lock-closed",
|
|
97
|
+
"shield-check",
|
|
98
|
+
"shield-exclamation",
|
|
99
|
+
"currency-dollar",
|
|
100
|
+
"currency-yen",
|
|
101
|
+
"globe",
|
|
102
|
+
"at-symbol",
|
|
103
|
+
"slack",
|
|
104
|
+
"microphone",
|
|
105
|
+
"speakerphone",
|
|
106
|
+
"trash",
|
|
107
|
+
"book-open",
|
|
108
|
+
"truck",
|
|
109
|
+
"filter",
|
|
110
|
+
"essetional-filter-search",
|
|
111
|
+
"essetional-filter-tick",
|
|
112
|
+
"table",
|
|
113
|
+
"calculator",
|
|
114
|
+
"location-radar",
|
|
115
|
+
"essetional-weight",
|
|
116
|
+
"school-award",
|
|
117
|
+
"comp-cloud-connection",
|
|
118
|
+
"comp-cloud-remove",
|
|
119
|
+
"comp-cpu-charge",
|
|
120
|
+
"comp-cpu-setting",
|
|
121
|
+
"comp-cpu",
|
|
122
|
+
"comp-devices",
|
|
123
|
+
"comp-driver-2",
|
|
124
|
+
"comp-driver-refresh",
|
|
125
|
+
"location-global",
|
|
126
|
+
"location-location",
|
|
127
|
+
"location-map",
|
|
128
|
+
"location-gps",
|
|
129
|
+
"essetional-ranking",
|
|
130
|
+
"chart-bar",
|
|
131
|
+
"business-graph",
|
|
132
|
+
"business-status-up",
|
|
133
|
+
"business-trend-down",
|
|
134
|
+
"business-trend-up",
|
|
135
|
+
"business-presention-chart",
|
|
136
|
+
"business-favorite-chart",
|
|
137
|
+
"business-health",
|
|
138
|
+
"receipt-refund",
|
|
139
|
+
"receipt-tax",
|
|
140
|
+
"money-receipt-2-1",
|
|
141
|
+
"money-transaction-minus",
|
|
142
|
+
"action-hourglass-full",
|
|
143
|
+
"action-work",
|
|
144
|
+
"bug-f",
|
|
145
|
+
"essetional-pet",
|
|
146
|
+
"files-folder",
|
|
147
|
+
"badge-check",
|
|
148
|
+
"money-wallet-1",
|
|
149
|
+
"money-ticket",
|
|
150
|
+
"money-money",
|
|
151
|
+
"money-tag",
|
|
152
|
+
"money-wallet-2",
|
|
153
|
+
"business-personalcard",
|
|
154
|
+
"car-airplane",
|
|
155
|
+
"car-bus",
|
|
156
|
+
"car-car",
|
|
157
|
+
"car-driving",
|
|
158
|
+
"car-gas-station",
|
|
159
|
+
"car-smart-car",
|
|
160
|
+
"car-ship",
|
|
161
|
+
"location-map-1",
|
|
162
|
+
"location-route-square",
|
|
163
|
+
"cone",
|
|
164
|
+
"design-brush-4",
|
|
165
|
+
"paint-roll",
|
|
166
|
+
"wrench-f",
|
|
167
|
+
"essetional-reserve",
|
|
168
|
+
"essetional-broom",
|
|
169
|
+
"design-brush-2",
|
|
170
|
+
"essetional-judge",
|
|
171
|
+
"design-bucket",
|
|
172
|
+
"palette",
|
|
173
|
+
"comp-electricity",
|
|
174
|
+
"vial",
|
|
175
|
+
"beaker",
|
|
176
|
+
"leaf-f",
|
|
177
|
+
"cursor-click",
|
|
178
|
+
"solid-search-alt-2",
|
|
179
|
+
"md-library",
|
|
180
|
+
"building-3",
|
|
181
|
+
"office-building",
|
|
182
|
+
"building-hospital",
|
|
183
|
+
"school",
|
|
184
|
+
"store",
|
|
185
|
+
"video-camera-vintage-f",
|
|
186
|
+
"comp-monitor",
|
|
187
|
+
"delivery-truck",
|
|
188
|
+
"delivery-box-1",
|
|
189
|
+
"delivery-box-add",
|
|
190
|
+
"delivery-box-remove",
|
|
191
|
+
"settings-setting-3",
|
|
192
|
+
"document-duplicate",
|
|
193
|
+
"essetional-flag-2",
|
|
194
|
+
"flag",
|
|
195
|
+
"icon-currency-dollar",
|
|
196
|
+
"clipboard-list",
|
|
197
|
+
"save-as",
|
|
198
|
+
"wifi",
|
|
199
|
+
"status-online",
|
|
200
|
+
"scissors",
|
|
201
|
+
"globe-alt",
|
|
202
|
+
"ban",
|
|
203
|
+
"finger-print",
|
|
204
|
+
"qrcode",
|
|
205
|
+
"paper-clip",
|
|
206
|
+
"translate",
|
|
207
|
+
"cube-transparent",
|
|
208
|
+
"variable",
|
|
209
|
+
"switch-vertical",
|
|
210
|
+
"sports-baseball",
|
|
211
|
+
"sports-basketball",
|
|
212
|
+
"sports-soccer",
|
|
213
|
+
"sports-football",
|
|
214
|
+
"sports-volleyball",
|
|
215
|
+
)
|
|
216
|
+
GENERIC_WORKSPACE_ICON_NAMES: tuple[str, ...] = ("template",)
|
|
8
217
|
DEFAULT_ICON_STYLE_POOL: tuple[tuple[str, str], ...] = (
|
|
9
218
|
("briefcase", "qing-orange"),
|
|
10
219
|
("calendar", "emerald"),
|
|
@@ -115,6 +324,91 @@ def parse_workspace_icon(value: str | None) -> tuple[str | None, str | None, str
|
|
|
115
324
|
return normalize_workspace_icon_name(stripped), None, None
|
|
116
325
|
|
|
117
326
|
|
|
327
|
+
def workspace_icon_config(value: str | None) -> dict[str, str | None]:
|
|
328
|
+
icon_name, icon_color, icon_text = parse_workspace_icon(value)
|
|
329
|
+
raw = str(value).strip() if value not in (None, "") else None
|
|
330
|
+
return {
|
|
331
|
+
"icon_name": icon_name,
|
|
332
|
+
"icon_color": icon_color,
|
|
333
|
+
"icon_text": icon_text,
|
|
334
|
+
"raw": raw,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def workspace_icon_catalog_payload() -> dict[str, object]:
|
|
339
|
+
return {
|
|
340
|
+
"icon_names": list(WORKSPACE_ICON_NAMES),
|
|
341
|
+
"icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
342
|
+
"generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
343
|
+
"notes": [
|
|
344
|
+
"Use explicit icon + color for app/package/portal creation.",
|
|
345
|
+
"Do not use template for newly created workspace resources.",
|
|
346
|
+
"The CLI validates candidates only; it does not infer an icon from business names.",
|
|
347
|
+
],
|
|
348
|
+
"common_examples": {
|
|
349
|
+
"employee": ["business-personalcard", "user-group", "user"],
|
|
350
|
+
"task": ["clipboard-check", "action-work"],
|
|
351
|
+
"worklog": ["clock", "action-hourglass-full"],
|
|
352
|
+
"order": ["delivery-box-1", "shopping-bag"],
|
|
353
|
+
"payment": ["money-receipt-2-1", "money-wallet-1"],
|
|
354
|
+
"opportunity": ["business-graph", "business-trend-up"],
|
|
355
|
+
"dashboard": ["view-grid", "chart-square-bar", "presentation-chart-bar"],
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def validate_workspace_icon_choice(
|
|
361
|
+
*,
|
|
362
|
+
icon: str | None,
|
|
363
|
+
color: str | None,
|
|
364
|
+
require_explicit: bool,
|
|
365
|
+
disallow_generic: bool,
|
|
366
|
+
) -> tuple[bool, str | None, str | None, dict[str, object]]:
|
|
367
|
+
normalized_icon = _normalize_workspace_icon_candidate(icon)
|
|
368
|
+
normalized_color = str(color or "").strip() or None
|
|
369
|
+
details: dict[str, object] = {
|
|
370
|
+
"icon": icon,
|
|
371
|
+
"normalized_icon": normalized_icon,
|
|
372
|
+
"color": color,
|
|
373
|
+
"icon_catalog_command": "qingflow --json builder icon catalog",
|
|
374
|
+
}
|
|
375
|
+
if require_explicit and not normalized_icon:
|
|
376
|
+
return False, "WORKSPACE_ICON_REQUIRED", "icon is required when creating a workspace resource", details
|
|
377
|
+
if require_explicit and not normalized_color:
|
|
378
|
+
return False, "WORKSPACE_ICON_COLOR_REQUIRED", "color is required when creating a workspace resource", details
|
|
379
|
+
if normalized_icon and normalized_icon not in WORKSPACE_ICON_NAMES:
|
|
380
|
+
details["allowed_icon_names"] = list(WORKSPACE_ICON_NAMES)
|
|
381
|
+
return False, "WORKSPACE_ICON_NOT_FOUND", "icon is not in the workspace icon catalog", details
|
|
382
|
+
if normalized_color and normalized_color not in WORKSPACE_ICON_COLORS:
|
|
383
|
+
details["allowed_icon_colors"] = list(WORKSPACE_ICON_COLORS)
|
|
384
|
+
return False, "WORKSPACE_ICON_COLOR_NOT_FOUND", "color is not in the workspace icon color catalog", details
|
|
385
|
+
if disallow_generic and normalized_icon in GENERIC_WORKSPACE_ICON_NAMES:
|
|
386
|
+
details["generic_icon_names"] = list(GENERIC_WORKSPACE_ICON_NAMES)
|
|
387
|
+
return False, "GENERIC_WORKSPACE_ICON_NOT_ALLOWED", "template is a generic icon and is not allowed for new workspace resources", details
|
|
388
|
+
return True, None, None, details
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _normalize_workspace_icon_candidate(icon: str | None) -> str | None:
|
|
392
|
+
if not icon:
|
|
393
|
+
return None
|
|
394
|
+
raw = str(icon).strip()
|
|
395
|
+
if not raw:
|
|
396
|
+
return None
|
|
397
|
+
if _looks_like_icon_json(raw):
|
|
398
|
+
try:
|
|
399
|
+
payload = json.loads(raw)
|
|
400
|
+
except Exception:
|
|
401
|
+
return None
|
|
402
|
+
return _normalize_workspace_icon_candidate(payload.get("iconName"))
|
|
403
|
+
normalized = raw.lower()
|
|
404
|
+
if normalized in WORKSPACE_ICON_NAMES:
|
|
405
|
+
return normalized
|
|
406
|
+
legacy = LEGACY_EX_ICON_MAP.get(normalized)
|
|
407
|
+
if legacy in WORKSPACE_ICON_NAMES:
|
|
408
|
+
return legacy
|
|
409
|
+
return normalized
|
|
410
|
+
|
|
411
|
+
|
|
118
412
|
def encode_workspace_icon_with_defaults(
|
|
119
413
|
*,
|
|
120
414
|
icon: str | None,
|
|
@@ -5,7 +5,7 @@ from copy import deepcopy
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
|
-
from ..errors import QingflowApiError
|
|
8
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error
|
|
9
9
|
from ..tools.app_tools import AppTools
|
|
10
10
|
from ..tools.navigation_tools import NavigationTools
|
|
11
11
|
from ..tools.package_tools import PackageTools
|
|
@@ -76,7 +76,8 @@ class SolutionExecutor:
|
|
|
76
76
|
except Exception as exc: # noqa: BLE001
|
|
77
77
|
store.record_step_failed(step.step_name, str(exc), debug_context=debug_context)
|
|
78
78
|
return store.summary()
|
|
79
|
-
store.
|
|
79
|
+
final_status = "partial_success" if _artifacts_have_post_write_readback_pending(store.data.get("artifacts", {})) else "success"
|
|
80
|
+
store.mark_finished(status=final_status)
|
|
80
81
|
return store.summary()
|
|
81
82
|
|
|
82
83
|
def _repair_start_index(self, compiled: CompiledSolution, store: RunArtifactStore) -> int:
|
|
@@ -203,6 +204,11 @@ class SolutionExecutor:
|
|
|
203
204
|
dash_key = store.get_artifact("portal", "dash_key")
|
|
204
205
|
if dash_key:
|
|
205
206
|
self.portal_tools.portal_publish(profile=profile, dash_key=dash_key)
|
|
207
|
+
store.set_artifact(
|
|
208
|
+
"portal",
|
|
209
|
+
"publish",
|
|
210
|
+
{"published": True, "write_executed": True, "safe_to_retry": False},
|
|
211
|
+
)
|
|
206
212
|
self._refresh_portal_artifact(profile=profile, store=store, being_draft=False, artifact_key="published_result")
|
|
207
213
|
return
|
|
208
214
|
if step_name == "publish.navigation" and publish and compiled.normalized_spec.publish_policy.navigation:
|
|
@@ -347,7 +353,32 @@ class SolutionExecutor:
|
|
|
347
353
|
updated_items.insert(insert_at, item)
|
|
348
354
|
self.package_tools.package_sort_items(profile=profile, tag_id=tag_id, tag_items=updated_items)
|
|
349
355
|
|
|
350
|
-
|
|
356
|
+
try:
|
|
357
|
+
verified_detail = _verify_package_attachment(self.package_tools, profile=profile, tag_id=tag_id, app_key=app_key)
|
|
358
|
+
except Exception as exc: # noqa: BLE001
|
|
359
|
+
api_error = _coerce_qingflow_error(exc)
|
|
360
|
+
if api_error is None or not _is_permission_restricted_error(api_error):
|
|
361
|
+
raise
|
|
362
|
+
store.set_artifact(
|
|
363
|
+
"package",
|
|
364
|
+
"attachment_readback",
|
|
365
|
+
_post_write_readback_artifact(
|
|
366
|
+
resource="package_attach",
|
|
367
|
+
target={"tag_id": tag_id, "app_key": app_key},
|
|
368
|
+
error=api_error,
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
self._record_package_attachment(
|
|
372
|
+
store,
|
|
373
|
+
entity.entity_id,
|
|
374
|
+
app_artifact,
|
|
375
|
+
tag_id=tag_id,
|
|
376
|
+
attached=True,
|
|
377
|
+
reused=False,
|
|
378
|
+
readback_status="unavailable",
|
|
379
|
+
readback_verified=False,
|
|
380
|
+
)
|
|
381
|
+
return
|
|
351
382
|
verified_result = verified_detail.get("result") if isinstance(verified_detail.get("result"), dict) else {}
|
|
352
383
|
verified_items = [deepcopy(existing) for existing in verified_result.get("tagItems", []) if isinstance(existing, dict)]
|
|
353
384
|
if not any(_package_item_app_key(existing) == app_key for existing in verified_items):
|
|
@@ -369,12 +400,18 @@ class SolutionExecutor:
|
|
|
369
400
|
tag_id: int,
|
|
370
401
|
attached: bool,
|
|
371
402
|
reused: bool,
|
|
403
|
+
readback_status: str = "verified",
|
|
404
|
+
readback_verified: bool = True,
|
|
372
405
|
) -> None:
|
|
373
406
|
next_artifact = deepcopy(app_artifact)
|
|
374
407
|
next_artifact["package_attachment"] = {
|
|
375
408
|
"tag_id": tag_id,
|
|
376
409
|
"attached": attached,
|
|
377
410
|
"reused": reused,
|
|
411
|
+
"readback_status": readback_status,
|
|
412
|
+
"readback_verified": readback_verified,
|
|
413
|
+
"write_executed": not reused,
|
|
414
|
+
"safe_to_retry": reused or not attached,
|
|
378
415
|
}
|
|
379
416
|
store.set_artifact("apps", entity_id, next_artifact)
|
|
380
417
|
|
|
@@ -698,12 +735,23 @@ class SolutionExecutor:
|
|
|
698
735
|
api_error = _coerce_qingflow_error(exc)
|
|
699
736
|
if api_error is None or not _is_permission_restricted_error(api_error):
|
|
700
737
|
raise
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
738
|
+
if not result:
|
|
739
|
+
raise _required_state_read_blocked_error(
|
|
740
|
+
resource="portal",
|
|
741
|
+
message=f"portal update requires readable draft state for dash '{dash_key}'",
|
|
742
|
+
error=api_error,
|
|
743
|
+
details={"dash_key": dash_key},
|
|
744
|
+
) from exc
|
|
745
|
+
base_payload = {}
|
|
746
|
+
store.set_artifact(
|
|
747
|
+
"portal",
|
|
748
|
+
"draft_readback_before_update",
|
|
749
|
+
_post_write_readback_artifact(
|
|
750
|
+
resource="portal",
|
|
751
|
+
target={"dash_key": dash_key, "phase": "created_portal_draft_readback"},
|
|
752
|
+
error=api_error,
|
|
753
|
+
),
|
|
754
|
+
)
|
|
707
755
|
update_payload = self._resolve_portal_payload(compiled.portal_plan["update_payload"], store, base_payload=base_payload)
|
|
708
756
|
self.portal_tools.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
709
757
|
self._refresh_portal_artifact(profile=profile, store=store, being_draft=True, artifact_key="draft_result")
|
|
@@ -744,7 +792,19 @@ class SolutionExecutor:
|
|
|
744
792
|
return
|
|
745
793
|
try:
|
|
746
794
|
result = self.portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
|
|
747
|
-
except Exception: # noqa: BLE001
|
|
795
|
+
except Exception as exc: # noqa: BLE001
|
|
796
|
+
api_error = _coerce_qingflow_error(exc)
|
|
797
|
+
if api_error is None:
|
|
798
|
+
raise
|
|
799
|
+
store.set_artifact(
|
|
800
|
+
"portal",
|
|
801
|
+
f"{artifact_key}_readback",
|
|
802
|
+
_post_write_readback_artifact(
|
|
803
|
+
resource="portal",
|
|
804
|
+
target={"dash_key": dash_key, "being_draft": being_draft, "artifact_key": artifact_key},
|
|
805
|
+
error=api_error,
|
|
806
|
+
),
|
|
807
|
+
)
|
|
748
808
|
return
|
|
749
809
|
store.set_artifact("portal", artifact_key, result)
|
|
750
810
|
store.set_artifact("portal", "result", result)
|
|
@@ -2112,10 +2172,9 @@ def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | Non
|
|
|
2112
2172
|
|
|
2113
2173
|
|
|
2114
2174
|
def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
backend_code = None
|
|
2175
|
+
if is_auth_like_error(error):
|
|
2176
|
+
return False
|
|
2177
|
+
backend_code = backend_code_int(error)
|
|
2119
2178
|
if backend_code != 50004:
|
|
2120
2179
|
return False
|
|
2121
2180
|
message = error.message or ""
|
|
@@ -2152,7 +2211,9 @@ def _coerce_qingflow_error(error: Exception) -> QingflowApiError | None:
|
|
|
2152
2211
|
|
|
2153
2212
|
|
|
2154
2213
|
def _is_permission_restricted_error(error: QingflowApiError) -> bool:
|
|
2155
|
-
|
|
2214
|
+
if is_auth_like_error(error):
|
|
2215
|
+
return False
|
|
2216
|
+
return backend_code_int(error) in {40002, 40027}
|
|
2156
2217
|
|
|
2157
2218
|
|
|
2158
2219
|
def _required_state_read_blocked_error(
|
|
@@ -2182,6 +2243,42 @@ def _required_state_read_blocked_error(
|
|
|
2182
2243
|
)
|
|
2183
2244
|
|
|
2184
2245
|
|
|
2246
|
+
def _post_write_readback_artifact(
|
|
2247
|
+
*,
|
|
2248
|
+
resource: str,
|
|
2249
|
+
target: dict[str, Any],
|
|
2250
|
+
error: QingflowApiError,
|
|
2251
|
+
) -> dict[str, Any]:
|
|
2252
|
+
return {
|
|
2253
|
+
"resource": resource,
|
|
2254
|
+
"target": deepcopy(target),
|
|
2255
|
+
"readback_status": "unavailable",
|
|
2256
|
+
"readback_verified": False,
|
|
2257
|
+
"write_executed": True,
|
|
2258
|
+
"safe_to_retry": False,
|
|
2259
|
+
"transport_error": {
|
|
2260
|
+
"http_status": error.http_status,
|
|
2261
|
+
"backend_code": error.backend_code,
|
|
2262
|
+
"category": error.category,
|
|
2263
|
+
"request_id": error.request_id,
|
|
2264
|
+
},
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
|
|
2268
|
+
def _artifacts_have_post_write_readback_pending(value: Any) -> bool:
|
|
2269
|
+
if isinstance(value, dict):
|
|
2270
|
+
if (
|
|
2271
|
+
value.get("write_executed") is True
|
|
2272
|
+
and value.get("safe_to_retry") is False
|
|
2273
|
+
and value.get("readback_status") == "unavailable"
|
|
2274
|
+
):
|
|
2275
|
+
return True
|
|
2276
|
+
return any(_artifacts_have_post_write_readback_pending(item) for item in value.values())
|
|
2277
|
+
if isinstance(value, list):
|
|
2278
|
+
return any(_artifacts_have_post_write_readback_pending(item) for item in value)
|
|
2279
|
+
return False
|
|
2280
|
+
|
|
2281
|
+
|
|
2185
2282
|
def _portal_component_position(
|
|
2186
2283
|
source_type: Any,
|
|
2187
2284
|
*,
|