@josephyan/qingflow-app-builder-mcp 0.1.0-beta.13 → 0.1.0-beta.14
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/server_app_builder.py +10 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +19 -2
- package/src/qingflow_mcp/solution/executor.py +92 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +38 -2
- package/src/qingflow_mcp/tools/solution_tools.py +511 -22
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-builder-mcp@0.1.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.1.0-beta.14
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-builder-mcp@0.1.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.1.0-beta.14 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -229,6 +229,8 @@ def build_builder_server() -> FastMCP:
|
|
|
229
229
|
requirement_text: str = "",
|
|
230
230
|
package_tag_id: int = 0,
|
|
231
231
|
package_name: str = "",
|
|
232
|
+
app_key: str = "",
|
|
233
|
+
update_only: bool = False,
|
|
232
234
|
layout_style: str = "auto",
|
|
233
235
|
publish: bool = True,
|
|
234
236
|
run_label: str | None = None,
|
|
@@ -242,6 +244,8 @@ def build_builder_server() -> FastMCP:
|
|
|
242
244
|
requirement_text=requirement_text,
|
|
243
245
|
package_tag_id=package_tag_id,
|
|
244
246
|
package_name=package_name,
|
|
247
|
+
app_key=app_key,
|
|
248
|
+
update_only=update_only,
|
|
245
249
|
layout_style=layout_style,
|
|
246
250
|
publish=publish,
|
|
247
251
|
run_label=run_label,
|
|
@@ -278,6 +282,9 @@ def build_builder_server() -> FastMCP:
|
|
|
278
282
|
mode: str = "plan",
|
|
279
283
|
build_id: str = "",
|
|
280
284
|
app_spec: dict | None = None,
|
|
285
|
+
app_key: str = "",
|
|
286
|
+
package_tag_id: int = 0,
|
|
287
|
+
update_only: bool = False,
|
|
281
288
|
publish: bool = True,
|
|
282
289
|
run_label: str | None = None,
|
|
283
290
|
target: dict | None = None,
|
|
@@ -288,6 +295,9 @@ def build_builder_server() -> FastMCP:
|
|
|
288
295
|
mode=mode,
|
|
289
296
|
build_id=build_id,
|
|
290
297
|
app_spec=app_spec or {},
|
|
298
|
+
app_key=app_key,
|
|
299
|
+
package_tag_id=package_tag_id,
|
|
300
|
+
update_only=update_only,
|
|
291
301
|
publish=publish,
|
|
292
302
|
run_label=run_label,
|
|
293
303
|
target=target or {},
|
|
@@ -128,8 +128,13 @@ def compile_role(role: RoleSpec) -> CompiledRole:
|
|
|
128
128
|
return CompiledRole(role_id=role.role_id, name=role.name, payload=payload)
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
def build_execution_plan(
|
|
131
|
+
def build_execution_plan(
|
|
132
|
+
spec: SolutionSpec,
|
|
133
|
+
include_package: bool,
|
|
134
|
+
attach_package: bool | None = None,
|
|
135
|
+
) -> ExecutionPlan:
|
|
132
136
|
steps: list[ExecutionStep] = []
|
|
137
|
+
attach_package = include_package if attach_package is None else attach_package
|
|
133
138
|
if include_package:
|
|
134
139
|
steps.append(ExecutionStep("package.create", "package", spec.package.name or spec.solution_name, "创建应用包"))
|
|
135
140
|
for role in spec.roles:
|
|
@@ -139,7 +144,19 @@ def build_execution_plan(spec: SolutionSpec, include_package: bool) -> Execution
|
|
|
139
144
|
entity_ref = entity.entity_id
|
|
140
145
|
dependencies = ["package.create"] if include_package else []
|
|
141
146
|
steps.append(ExecutionStep(f"app.create.{entity_ref}", "app", entity_ref, f"创建应用 {entity.display_name}", dependencies.copy()))
|
|
142
|
-
|
|
147
|
+
last_form_step = f"app.create.{entity_ref}"
|
|
148
|
+
if attach_package:
|
|
149
|
+
steps.append(
|
|
150
|
+
ExecutionStep(
|
|
151
|
+
f"package.attach.{entity_ref}",
|
|
152
|
+
"package_attach",
|
|
153
|
+
entity_ref,
|
|
154
|
+
f"将应用加入应用包 {entity.display_name}",
|
|
155
|
+
[f"app.create.{entity_ref}"],
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
last_form_step = f"package.attach.{entity_ref}"
|
|
159
|
+
steps.append(ExecutionStep(f"form.base.{entity_ref}", "form", entity_ref, f"写入基础表单 {entity.display_name}", [last_form_step]))
|
|
143
160
|
last_form_step = f"form.base.{entity_ref}"
|
|
144
161
|
if any(field.type.value == "relation" for field in entity.fields):
|
|
145
162
|
steps.append(ExecutionStep(f"form.relations.{entity_ref}", "form", entity_ref, f"回填关联字段 {entity.display_name}", [f"form.base.{entity_ref}"]))
|
|
@@ -24,6 +24,7 @@ from .spec_models import FieldType
|
|
|
24
24
|
NAVIGATION_PLUGIN_ID = 45
|
|
25
25
|
GRID_COMPONENT_PLUGIN_ID = 34
|
|
26
26
|
BI_CHART_COMPONENT_TYPE = 9
|
|
27
|
+
PACKAGE_ITEM_TYPE_FORM = 1
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class SolutionExecutor:
|
|
@@ -90,6 +91,17 @@ class SolutionExecutor:
|
|
|
90
91
|
context["resource"] = "package"
|
|
91
92
|
context["package_name"] = compiled.normalized_spec.package.name or compiled.normalized_spec.solution_name
|
|
92
93
|
return context
|
|
94
|
+
if step_name.startswith("package.attach."):
|
|
95
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
96
|
+
context.update(
|
|
97
|
+
{
|
|
98
|
+
"resource": "package_attach",
|
|
99
|
+
"entity_id": entity.entity_id,
|
|
100
|
+
"display_name": entity.display_name,
|
|
101
|
+
"package_tag_id": self._current_store.get_artifact("package", "tag_id") if hasattr(self, "_current_store") else None,
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
return context
|
|
93
105
|
if step_name == "portal.create":
|
|
94
106
|
context["resource"] = "portal"
|
|
95
107
|
context["portal_name"] = compiled.normalized_spec.portal.name
|
|
@@ -125,6 +137,10 @@ class SolutionExecutor:
|
|
|
125
137
|
if step_name == "package.create":
|
|
126
138
|
self._create_package(profile, compiled, store)
|
|
127
139
|
return
|
|
140
|
+
if step_name.startswith("package.attach."):
|
|
141
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
142
|
+
self._attach_app_to_package(profile, entity, store)
|
|
143
|
+
return
|
|
128
144
|
if step_name.startswith("role.create."):
|
|
129
145
|
role = self._role_from_step(compiled, step_name)
|
|
130
146
|
self._create_role(profile, role, store)
|
|
@@ -261,6 +277,10 @@ class SolutionExecutor:
|
|
|
261
277
|
existing_artifact = store.get_artifact("apps", entity.entity_id, {}) or {}
|
|
262
278
|
existing_app_key = existing_artifact.get("app_key")
|
|
263
279
|
if isinstance(existing_app_key, str) and existing_app_key:
|
|
280
|
+
next_artifact = deepcopy(existing_artifact)
|
|
281
|
+
next_artifact["reused"] = True
|
|
282
|
+
next_artifact.setdefault("target_mode", "update")
|
|
283
|
+
store.set_artifact("apps", entity.entity_id, next_artifact)
|
|
264
284
|
return
|
|
265
285
|
payload = self._resolve_app_payload(entity.app_create_payload, store)
|
|
266
286
|
result = self.app_tools.app_create(profile=profile, payload=payload)
|
|
@@ -283,6 +303,64 @@ class SolutionExecutor:
|
|
|
283
303
|
store.set_artifact("apps", entity.entity_id, app_artifact)
|
|
284
304
|
raise
|
|
285
305
|
|
|
306
|
+
def _attach_app_to_package(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
307
|
+
tag_id = store.get_artifact("package", "tag_id")
|
|
308
|
+
if not isinstance(tag_id, int) or tag_id <= 0:
|
|
309
|
+
return
|
|
310
|
+
app_artifact = store.get_artifact("apps", entity.entity_id, {}) or {}
|
|
311
|
+
app_key = app_artifact.get("app_key")
|
|
312
|
+
if not isinstance(app_key, str) or not app_key:
|
|
313
|
+
raise ValueError(f"missing app_key for package attach on entity '{entity.entity_id}'")
|
|
314
|
+
|
|
315
|
+
package_detail = self.package_tools.package_get(profile=profile, tag_id=tag_id, include_raw=True)
|
|
316
|
+
package_result = package_detail.get("result") if isinstance(package_detail.get("result"), dict) else {}
|
|
317
|
+
tag_items = [deepcopy(item) for item in package_result.get("tagItems", []) if isinstance(item, dict)]
|
|
318
|
+
if any(_package_item_app_key(item) == app_key for item in tag_items):
|
|
319
|
+
self._record_package_attachment(store, entity.entity_id, app_artifact, tag_id=tag_id, attached=True, reused=True)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
item = {
|
|
323
|
+
"itemType": PACKAGE_ITEM_TYPE_FORM,
|
|
324
|
+
"appKey": app_key,
|
|
325
|
+
"title": entity.display_name,
|
|
326
|
+
"iconUrl": entity.app_create_payload.get("appIcon"),
|
|
327
|
+
}
|
|
328
|
+
insert_at = _resolve_package_item_insert_index(tag_items, entity.app_create_payload.get("ordinal"))
|
|
329
|
+
updated_items = list(tag_items)
|
|
330
|
+
updated_items.insert(insert_at, item)
|
|
331
|
+
self.package_tools.package_update(profile=profile, tag_id=tag_id, payload={"tagItems": updated_items})
|
|
332
|
+
|
|
333
|
+
verified_detail = self.package_tools.package_get(profile=profile, tag_id=tag_id, include_raw=True)
|
|
334
|
+
verified_result = verified_detail.get("result") if isinstance(verified_detail.get("result"), dict) else {}
|
|
335
|
+
verified_items = [deepcopy(existing) for existing in verified_result.get("tagItems", []) if isinstance(existing, dict)]
|
|
336
|
+
if not any(_package_item_app_key(existing) == app_key for existing in verified_items):
|
|
337
|
+
raise QingflowApiError(
|
|
338
|
+
category="runtime",
|
|
339
|
+
message=f"failed to attach app '{app_key}' to package '{tag_id}'",
|
|
340
|
+
details={"tag_id": tag_id, "app_key": app_key},
|
|
341
|
+
)
|
|
342
|
+
store.set_artifact("package", "result", verified_detail)
|
|
343
|
+
store.set_artifact("package", "tag_id", tag_id)
|
|
344
|
+
self._record_package_attachment(store, entity.entity_id, app_artifact, tag_id=tag_id, attached=True, reused=False)
|
|
345
|
+
|
|
346
|
+
def _record_package_attachment(
|
|
347
|
+
self,
|
|
348
|
+
store: RunArtifactStore,
|
|
349
|
+
entity_id: str,
|
|
350
|
+
app_artifact: dict[str, Any],
|
|
351
|
+
*,
|
|
352
|
+
tag_id: int,
|
|
353
|
+
attached: bool,
|
|
354
|
+
reused: bool,
|
|
355
|
+
) -> None:
|
|
356
|
+
next_artifact = deepcopy(app_artifact)
|
|
357
|
+
next_artifact["package_attachment"] = {
|
|
358
|
+
"tag_id": tag_id,
|
|
359
|
+
"attached": attached,
|
|
360
|
+
"reused": reused,
|
|
361
|
+
}
|
|
362
|
+
store.set_artifact("apps", entity_id, next_artifact)
|
|
363
|
+
|
|
286
364
|
def _update_form_base(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
287
365
|
app_key = self._get_app_key(store, entity.entity_id)
|
|
288
366
|
payload = deepcopy(entity.form_base_payload)
|
|
@@ -2156,6 +2234,20 @@ def _flatten_questions(value: Any) -> list[dict[str, Any]]:
|
|
|
2156
2234
|
return []
|
|
2157
2235
|
|
|
2158
2236
|
|
|
2237
|
+
def _package_item_app_key(item: dict[str, Any]) -> str | None:
|
|
2238
|
+
item_type = item.get("itemType")
|
|
2239
|
+
if item_type is not None and item_type not in {PACKAGE_ITEM_TYPE_FORM, str(PACKAGE_ITEM_TYPE_FORM)}:
|
|
2240
|
+
return None
|
|
2241
|
+
app_key = item.get("appKey")
|
|
2242
|
+
return app_key if isinstance(app_key, str) and app_key else None
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
def _resolve_package_item_insert_index(tag_items: list[dict[str, Any]], ordinal: Any) -> int:
|
|
2246
|
+
if not isinstance(ordinal, int) or ordinal <= 0:
|
|
2247
|
+
return len(tag_items)
|
|
2248
|
+
return max(0, min(len(tag_items), ordinal - 1))
|
|
2249
|
+
|
|
2250
|
+
|
|
2159
2251
|
def _build_viewgraph_questions(schema: dict[str, Any], visible_que_ids: list[int]) -> list[dict[str, Any]]:
|
|
2160
2252
|
visible_map = {que_id: index for index, que_id in enumerate(visible_que_ids, start=1)}
|
|
2161
2253
|
questions: list[dict[str, Any]] = []
|
|
@@ -351,7 +351,22 @@ def _build_relation_field(*, requirement_text: str, entity_id: str) -> dict[str,
|
|
|
351
351
|
"relation field requires an explicit target entity; specify what it should relate to",
|
|
352
352
|
missing_required_fields=["relation.target_entity_id"],
|
|
353
353
|
invalid_field_types=["relation"],
|
|
354
|
-
details={
|
|
354
|
+
details={
|
|
355
|
+
"requested_field_type": "relation",
|
|
356
|
+
"suggested_requirement_hint": "例如:关联到客户档案。",
|
|
357
|
+
"suggested_patch": {
|
|
358
|
+
"entities": [
|
|
359
|
+
{
|
|
360
|
+
"entity_id": "target_entity",
|
|
361
|
+
"display_name": "目标应用",
|
|
362
|
+
"kind": EntityKind.master.value,
|
|
363
|
+
"title_field_id": "title",
|
|
364
|
+
"fields": [{"field_id": "title", "label": "标题", "type": FieldType.text.value, "required": True}],
|
|
365
|
+
"form_layout": {"rows": [{"field_ids": ["title"]}]},
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
},
|
|
369
|
+
},
|
|
355
370
|
)
|
|
356
371
|
target_entity_id = entity_id if target_phrase in {"当前应用", "当前表单", "自身", "本应用"} else _slugify_identifier(target_phrase, prefix="entity")
|
|
357
372
|
field = _clone_field_blueprint(FIELD_BLUEPRINTS_BY_TYPE[FieldType.relation.value])
|
|
@@ -366,7 +381,28 @@ def _build_subtable_field(*, requirement_text: str) -> dict[str, Any]:
|
|
|
366
381
|
"subtable field requires explicit child field definitions; specify the row fields first",
|
|
367
382
|
missing_required_fields=["subtable.subfields"],
|
|
368
383
|
invalid_field_types=["subtable"],
|
|
369
|
-
details={
|
|
384
|
+
details={
|
|
385
|
+
"requested_field_type": "subtable",
|
|
386
|
+
"suggested_requirement_hint": "例如:明细表字段包括 子项名称、数量、金额。",
|
|
387
|
+
"suggested_patch": {
|
|
388
|
+
"entities": [
|
|
389
|
+
{
|
|
390
|
+
"fields": [
|
|
391
|
+
{
|
|
392
|
+
"field_id": "detail_items",
|
|
393
|
+
"label": "明细",
|
|
394
|
+
"type": FieldType.subtable.value,
|
|
395
|
+
"subfields": [
|
|
396
|
+
{"field_id": "item_name", "label": "子项名称", "type": FieldType.text.value, "required": True},
|
|
397
|
+
{"field_id": "quantity", "label": "数量", "type": FieldType.number.value},
|
|
398
|
+
{"field_id": "amount", "label": "金额", "type": FieldType.amount.value},
|
|
399
|
+
],
|
|
400
|
+
}
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
},
|
|
405
|
+
},
|
|
370
406
|
)
|
|
371
407
|
field = _clone_field_blueprint(FIELD_BLUEPRINTS_BY_TYPE[FieldType.subtable.value])
|
|
372
408
|
field["subfields"] = subfields
|
|
@@ -45,6 +45,8 @@ STAGED_BUILD_MODE_ALIASES = {
|
|
|
45
45
|
"new": "plan",
|
|
46
46
|
"start": "plan",
|
|
47
47
|
"draft": "plan",
|
|
48
|
+
"update": "plan",
|
|
49
|
+
"modify": "plan",
|
|
48
50
|
"preview": "preflight",
|
|
49
51
|
"prepare": "preflight",
|
|
50
52
|
"run": "apply",
|
|
@@ -60,7 +62,7 @@ STAGE_TOOL_NAMES = {
|
|
|
60
62
|
"analytics_portal": "solution_build_analytics_portal",
|
|
61
63
|
"navigation": "solution_build_navigation",
|
|
62
64
|
}
|
|
63
|
-
SOLUTION_SCHEMA_STAGES = {"app", "flow", "views", "analytics_portal", "navigation", "app_flow", "all"}
|
|
65
|
+
SOLUTION_SCHEMA_STAGES = {"app", "app_update", "flow", "views", "analytics_portal", "navigation", "app_flow", "all"}
|
|
64
66
|
SOLUTION_SCHEMA_INTENTS = {"minimal", "full", "demo"}
|
|
65
67
|
SOLUTION_SCHEMA_STAGE_ALIASES = {
|
|
66
68
|
"all": "all",
|
|
@@ -71,6 +73,12 @@ SOLUTION_SCHEMA_STAGE_ALIASES = {
|
|
|
71
73
|
"form": "app",
|
|
72
74
|
"schema": "app",
|
|
73
75
|
"app_spec": "app",
|
|
76
|
+
"app_update": "app_update",
|
|
77
|
+
"update_app": "app_update",
|
|
78
|
+
"update": "app_update",
|
|
79
|
+
"更新应用": "app_update",
|
|
80
|
+
"更新表单": "app_update",
|
|
81
|
+
"更新schema": "app_update",
|
|
74
82
|
"应用": "app",
|
|
75
83
|
"表单": "app",
|
|
76
84
|
"flow": "flow",
|
|
@@ -163,6 +171,9 @@ class SolutionTools(ToolBase):
|
|
|
163
171
|
mode: str = "plan",
|
|
164
172
|
build_id: str = "",
|
|
165
173
|
app_spec: dict[str, Any] | None = None,
|
|
174
|
+
app_key: str = "",
|
|
175
|
+
package_tag_id: int = 0,
|
|
176
|
+
update_only: bool = False,
|
|
166
177
|
publish: bool = True,
|
|
167
178
|
run_label: str | None = None,
|
|
168
179
|
target: dict[str, Any] | None = None,
|
|
@@ -173,6 +184,9 @@ class SolutionTools(ToolBase):
|
|
|
173
184
|
mode=mode,
|
|
174
185
|
build_id=build_id,
|
|
175
186
|
app_spec=app_spec or {},
|
|
187
|
+
app_key=app_key,
|
|
188
|
+
package_tag_id=package_tag_id,
|
|
189
|
+
update_only=update_only,
|
|
176
190
|
publish=publish,
|
|
177
191
|
run_label=run_label,
|
|
178
192
|
target=target or {},
|
|
@@ -188,6 +202,8 @@ class SolutionTools(ToolBase):
|
|
|
188
202
|
requirement_text: str = "",
|
|
189
203
|
package_tag_id: int = 0,
|
|
190
204
|
package_name: str = "",
|
|
205
|
+
app_key: str = "",
|
|
206
|
+
update_only: bool = False,
|
|
191
207
|
layout_style: str = "auto",
|
|
192
208
|
publish: bool = True,
|
|
193
209
|
run_label: str | None = None,
|
|
@@ -201,6 +217,8 @@ class SolutionTools(ToolBase):
|
|
|
201
217
|
requirement_text=requirement_text,
|
|
202
218
|
package_tag_id=package_tag_id,
|
|
203
219
|
package_name=package_name,
|
|
220
|
+
app_key=app_key,
|
|
221
|
+
update_only=update_only,
|
|
204
222
|
layout_style=layout_style,
|
|
205
223
|
publish=publish,
|
|
206
224
|
run_label=run_label,
|
|
@@ -528,6 +546,9 @@ class SolutionTools(ToolBase):
|
|
|
528
546
|
mode: str,
|
|
529
547
|
build_id: str,
|
|
530
548
|
app_spec: dict[str, Any],
|
|
549
|
+
app_key: str = "",
|
|
550
|
+
package_tag_id: int = 0,
|
|
551
|
+
update_only: bool = False,
|
|
531
552
|
publish: bool,
|
|
532
553
|
run_label: str | None,
|
|
533
554
|
target: dict[str, Any],
|
|
@@ -537,6 +558,12 @@ class SolutionTools(ToolBase):
|
|
|
537
558
|
resolved_app_spec = deepcopy(app_spec) if isinstance(app_spec, dict) else {}
|
|
538
559
|
if not resolved_app_spec and build_id:
|
|
539
560
|
resolved_app_spec = self._load_generated_app_spec(build_id) or {}
|
|
561
|
+
resolved_target = _merge_app_target(
|
|
562
|
+
target,
|
|
563
|
+
app_key=app_key,
|
|
564
|
+
package_tag_id=package_tag_id,
|
|
565
|
+
update_only=update_only,
|
|
566
|
+
)
|
|
540
567
|
return self._stage_build(
|
|
541
568
|
profile=profile,
|
|
542
569
|
mode=mode,
|
|
@@ -546,7 +573,7 @@ class SolutionTools(ToolBase):
|
|
|
546
573
|
stage_model=AppBuildSpec,
|
|
547
574
|
publish=publish,
|
|
548
575
|
run_label=run_label,
|
|
549
|
-
target=
|
|
576
|
+
target=resolved_target,
|
|
550
577
|
repair_patch=repair_patch,
|
|
551
578
|
public_stage_name="app",
|
|
552
579
|
tool_name="solution_build_app",
|
|
@@ -563,6 +590,8 @@ class SolutionTools(ToolBase):
|
|
|
563
590
|
requirement_text: str,
|
|
564
591
|
package_tag_id: int,
|
|
565
592
|
package_name: str,
|
|
593
|
+
app_key: str = "",
|
|
594
|
+
update_only: bool = False,
|
|
566
595
|
layout_style: str,
|
|
567
596
|
publish: bool,
|
|
568
597
|
run_label: str | None,
|
|
@@ -571,6 +600,12 @@ class SolutionTools(ToolBase):
|
|
|
571
600
|
mode = _normalize_staged_build_mode(mode)
|
|
572
601
|
if mode in {"preflight", "plan"} and not build_id:
|
|
573
602
|
build_id = _generate_build_id(run_label=run_label, stage_name="app")
|
|
603
|
+
resolved_target = _merge_app_target(
|
|
604
|
+
{},
|
|
605
|
+
app_key=app_key,
|
|
606
|
+
package_tag_id=package_tag_id,
|
|
607
|
+
update_only=update_only,
|
|
608
|
+
)
|
|
574
609
|
resolved_package = self._resolve_builder_package_reference(
|
|
575
610
|
profile=profile,
|
|
576
611
|
package_tag_id=package_tag_id,
|
|
@@ -578,11 +613,20 @@ class SolutionTools(ToolBase):
|
|
|
578
613
|
)
|
|
579
614
|
if resolved_package.get("status") == "failed":
|
|
580
615
|
return resolved_package["response"]
|
|
616
|
+
existing_app = self._resolve_builder_existing_app_reference(
|
|
617
|
+
profile=profile,
|
|
618
|
+
app_key=app_key,
|
|
619
|
+
package_resolution=resolved_package,
|
|
620
|
+
update_only=_target_is_update_only(resolved_target),
|
|
621
|
+
)
|
|
622
|
+
if existing_app.get("status") == "failed":
|
|
623
|
+
return existing_app["response"]
|
|
624
|
+
effective_package_resolution = existing_app.get("package_resolution") if existing_app.get("package_resolution") else resolved_package
|
|
581
625
|
try:
|
|
582
626
|
generated = build_app_spec_from_requirements(
|
|
583
627
|
title=title,
|
|
584
628
|
requirement_text=requirement_text,
|
|
585
|
-
package_name=
|
|
629
|
+
package_name=effective_package_resolution.get("tag_name"),
|
|
586
630
|
layout_style=layout_style,
|
|
587
631
|
)
|
|
588
632
|
except RequirementsBuildError as exc:
|
|
@@ -591,7 +635,9 @@ class SolutionTools(ToolBase):
|
|
|
591
635
|
build_id=build_id,
|
|
592
636
|
title=title,
|
|
593
637
|
requirement_text=requirement_text,
|
|
594
|
-
package_resolution=
|
|
638
|
+
package_resolution=effective_package_resolution,
|
|
639
|
+
app_key=app_key,
|
|
640
|
+
update_only=_target_is_update_only(resolved_target),
|
|
595
641
|
layout_style=layout_style,
|
|
596
642
|
publish=publish,
|
|
597
643
|
run_label=run_label,
|
|
@@ -608,7 +654,9 @@ class SolutionTools(ToolBase):
|
|
|
608
654
|
build_id=build_id,
|
|
609
655
|
title=title,
|
|
610
656
|
requirement_text=requirement_text,
|
|
611
|
-
package_resolution=
|
|
657
|
+
package_resolution=effective_package_resolution,
|
|
658
|
+
app_key=app_key,
|
|
659
|
+
update_only=_target_is_update_only(resolved_target),
|
|
612
660
|
layout_style=layout_style,
|
|
613
661
|
publish=publish,
|
|
614
662
|
run_label=run_label,
|
|
@@ -618,7 +666,7 @@ class SolutionTools(ToolBase):
|
|
|
618
666
|
self._persist_generated_app_spec(
|
|
619
667
|
build_id=build_id,
|
|
620
668
|
generated=generated,
|
|
621
|
-
package_resolution=
|
|
669
|
+
package_resolution=effective_package_resolution,
|
|
622
670
|
title=title,
|
|
623
671
|
requirement_text=requirement_text,
|
|
624
672
|
layout_style=layout_style,
|
|
@@ -628,16 +676,23 @@ class SolutionTools(ToolBase):
|
|
|
628
676
|
mode=mode,
|
|
629
677
|
build_id=build_id,
|
|
630
678
|
app_spec=generated.app_spec,
|
|
679
|
+
app_key=app_key,
|
|
680
|
+
package_tag_id=int(effective_package_resolution.get("tag_id") or 0),
|
|
681
|
+
update_only=_target_is_update_only(resolved_target),
|
|
631
682
|
publish=publish,
|
|
632
683
|
run_label=run_label,
|
|
633
|
-
target=_requirements_target(
|
|
684
|
+
target=_requirements_target(
|
|
685
|
+
effective_package_resolution,
|
|
686
|
+
app_key=app_key if isinstance(app_key, str) else "",
|
|
687
|
+
update_only=_target_is_update_only(resolved_target),
|
|
688
|
+
),
|
|
634
689
|
repair_patch={},
|
|
635
690
|
)
|
|
636
691
|
return self._requirements_success_response(
|
|
637
692
|
mode=mode,
|
|
638
693
|
title=title,
|
|
639
694
|
requirement_text=requirement_text,
|
|
640
|
-
package_resolution=
|
|
695
|
+
package_resolution=effective_package_resolution,
|
|
641
696
|
layout_style=layout_style,
|
|
642
697
|
publish=publish,
|
|
643
698
|
run_label=run_label,
|
|
@@ -1101,9 +1156,18 @@ class SolutionTools(ToolBase):
|
|
|
1101
1156
|
parsed_manifest = BuildManifest.model_validate(merged_manifest)
|
|
1102
1157
|
normalized_manifest = normalize_solution_spec(parsed_manifest)
|
|
1103
1158
|
compiled = compile_solution(normalized_manifest)
|
|
1159
|
+
existing_app_targets = _resolve_target_app_keys(
|
|
1160
|
+
target=target,
|
|
1161
|
+
entity_ids=[entity.entity_id for entity in compiled.entities],
|
|
1162
|
+
)
|
|
1163
|
+
update_only = _target_is_update_only(target) or bool(existing_app_targets)
|
|
1164
|
+
if update_only and not existing_app_targets:
|
|
1165
|
+
raise ValueError("update target requires app_key or target.app_keys")
|
|
1104
1166
|
package_tag_id = _resolve_target_package_tag_id(target)
|
|
1105
1167
|
if package_tag_id is not None:
|
|
1106
1168
|
compiled = _bind_existing_package_target(compiled, package_tag_id)
|
|
1169
|
+
elif update_only:
|
|
1170
|
+
compiled = _bind_existing_app_target(compiled)
|
|
1107
1171
|
filtered_compiled = _filter_compiled_solution(
|
|
1108
1172
|
compiled,
|
|
1109
1173
|
stage_name=stage_name,
|
|
@@ -1112,6 +1176,8 @@ class SolutionTools(ToolBase):
|
|
|
1112
1176
|
)
|
|
1113
1177
|
assembly.set_stage_spec(stage_name, resolved_stage_payload)
|
|
1114
1178
|
assembly.set_manifest(normalized_manifest.model_dump(mode="json"))
|
|
1179
|
+
if existing_app_targets:
|
|
1180
|
+
_seed_existing_app_artifacts(assembly=assembly, existing_app_targets=existing_app_targets)
|
|
1115
1181
|
|
|
1116
1182
|
if mode in {"preflight", "plan"}:
|
|
1117
1183
|
status = "preflighted" if mode == "preflight" else "planned"
|
|
@@ -1170,11 +1236,24 @@ class SolutionTools(ToolBase):
|
|
|
1170
1236
|
mode=mode,
|
|
1171
1237
|
)
|
|
1172
1238
|
assembly.set_artifacts(result["artifacts"])
|
|
1239
|
+
stage_verification = None
|
|
1240
|
+
effective_status = result["status"]
|
|
1241
|
+
if result["status"] == "success":
|
|
1242
|
+
stage_verification = self._verify_stage_write_result(
|
|
1243
|
+
profile=profile,
|
|
1244
|
+
build_id=build_id,
|
|
1245
|
+
public_stage_name=public_stage_name,
|
|
1246
|
+
artifacts=result["artifacts"],
|
|
1247
|
+
solution_spec=normalized_manifest.model_dump(mode="json"),
|
|
1248
|
+
target=target,
|
|
1249
|
+
)
|
|
1250
|
+
if isinstance(stage_verification, dict) and stage_verification.get("status") == "partial":
|
|
1251
|
+
effective_status = "partial_success"
|
|
1173
1252
|
failure_signature = None
|
|
1174
1253
|
stage_failure_count = 0
|
|
1175
1254
|
next_strategy = None
|
|
1176
1255
|
suggested_next_call_override = None
|
|
1177
|
-
if
|
|
1256
|
+
if effective_status == "failed":
|
|
1178
1257
|
failure_signature, stage_failure_count = self._record_failure_signature(
|
|
1179
1258
|
assembly=assembly,
|
|
1180
1259
|
stage_name=public_stage_name,
|
|
@@ -1191,12 +1270,12 @@ class SolutionTools(ToolBase):
|
|
|
1191
1270
|
{
|
|
1192
1271
|
"stage": stage_name,
|
|
1193
1272
|
"mode": mode,
|
|
1194
|
-
"status":
|
|
1273
|
+
"status": effective_status,
|
|
1195
1274
|
"run_path": result["run_path"],
|
|
1196
1275
|
"execution_step_count": len(filtered_compiled.execution_plan.steps),
|
|
1197
1276
|
}
|
|
1198
1277
|
)
|
|
1199
|
-
assembly.mark_status(_build_status_for_stage(stage_name,
|
|
1278
|
+
assembly.mark_status(_build_status_for_stage(stage_name, effective_status))
|
|
1200
1279
|
response = self._stage_success_response(
|
|
1201
1280
|
build_id=build_id,
|
|
1202
1281
|
stage_name=public_stage_name,
|
|
@@ -1208,11 +1287,12 @@ class SolutionTools(ToolBase):
|
|
|
1208
1287
|
errors=result["errors"],
|
|
1209
1288
|
run_path=result["run_path"],
|
|
1210
1289
|
build_path=str(assembly.path),
|
|
1211
|
-
status=
|
|
1290
|
+
status=effective_status,
|
|
1212
1291
|
build_summary=_build_summary(assembly),
|
|
1213
1292
|
tool_name=tool_name,
|
|
1293
|
+
verification=stage_verification,
|
|
1214
1294
|
)
|
|
1215
|
-
if
|
|
1295
|
+
if effective_status == "failed":
|
|
1216
1296
|
response["failure_signature"] = failure_signature
|
|
1217
1297
|
response["stage_failure_count"] = stage_failure_count or 1
|
|
1218
1298
|
if next_strategy:
|
|
@@ -1415,6 +1495,100 @@ class SolutionTools(ToolBase):
|
|
|
1415
1495
|
verification["status"] = "partial"
|
|
1416
1496
|
return verification
|
|
1417
1497
|
|
|
1498
|
+
def _verify_stage_write_result(
|
|
1499
|
+
self,
|
|
1500
|
+
*,
|
|
1501
|
+
profile: str,
|
|
1502
|
+
build_id: str,
|
|
1503
|
+
public_stage_name: str,
|
|
1504
|
+
artifacts: dict[str, Any],
|
|
1505
|
+
solution_spec: dict[str, Any],
|
|
1506
|
+
target: dict[str, Any],
|
|
1507
|
+
) -> dict[str, Any] | None:
|
|
1508
|
+
if public_stage_name != "app":
|
|
1509
|
+
return None
|
|
1510
|
+
app_tools = AppTools(self.sessions, self.backend)
|
|
1511
|
+
expected_package_tag_id = _resolve_target_package_tag_id(target)
|
|
1512
|
+
verification: dict[str, Any] = {
|
|
1513
|
+
"stage": public_stage_name,
|
|
1514
|
+
"status": "success",
|
|
1515
|
+
"expected_package_tag_id": expected_package_tag_id,
|
|
1516
|
+
"package_attached": None if expected_package_tag_id is None else True,
|
|
1517
|
+
"apps": [],
|
|
1518
|
+
"views_created": [],
|
|
1519
|
+
"views_strategy": "not_included",
|
|
1520
|
+
"errors": [],
|
|
1521
|
+
}
|
|
1522
|
+
apps_artifact = artifacts.get("apps", {}) if isinstance(artifacts.get("apps"), dict) else {}
|
|
1523
|
+
views_artifact = artifacts.get("views", {}) if isinstance(artifacts.get("views"), dict) else {}
|
|
1524
|
+
entity_specs = {
|
|
1525
|
+
entity.get("entity_id"): entity
|
|
1526
|
+
for entity in solution_spec.get("entities", [])
|
|
1527
|
+
if isinstance(entity, dict) and isinstance(entity.get("entity_id"), str)
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
for entity_id, app_info in apps_artifact.items():
|
|
1531
|
+
if not isinstance(app_info, dict):
|
|
1532
|
+
continue
|
|
1533
|
+
app_key = app_info.get("app_key")
|
|
1534
|
+
if not isinstance(app_key, str) or not app_key:
|
|
1535
|
+
continue
|
|
1536
|
+
try:
|
|
1537
|
+
base_info = app_tools.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1538
|
+
raw_tag_ids = base_info.get("tagIds") if isinstance(base_info.get("tagIds"), list) else []
|
|
1539
|
+
tag_ids_after = [tag_id for tag_id in (_coerce_count(value) for value in raw_tag_ids) if tag_id is not None]
|
|
1540
|
+
package_attached = None if expected_package_tag_id is None else expected_package_tag_id in tag_ids_after
|
|
1541
|
+
if package_attached is False:
|
|
1542
|
+
verification["status"] = "partial"
|
|
1543
|
+
verification["package_attached"] = False
|
|
1544
|
+
verification["errors"].append(
|
|
1545
|
+
{
|
|
1546
|
+
"category": "verification",
|
|
1547
|
+
"detail": f"app '{app_key}' is not attached to expected package '{expected_package_tag_id}'",
|
|
1548
|
+
"entity_id": entity_id,
|
|
1549
|
+
"app_key": app_key,
|
|
1550
|
+
"tag_ids_after": tag_ids_after,
|
|
1551
|
+
}
|
|
1552
|
+
)
|
|
1553
|
+
entity_views = views_artifact.get(entity_id) if isinstance(views_artifact.get(entity_id), dict) else {}
|
|
1554
|
+
created_view_keys = sorted(
|
|
1555
|
+
{
|
|
1556
|
+
str(view_result.get("view_key"))
|
|
1557
|
+
for view_result in entity_views.values()
|
|
1558
|
+
if isinstance(view_result, dict) and isinstance(view_result.get("view_key"), str) and view_result.get("view_key")
|
|
1559
|
+
}
|
|
1560
|
+
)
|
|
1561
|
+
verification["views_created"].extend(created_view_keys)
|
|
1562
|
+
verification["apps"].append(
|
|
1563
|
+
{
|
|
1564
|
+
"entity_id": entity_id,
|
|
1565
|
+
"display_name": entity_specs.get(entity_id, {}).get("display_name"),
|
|
1566
|
+
"app_key": app_key,
|
|
1567
|
+
"tag_ids_after": tag_ids_after,
|
|
1568
|
+
"package_attached": package_attached,
|
|
1569
|
+
"views_created": created_view_keys,
|
|
1570
|
+
"publish_status": base_info.get("appPublishStatus"),
|
|
1571
|
+
}
|
|
1572
|
+
)
|
|
1573
|
+
except Exception as exc: # noqa: BLE001
|
|
1574
|
+
verification["status"] = "partial"
|
|
1575
|
+
verification["errors"].append(
|
|
1576
|
+
{
|
|
1577
|
+
"category": "verification",
|
|
1578
|
+
"detail": str(exc),
|
|
1579
|
+
"entity_id": entity_id,
|
|
1580
|
+
"app_key": app_key,
|
|
1581
|
+
}
|
|
1582
|
+
)
|
|
1583
|
+
if verification["views_created"]:
|
|
1584
|
+
verification["views_strategy"] = "created"
|
|
1585
|
+
else:
|
|
1586
|
+
verification["suggested_next_call"] = {
|
|
1587
|
+
"tool_name": "solution_build_views",
|
|
1588
|
+
"arguments": {"mode": "plan", "build_id": build_id},
|
|
1589
|
+
}
|
|
1590
|
+
return verification
|
|
1591
|
+
|
|
1418
1592
|
def _failure_response(self, mode: str, idempotency_key: str, category: str, detail: Any) -> dict[str, Any]:
|
|
1419
1593
|
error_fields = _solution_error_fields(
|
|
1420
1594
|
category=category,
|
|
@@ -1458,6 +1632,8 @@ class SolutionTools(ToolBase):
|
|
|
1458
1632
|
),
|
|
1459
1633
|
stage=stage_name,
|
|
1460
1634
|
)
|
|
1635
|
+
if build_id and stage_name == "app":
|
|
1636
|
+
error_fields["suggested_schema_call"] = None
|
|
1461
1637
|
return {
|
|
1462
1638
|
"build_id": build_id,
|
|
1463
1639
|
"stage": stage_name,
|
|
@@ -1521,8 +1697,9 @@ class SolutionTools(ToolBase):
|
|
|
1521
1697
|
status: str,
|
|
1522
1698
|
build_summary: dict[str, Any],
|
|
1523
1699
|
tool_name: str,
|
|
1700
|
+
verification: dict[str, Any] | None = None,
|
|
1524
1701
|
) -> dict[str, Any]:
|
|
1525
|
-
|
|
1702
|
+
response = {
|
|
1526
1703
|
"build_id": build_id,
|
|
1527
1704
|
"stage": stage_name,
|
|
1528
1705
|
"mode": mode,
|
|
@@ -1535,6 +1712,7 @@ class SolutionTools(ToolBase):
|
|
|
1535
1712
|
"errors": _sanitize_errors(errors),
|
|
1536
1713
|
"build_path": build_path,
|
|
1537
1714
|
"run_path": run_path,
|
|
1715
|
+
"verification": verification,
|
|
1538
1716
|
"build_summary": build_summary,
|
|
1539
1717
|
"suggested_next_call": _solution_build_next_call(
|
|
1540
1718
|
tool_name=tool_name,
|
|
@@ -1542,6 +1720,11 @@ class SolutionTools(ToolBase):
|
|
|
1542
1720
|
build_id=build_id,
|
|
1543
1721
|
),
|
|
1544
1722
|
}
|
|
1723
|
+
if isinstance(verification, dict):
|
|
1724
|
+
for key in ("package_attached", "views_created", "views_strategy"):
|
|
1725
|
+
if key in verification:
|
|
1726
|
+
response[key] = verification.get(key)
|
|
1727
|
+
return response
|
|
1545
1728
|
|
|
1546
1729
|
def _design_failure_response(self, session_id: str, action: str, category: str, detail: Any) -> dict[str, Any]:
|
|
1547
1730
|
error_fields = _solution_error_fields(
|
|
@@ -1761,6 +1944,116 @@ class SolutionTools(ToolBase):
|
|
|
1761
1944
|
},
|
|
1762
1945
|
}
|
|
1763
1946
|
|
|
1947
|
+
def _resolve_builder_existing_app_reference(
|
|
1948
|
+
self,
|
|
1949
|
+
*,
|
|
1950
|
+
profile: str,
|
|
1951
|
+
app_key: str,
|
|
1952
|
+
package_resolution: dict[str, Any],
|
|
1953
|
+
update_only: bool,
|
|
1954
|
+
) -> dict[str, Any]:
|
|
1955
|
+
normalized_app_key = str(app_key or "").strip()
|
|
1956
|
+
if not normalized_app_key:
|
|
1957
|
+
if update_only:
|
|
1958
|
+
detail = "update target requires app_key"
|
|
1959
|
+
error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
|
|
1960
|
+
error_fields["missing_required_fields"] = sorted(
|
|
1961
|
+
dict.fromkeys(list(error_fields.get("missing_required_fields", [])) + ["app_key"])
|
|
1962
|
+
)
|
|
1963
|
+
error_fields["suggested_schema_call"] = None
|
|
1964
|
+
return {
|
|
1965
|
+
"status": "failed",
|
|
1966
|
+
"response": {
|
|
1967
|
+
"status": "failed",
|
|
1968
|
+
"stage": "app",
|
|
1969
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
1970
|
+
**error_fields,
|
|
1971
|
+
},
|
|
1972
|
+
}
|
|
1973
|
+
return {"status": "none"}
|
|
1974
|
+
|
|
1975
|
+
app_tools = AppTools(self.sessions, self.backend)
|
|
1976
|
+
try:
|
|
1977
|
+
base_info = app_tools.app_get_base(profile=profile, app_key=normalized_app_key, include_raw=True).get("result") or {}
|
|
1978
|
+
except Exception as exc: # noqa: BLE001
|
|
1979
|
+
detail = f"failed to resolve existing app '{normalized_app_key}': {exc}"
|
|
1980
|
+
error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
|
|
1981
|
+
error_fields["suggested_schema_call"] = None
|
|
1982
|
+
return {
|
|
1983
|
+
"status": "failed",
|
|
1984
|
+
"response": {
|
|
1985
|
+
"status": "failed",
|
|
1986
|
+
"stage": "app",
|
|
1987
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
1988
|
+
**error_fields,
|
|
1989
|
+
},
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
raw_tag_ids = base_info.get("tagIds") if isinstance(base_info.get("tagIds"), list) else []
|
|
1993
|
+
tag_ids = [tag_id for tag_id in (_coerce_count(item) for item in raw_tag_ids) if tag_id is not None]
|
|
1994
|
+
effective_package_resolution = deepcopy(package_resolution)
|
|
1995
|
+
if package_resolution.get("status") == "resolved":
|
|
1996
|
+
resolved_tag_id = _coerce_count(package_resolution.get("tag_id"))
|
|
1997
|
+
if resolved_tag_id is not None and tag_ids and resolved_tag_id not in tag_ids:
|
|
1998
|
+
detail = (
|
|
1999
|
+
f"app_key '{normalized_app_key}' does not belong to requested package "
|
|
2000
|
+
f"'{package_resolution.get('tag_name') or resolved_tag_id}'"
|
|
2001
|
+
)
|
|
2002
|
+
error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
|
|
2003
|
+
error_fields["error_code"] = "APP_PACKAGE_MISMATCH"
|
|
2004
|
+
error_fields["suggested_schema_call"] = None
|
|
2005
|
+
return {
|
|
2006
|
+
"status": "failed",
|
|
2007
|
+
"response": {
|
|
2008
|
+
"status": "failed",
|
|
2009
|
+
"stage": "app",
|
|
2010
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
2011
|
+
"existing_app": {
|
|
2012
|
+
"app_key": normalized_app_key,
|
|
2013
|
+
"app_name": base_info.get("formTitle"),
|
|
2014
|
+
"tag_ids": tag_ids,
|
|
2015
|
+
},
|
|
2016
|
+
**error_fields,
|
|
2017
|
+
},
|
|
2018
|
+
}
|
|
2019
|
+
elif package_resolution.get("status") == "new_package":
|
|
2020
|
+
if len(tag_ids) == 1:
|
|
2021
|
+
package_tools = PackageTools(self.sessions, self.backend)
|
|
2022
|
+
package_detail = package_tools.package_get(profile=profile, tag_id=tag_ids[0], include_raw=False).get("result") or {}
|
|
2023
|
+
effective_package_resolution = {
|
|
2024
|
+
"status": "resolved",
|
|
2025
|
+
"matched_via": "existing_app",
|
|
2026
|
+
"tag_id": tag_ids[0],
|
|
2027
|
+
"tag_name": package_detail.get("tagName"),
|
|
2028
|
+
"candidates": [],
|
|
2029
|
+
}
|
|
2030
|
+
elif len(tag_ids) > 1:
|
|
2031
|
+
detail = f"app_key '{normalized_app_key}' belongs to multiple packages; provide package_tag_id or package_name explicitly"
|
|
2032
|
+
error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
|
|
2033
|
+
error_fields["error_code"] = "AMBIGUOUS_PACKAGE"
|
|
2034
|
+
error_fields["suggested_schema_call"] = None
|
|
2035
|
+
return {
|
|
2036
|
+
"status": "failed",
|
|
2037
|
+
"response": {
|
|
2038
|
+
"status": "failed",
|
|
2039
|
+
"stage": "app",
|
|
2040
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
2041
|
+
"existing_app": {
|
|
2042
|
+
"app_key": normalized_app_key,
|
|
2043
|
+
"app_name": base_info.get("formTitle"),
|
|
2044
|
+
"tag_ids": tag_ids,
|
|
2045
|
+
},
|
|
2046
|
+
**error_fields,
|
|
2047
|
+
},
|
|
2048
|
+
}
|
|
2049
|
+
return {
|
|
2050
|
+
"status": "resolved",
|
|
2051
|
+
"app_key": normalized_app_key,
|
|
2052
|
+
"app_name": base_info.get("formTitle"),
|
|
2053
|
+
"package_resolution": effective_package_resolution,
|
|
2054
|
+
"tag_ids": tag_ids,
|
|
2055
|
+
}
|
|
2056
|
+
|
|
1764
2057
|
def _requirements_failure_response(
|
|
1765
2058
|
self,
|
|
1766
2059
|
*,
|
|
@@ -1769,6 +2062,8 @@ class SolutionTools(ToolBase):
|
|
|
1769
2062
|
title: str,
|
|
1770
2063
|
requirement_text: str,
|
|
1771
2064
|
package_resolution: dict[str, Any],
|
|
2065
|
+
app_key: str,
|
|
2066
|
+
update_only: bool,
|
|
1772
2067
|
layout_style: str,
|
|
1773
2068
|
publish: bool,
|
|
1774
2069
|
run_label: str | None,
|
|
@@ -1788,6 +2083,8 @@ class SolutionTools(ToolBase):
|
|
|
1788
2083
|
title=title,
|
|
1789
2084
|
requirement_text=requirement_text,
|
|
1790
2085
|
package_resolution=package_resolution,
|
|
2086
|
+
app_key=app_key,
|
|
2087
|
+
update_only=update_only,
|
|
1791
2088
|
layout_style=layout_style,
|
|
1792
2089
|
publish=publish,
|
|
1793
2090
|
run_label=run_label,
|
|
@@ -1823,6 +2120,8 @@ class SolutionTools(ToolBase):
|
|
|
1823
2120
|
"failure_signature": None,
|
|
1824
2121
|
"stage_failure_count": 0,
|
|
1825
2122
|
"details": deepcopy(error_details or {}),
|
|
2123
|
+
"suggested_requirement_hint": (error_details or {}).get("suggested_requirement_hint") if isinstance(error_details, dict) else None,
|
|
2124
|
+
"suggested_patch": deepcopy((error_details or {}).get("suggested_patch")) if isinstance(error_details, dict) else None,
|
|
1826
2125
|
**error_fields,
|
|
1827
2126
|
}
|
|
1828
2127
|
|
|
@@ -1857,7 +2156,21 @@ class SolutionTools(ToolBase):
|
|
|
1857
2156
|
"excluded_advanced_fields": generated.summary.get("excluded_advanced_fields"),
|
|
1858
2157
|
"execution_step_count": len((stage_result.get("execution_plan") or {}).get("steps", [])),
|
|
1859
2158
|
"suggested_next_call": next_call,
|
|
2159
|
+
"views_strategy": _resolve_app_stage_views_strategy(stage_result),
|
|
1860
2160
|
}
|
|
2161
|
+
if isinstance(stage_result.get("verification"), dict):
|
|
2162
|
+
verification = stage_result["verification"]
|
|
2163
|
+
response["verification"] = verification
|
|
2164
|
+
if "package_attached" in verification:
|
|
2165
|
+
response["package_attached"] = verification.get("package_attached")
|
|
2166
|
+
if isinstance(verification.get("apps"), list):
|
|
2167
|
+
response["tag_ids_after"] = {
|
|
2168
|
+
str(item.get("entity_id")): item.get("tag_ids_after")
|
|
2169
|
+
for item in verification["apps"]
|
|
2170
|
+
if isinstance(item, dict) and item.get("entity_id") is not None
|
|
2171
|
+
}
|
|
2172
|
+
if isinstance(verification.get("views_created"), list):
|
|
2173
|
+
response["views_created"] = verification.get("views_created")
|
|
1861
2174
|
for key in (
|
|
1862
2175
|
"error_code",
|
|
1863
2176
|
"recoverable",
|
|
@@ -1967,6 +2280,8 @@ def _requirements_next_call(
|
|
|
1967
2280
|
title: str,
|
|
1968
2281
|
requirement_text: str,
|
|
1969
2282
|
package_resolution: dict[str, Any],
|
|
2283
|
+
app_key: str,
|
|
2284
|
+
update_only: bool,
|
|
1970
2285
|
layout_style: str,
|
|
1971
2286
|
publish: bool,
|
|
1972
2287
|
run_label: str | None,
|
|
@@ -1986,6 +2301,10 @@ def _requirements_next_call(
|
|
|
1986
2301
|
arguments["package_tag_id"] = package_resolution["tag_id"]
|
|
1987
2302
|
elif isinstance(package_resolution.get("tag_name"), str) and package_resolution["tag_name"]:
|
|
1988
2303
|
arguments["package_name"] = package_resolution["tag_name"]
|
|
2304
|
+
if isinstance(app_key, str) and app_key:
|
|
2305
|
+
arguments["app_key"] = app_key
|
|
2306
|
+
if update_only or arguments.get("app_key"):
|
|
2307
|
+
arguments["update_only"] = True
|
|
1989
2308
|
return {"tool_name": "solution_build_app_from_requirements", "arguments": arguments}
|
|
1990
2309
|
|
|
1991
2310
|
|
|
@@ -1995,6 +2314,8 @@ def _rewrite_requirements_next_call(
|
|
|
1995
2314
|
title: str,
|
|
1996
2315
|
requirement_text: str,
|
|
1997
2316
|
package_resolution: dict[str, Any],
|
|
2317
|
+
app_key: str,
|
|
2318
|
+
update_only: bool,
|
|
1998
2319
|
layout_style: str,
|
|
1999
2320
|
publish: bool,
|
|
2000
2321
|
run_label: str | None,
|
|
@@ -2014,17 +2335,93 @@ def _rewrite_requirements_next_call(
|
|
|
2014
2335
|
title=title,
|
|
2015
2336
|
requirement_text=requirement_text,
|
|
2016
2337
|
package_resolution=package_resolution,
|
|
2338
|
+
app_key=app_key,
|
|
2339
|
+
update_only=update_only,
|
|
2017
2340
|
layout_style=layout_style,
|
|
2018
2341
|
publish=publish,
|
|
2019
2342
|
run_label=run_label,
|
|
2020
2343
|
)
|
|
2021
2344
|
|
|
2022
2345
|
|
|
2023
|
-
def _requirements_target(
|
|
2346
|
+
def _requirements_target(
|
|
2347
|
+
package_resolution: dict[str, Any],
|
|
2348
|
+
*,
|
|
2349
|
+
app_key: str = "",
|
|
2350
|
+
update_only: bool = False,
|
|
2351
|
+
) -> dict[str, Any]:
|
|
2024
2352
|
tag_id = package_resolution.get("tag_id")
|
|
2353
|
+
target: dict[str, Any] = {}
|
|
2025
2354
|
if isinstance(tag_id, int) and tag_id > 0:
|
|
2026
|
-
|
|
2027
|
-
|
|
2355
|
+
target["package_tag_id"] = tag_id
|
|
2356
|
+
if isinstance(app_key, str) and app_key:
|
|
2357
|
+
target["app_key"] = app_key
|
|
2358
|
+
if update_only or target.get("app_key"):
|
|
2359
|
+
target["mode"] = "update"
|
|
2360
|
+
target["update_only"] = True
|
|
2361
|
+
return target
|
|
2362
|
+
|
|
2363
|
+
|
|
2364
|
+
def _merge_app_target(
|
|
2365
|
+
target: dict[str, Any] | None,
|
|
2366
|
+
*,
|
|
2367
|
+
app_key: str,
|
|
2368
|
+
package_tag_id: int,
|
|
2369
|
+
update_only: bool,
|
|
2370
|
+
) -> dict[str, Any]:
|
|
2371
|
+
merged = deepcopy(target) if isinstance(target, dict) else {}
|
|
2372
|
+
if isinstance(app_key, str) and app_key.strip():
|
|
2373
|
+
merged["app_key"] = app_key.strip()
|
|
2374
|
+
if isinstance(package_tag_id, int) and package_tag_id > 0:
|
|
2375
|
+
merged["package_tag_id"] = package_tag_id
|
|
2376
|
+
if update_only or merged.get("app_key"):
|
|
2377
|
+
merged["mode"] = "update"
|
|
2378
|
+
merged["update_only"] = True
|
|
2379
|
+
return merged
|
|
2380
|
+
|
|
2381
|
+
|
|
2382
|
+
def _target_is_update_only(target: dict[str, Any]) -> bool:
|
|
2383
|
+
if not target:
|
|
2384
|
+
return False
|
|
2385
|
+
if target.get("update_only") is True:
|
|
2386
|
+
return True
|
|
2387
|
+
mode = target.get("mode")
|
|
2388
|
+
return isinstance(mode, str) and mode.strip().lower() == "update"
|
|
2389
|
+
|
|
2390
|
+
|
|
2391
|
+
def _resolve_target_app_keys(*, target: dict[str, Any], entity_ids: list[str]) -> dict[str, str]:
|
|
2392
|
+
if not target:
|
|
2393
|
+
return {}
|
|
2394
|
+
resolved: dict[str, str] = {}
|
|
2395
|
+
direct_app_key = target.get("app_key")
|
|
2396
|
+
if isinstance(direct_app_key, str) and direct_app_key.strip():
|
|
2397
|
+
if len(entity_ids) != 1:
|
|
2398
|
+
raise ValueError("target.app_key requires a single-entity app stage; use target.app_keys for multiple entities")
|
|
2399
|
+
resolved[entity_ids[0]] = direct_app_key.strip()
|
|
2400
|
+
raw_mapping = target.get("app_keys")
|
|
2401
|
+
if isinstance(raw_mapping, dict):
|
|
2402
|
+
for entity_id, app_key in raw_mapping.items():
|
|
2403
|
+
if not isinstance(entity_id, str) or entity_id not in entity_ids:
|
|
2404
|
+
raise ValueError(f"target.app_keys contains unknown entity '{entity_id}'")
|
|
2405
|
+
if not isinstance(app_key, str) or not app_key.strip():
|
|
2406
|
+
raise ValueError(f"target.app_keys['{entity_id}'] must be a non-empty string")
|
|
2407
|
+
resolved[entity_id] = app_key.strip()
|
|
2408
|
+
return resolved
|
|
2409
|
+
|
|
2410
|
+
|
|
2411
|
+
def _seed_existing_app_artifacts(*, assembly: BuildAssemblyStore, existing_app_targets: dict[str, str]) -> None:
|
|
2412
|
+
if not existing_app_targets:
|
|
2413
|
+
return
|
|
2414
|
+
artifacts = assembly.get_artifacts()
|
|
2415
|
+
apps_artifact = artifacts.get("apps", {}) if isinstance(artifacts.get("apps"), dict) else {}
|
|
2416
|
+
for entity_id, app_key in existing_app_targets.items():
|
|
2417
|
+
existing = apps_artifact.get(entity_id, {}) if isinstance(apps_artifact.get(entity_id), dict) else {}
|
|
2418
|
+
next_artifact = deepcopy(existing)
|
|
2419
|
+
next_artifact["app_key"] = app_key
|
|
2420
|
+
next_artifact["reused"] = True
|
|
2421
|
+
next_artifact["target_mode"] = "update"
|
|
2422
|
+
apps_artifact[entity_id] = next_artifact
|
|
2423
|
+
artifacts["apps"] = apps_artifact
|
|
2424
|
+
assembly.set_artifacts(artifacts)
|
|
2028
2425
|
|
|
2029
2426
|
|
|
2030
2427
|
def _design_session_next_call(*, action: str, session_id: str | None) -> dict[str, Any] | None:
|
|
@@ -2076,6 +2473,7 @@ def _solution_schema_example_payload(
|
|
|
2076
2473
|
tool_name, payload_key = _solution_schema_tool_metadata(stage)
|
|
2077
2474
|
examples = _solution_schema_examples(stage)
|
|
2078
2475
|
selected = deepcopy(examples.get(intent) or examples["minimal"])
|
|
2476
|
+
call_example = _solution_schema_call_example(stage)
|
|
2079
2477
|
response = {
|
|
2080
2478
|
"status": "ok",
|
|
2081
2479
|
"stage": stage,
|
|
@@ -2087,10 +2485,8 @@ def _solution_schema_example_payload(
|
|
|
2087
2485
|
"optional_fields": _solution_schema_optional_fields(stage),
|
|
2088
2486
|
"common_errors": _solution_schema_common_errors(stage),
|
|
2089
2487
|
"notes": _solution_schema_notes(stage),
|
|
2090
|
-
"
|
|
2091
|
-
|
|
2092
|
-
"arguments": {"mode": "preflight"},
|
|
2093
|
-
},
|
|
2488
|
+
"call_example": call_example,
|
|
2489
|
+
"suggested_next_call": deepcopy(call_example),
|
|
2094
2490
|
}
|
|
2095
2491
|
if include_selected_example:
|
|
2096
2492
|
response["selected_example"] = selected
|
|
@@ -2102,6 +2498,7 @@ def _solution_schema_example_payload(
|
|
|
2102
2498
|
def _solution_schema_tool_metadata(stage: str) -> tuple[str, str]:
|
|
2103
2499
|
mapping = {
|
|
2104
2500
|
"app": ("solution_build_app", "app_spec"),
|
|
2501
|
+
"app_update": ("solution_build_app", "app_spec"),
|
|
2105
2502
|
"flow": ("solution_build_flow", "flow_spec"),
|
|
2106
2503
|
"views": ("solution_build_views", "views_spec"),
|
|
2107
2504
|
"analytics_portal": ("solution_build_analytics_portal", "analytics_portal_spec"),
|
|
@@ -2112,9 +2509,18 @@ def _solution_schema_tool_metadata(stage: str) -> tuple[str, str]:
|
|
|
2112
2509
|
return mapping[stage]
|
|
2113
2510
|
|
|
2114
2511
|
|
|
2512
|
+
def _solution_schema_call_example(stage: str) -> dict[str, Any]:
|
|
2513
|
+
tool_name, _payload_key = _solution_schema_tool_metadata(stage)
|
|
2514
|
+
arguments: dict[str, Any] = {"mode": "preflight"}
|
|
2515
|
+
if stage == "app_update":
|
|
2516
|
+
arguments.update({"app_key": "APP_123456", "update_only": True})
|
|
2517
|
+
return {"tool_name": tool_name, "arguments": arguments}
|
|
2518
|
+
|
|
2519
|
+
|
|
2115
2520
|
def _solution_schema_required_fields(stage: str) -> list[str]:
|
|
2116
2521
|
fields = {
|
|
2117
2522
|
"app": ["solution_name", "entities[].entity_id", "entities[].display_name", "entities[].kind", "entities[].fields[]"],
|
|
2523
|
+
"app_update": ["solution_name", "entities[].entity_id", "entities[].display_name", "entities[].kind", "entities[].fields[]", "app_key"],
|
|
2118
2524
|
"flow": ["entities[].entity_id", "entities[].workflow or entities[].lifecycle_stages"],
|
|
2119
2525
|
"views": ["entities[].entity_id", "entities[].views[]"],
|
|
2120
2526
|
"analytics_portal": ["entities[].entity_id or portal.sections[]"],
|
|
@@ -2128,6 +2534,7 @@ def _solution_schema_required_fields(stage: str) -> list[str]:
|
|
|
2128
2534
|
def _solution_schema_optional_fields(stage: str) -> list[str]:
|
|
2129
2535
|
fields = {
|
|
2130
2536
|
"app": ["summary", "business_context", "package", "roles", "publish_policy", "preferences", "entities[].form_layout", "entities[].sample_records"],
|
|
2537
|
+
"app_update": ["package_tag_id", "publish_policy", "preferences", "entities[].form_layout", "entities[].sample_records", "update_only"],
|
|
2131
2538
|
"flow": ["solution_name", "roles", "publish_policy", "entities[].status_field_id", "entities[].owner_field_id", "entities[].start_field_id", "entities[].end_field_id"],
|
|
2132
2539
|
"views": ["solution_name", "metadata"],
|
|
2133
2540
|
"analytics_portal": ["solution_name", "publish_policy", "portal.name", "portal.sections[]"],
|
|
@@ -2145,6 +2552,11 @@ def _solution_schema_common_errors(stage: str) -> list[str]:
|
|
|
2145
2552
|
"Each field_id must be unique within an entity.",
|
|
2146
2553
|
"Do not include workflow/views/charts in app_spec. Use solution_build_flow or later stage tools.",
|
|
2147
2554
|
],
|
|
2555
|
+
"app_update": [
|
|
2556
|
+
"app_key is required for update mode and must point to an existing app.",
|
|
2557
|
+
"Updating an app uses the same app_spec structure, but the tool target must stay in update mode.",
|
|
2558
|
+
"Do not omit entity_id; updates still need a deterministic entity identity.",
|
|
2559
|
+
],
|
|
2148
2560
|
"flow": [
|
|
2149
2561
|
"The target entity must already exist in the build manifest or from a prior solution_build_app run.",
|
|
2150
2562
|
"status_field_id must point to an existing field, usually 'status'.",
|
|
@@ -2177,6 +2589,10 @@ def _solution_schema_notes(stage: str) -> list[str]:
|
|
|
2177
2589
|
"Use solution_build_app for package, entity, field, layout, and sample record design.",
|
|
2178
2590
|
"If you are unsure, call solution_schema_example(stage='app', intent='minimal') before generating payloads.",
|
|
2179
2591
|
],
|
|
2592
|
+
"app_update": [
|
|
2593
|
+
"Use solution_build_app with app_key and update_only=true when modifying an existing app.",
|
|
2594
|
+
"Update mode is not allowed to fall back to new package or new app creation.",
|
|
2595
|
+
],
|
|
2180
2596
|
"flow": [
|
|
2181
2597
|
"Use solution_build_flow only after app/schema exists.",
|
|
2182
2598
|
"workflow.nodes uses node_type enum values like start, audit, fill, copy, branch, condition.",
|
|
@@ -2268,6 +2684,62 @@ def _solution_schema_examples(stage: str) -> dict[str, dict[str, Any]]:
|
|
|
2268
2684
|
],
|
|
2269
2685
|
},
|
|
2270
2686
|
},
|
|
2687
|
+
"app_update": {
|
|
2688
|
+
"minimal": {
|
|
2689
|
+
"solution_name": "Demo Workspace",
|
|
2690
|
+
"entities": [
|
|
2691
|
+
{
|
|
2692
|
+
"entity_id": "demo_request",
|
|
2693
|
+
"display_name": "测试申请",
|
|
2694
|
+
"kind": "transaction",
|
|
2695
|
+
"title_field_id": "title",
|
|
2696
|
+
"fields": [
|
|
2697
|
+
{"field_id": "title", "label": "工单标题", "type": "text", "required": True},
|
|
2698
|
+
],
|
|
2699
|
+
"form_layout": {"rows": [{"field_ids": ["title"]}]},
|
|
2700
|
+
}
|
|
2701
|
+
],
|
|
2702
|
+
},
|
|
2703
|
+
"full": {
|
|
2704
|
+
"solution_name": "Demo Workspace",
|
|
2705
|
+
"entities": [
|
|
2706
|
+
{
|
|
2707
|
+
"entity_id": "demo_request",
|
|
2708
|
+
"display_name": "测试申请",
|
|
2709
|
+
"kind": "transaction",
|
|
2710
|
+
"title_field_id": "title",
|
|
2711
|
+
"status_field_id": "status",
|
|
2712
|
+
"fields": [
|
|
2713
|
+
{"field_id": "title", "label": "工单标题", "type": "text", "required": True},
|
|
2714
|
+
{"field_id": "status", "label": "状态", "type": "single_select", "options": ["待处理", "处理中", "已完成"]},
|
|
2715
|
+
{"field_id": "priority", "label": "优先级", "type": "single_select", "options": ["P0", "P1", "P2"]},
|
|
2716
|
+
],
|
|
2717
|
+
"form_layout": {
|
|
2718
|
+
"sections": [
|
|
2719
|
+
{"section_id": "basic", "title": "基础信息", "rows": [{"field_ids": ["title", "status"]}, {"field_ids": ["priority"]}]}
|
|
2720
|
+
]
|
|
2721
|
+
},
|
|
2722
|
+
}
|
|
2723
|
+
],
|
|
2724
|
+
},
|
|
2725
|
+
"demo": {
|
|
2726
|
+
"solution_name": "Demo Workspace",
|
|
2727
|
+
"entities": [
|
|
2728
|
+
{
|
|
2729
|
+
"entity_id": "customer_order",
|
|
2730
|
+
"display_name": "客户订单",
|
|
2731
|
+
"kind": "transaction",
|
|
2732
|
+
"title_field_id": "title",
|
|
2733
|
+
"fields": [
|
|
2734
|
+
{"field_id": "title", "label": "订单标题", "type": "text", "required": True},
|
|
2735
|
+
{"field_id": "customer_name", "label": "客户名称", "type": "text"},
|
|
2736
|
+
{"field_id": "amount", "label": "订单金额", "type": "amount"},
|
|
2737
|
+
],
|
|
2738
|
+
"form_layout": {"rows": [{"field_ids": ["title", "customer_name"]}, {"field_ids": ["amount"]}]},
|
|
2739
|
+
}
|
|
2740
|
+
],
|
|
2741
|
+
},
|
|
2742
|
+
},
|
|
2271
2743
|
"flow": {
|
|
2272
2744
|
"minimal": {
|
|
2273
2745
|
"solution_name": "Demo Workspace",
|
|
@@ -3054,6 +3526,15 @@ def _extract_navigation_items(items: list[Any]) -> list[dict[str, Any]]:
|
|
|
3054
3526
|
return extracted
|
|
3055
3527
|
|
|
3056
3528
|
|
|
3529
|
+
def _resolve_app_stage_views_strategy(stage_result: dict[str, Any]) -> str:
|
|
3530
|
+
verification = stage_result.get("verification")
|
|
3531
|
+
if isinstance(verification, dict):
|
|
3532
|
+
strategy = verification.get("views_strategy")
|
|
3533
|
+
if isinstance(strategy, str) and strategy:
|
|
3534
|
+
return strategy
|
|
3535
|
+
return "not_included"
|
|
3536
|
+
|
|
3537
|
+
|
|
3057
3538
|
def _resolve_target_package_tag_id(target: dict[str, Any]) -> int | None:
|
|
3058
3539
|
if not target:
|
|
3059
3540
|
return None
|
|
@@ -3069,12 +3550,18 @@ def _resolve_target_package_tag_id(target: dict[str, Any]) -> int | None:
|
|
|
3069
3550
|
|
|
3070
3551
|
def _bind_existing_package_target(compiled: CompiledSolution, package_tag_id: int) -> CompiledSolution:
|
|
3071
3552
|
compiled.package_payload = None
|
|
3072
|
-
compiled.execution_plan = build_execution_plan(compiled.normalized_spec, include_package=False)
|
|
3553
|
+
compiled.execution_plan = build_execution_plan(compiled.normalized_spec, include_package=False, attach_package=True)
|
|
3073
3554
|
for entity in compiled.entities:
|
|
3074
3555
|
entity.app_create_payload["tagIds"] = [package_tag_id]
|
|
3075
3556
|
return compiled
|
|
3076
3557
|
|
|
3077
3558
|
|
|
3559
|
+
def _bind_existing_app_target(compiled: CompiledSolution) -> CompiledSolution:
|
|
3560
|
+
compiled.package_payload = None
|
|
3561
|
+
compiled.execution_plan = build_execution_plan(compiled.normalized_spec, include_package=False, attach_package=False)
|
|
3562
|
+
return compiled
|
|
3563
|
+
|
|
3564
|
+
|
|
3078
3565
|
def _merge_stage_manifest(*, stage_name: str, manifest: dict[str, Any], stage_payload: dict[str, Any]) -> dict[str, Any]:
|
|
3079
3566
|
merged = deepcopy(manifest or default_manifest())
|
|
3080
3567
|
if stage_name == "app_flow":
|
|
@@ -3226,6 +3713,7 @@ def _stage_step_names(
|
|
|
3226
3713
|
step.step_name.startswith(prefix)
|
|
3227
3714
|
for prefix in (
|
|
3228
3715
|
"app.create.",
|
|
3716
|
+
"package.attach.",
|
|
3229
3717
|
"form.base.",
|
|
3230
3718
|
"form.relations.",
|
|
3231
3719
|
"publish.form.",
|
|
@@ -3257,6 +3745,7 @@ def _stage_step_names(
|
|
|
3257
3745
|
step.step_name.startswith(prefix)
|
|
3258
3746
|
for prefix in (
|
|
3259
3747
|
"app.create.",
|
|
3748
|
+
"package.attach.",
|
|
3260
3749
|
"form.base.",
|
|
3261
3750
|
"form.relations.",
|
|
3262
3751
|
"workflow.",
|