@qingflow-tech/qingflow-cli 1.1.49 → 1.1.50-beta.1

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.
Files changed (64) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +29 -13
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/skills/qingflow-cli/SKILL.md +92 -410
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  16. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +13 -5
  17. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +5 -3
  18. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +1 -1
  19. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +1 -1
  20. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1665 -1738
  21. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  22. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +76 -95
  23. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  26. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  27. package/skills/qingflow-cli/reference/data/README.md +32 -0
  28. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +50 -0
  29. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  30. package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
  31. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  32. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  33. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  34. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  35. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
  36. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  37. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +48 -46
  38. package/skills/qingflow-common/SKILL.md +10 -17
  39. package/src/qingflow_mcp/backend_client.py +142 -12
  40. package/src/qingflow_mcp/builder_facade/flow_validation.py +569 -0
  41. package/src/qingflow_mcp/builder_facade/service.py +749 -6
  42. package/src/qingflow_mcp/builder_facade/workflow_spec.py +154 -2
  43. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  44. package/src/qingflow_mcp/cli/commands/auth.py +1 -1
  45. package/src/qingflow_mcp/cli/commands/builder.py +57 -41
  46. package/src/qingflow_mcp/cli/commands/data.py +134 -0
  47. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  48. package/src/qingflow_mcp/cli/context.py +4 -1
  49. package/src/qingflow_mcp/cli/formatters.py +159 -4
  50. package/src/qingflow_mcp/cli/main.py +4 -1
  51. package/src/qingflow_mcp/config.py +60 -0
  52. package/src/qingflow_mcp/public_surface.py +9 -0
  53. package/src/qingflow_mcp/resource_uri.py +241 -0
  54. package/src/qingflow_mcp/response_trim.py +53 -5
  55. package/src/qingflow_mcp/server.py +2 -0
  56. package/src/qingflow_mcp/server_app_builder.py +57 -2
  57. package/src/qingflow_mcp/server_app_user.py +10 -3
  58. package/src/qingflow_mcp/tools/ai_builder_tools.py +374 -41
  59. package/src/qingflow_mcp/tools/app_tools.py +52 -13
  60. package/src/qingflow_mcp/tools/csv_export.py +168 -0
  61. package/src/qingflow_mcp/tools/data_tools.py +284 -0
  62. package/src/qingflow_mcp/tools/login_link_tools.py +34 -6
  63. package/src/qingflow_mcp/tools/record_tools.py +11 -46
  64. package/src/qingflow_mcp/tools/resource_read_tools.py +13 -0
@@ -20,6 +20,7 @@ from ..public_surface import public_builder_contract_tool_names
20
20
  from ..config import DEFAULT_PROFILE
21
21
  from ..errors import QingflowApiError, backend_code_int, backend_code_value_int
22
22
  from ..json_types import JSONObject
23
+ from ..resource_uri import try_build_resource_uri
23
24
  from ..builder_facade.models import (
24
25
  AssociatedResourcesApplyRequest,
25
26
  ChartApplyRequest,
@@ -187,6 +188,26 @@ class AiBuilderTools(ToolBase):
187
188
  registry=app_form_schema_registry(),
188
189
  )
189
190
 
191
+ def _selected_ws_id(self, profile: str) -> int:
192
+ session_profile, _backend_session, _context = self._require_context(profile, require_workspace=True)
193
+ assert session_profile.selected_ws_id is not None
194
+ return session_profile.selected_ws_id
195
+
196
+ def _attach_apply_envelope(self, profile: str, tool_name: str, payload: JSONObject) -> JSONObject:
197
+ result = _attach_builder_apply_envelope(tool_name, payload)
198
+ eligible = [
199
+ item
200
+ for item in result.get("resources") or []
201
+ if isinstance(item, dict)
202
+ and item.get("resource_type") in {"package", "app", "view"}
203
+ and str(item.get("status") or "").lower()
204
+ not in {"failed", "pending", "planned", "readback_pending", "pending_readback", "unknown", "removed"}
205
+ and str(item.get("operation") or "").lower() not in {"failed", "removed", "planned"}
206
+ ]
207
+ if eligible:
208
+ _attach_builder_resource_uris(result, eligible, ws_id=self._selected_ws_id(profile))
209
+ return result
210
+
190
211
  def _apply_app_batch(self, *, tool_name: str, profile: str, apps: list[JSONObject], apply_one) -> JSONObject:
191
212
  started_at = time.perf_counter()
192
213
  batch_item_apply_ms = 0
@@ -199,7 +220,7 @@ class AiBuilderTools(ToolBase):
199
220
  child_total_ms=child_total_ms,
200
221
  total_ms=_elapsed_ms(started_at),
201
222
  )
202
- return _attach_builder_apply_envelope(tool_name, payload)
223
+ return self._attach_apply_envelope(profile, tool_name, payload)
203
224
 
204
225
  if not isinstance(apps, list) or not apps:
205
226
  return finish(
@@ -677,6 +698,41 @@ class AiBuilderTools(ToolBase):
677
698
  def app_flow_get_schema(profile: str = DEFAULT_PROFILE, schema_version: str = "") -> JSONObject:
678
699
  return self.app_flow_get_schema(profile=profile, schema_version=schema_version or None)
679
700
 
701
+ @mcp.tool()
702
+ def app_flow_validate(
703
+ profile: str = DEFAULT_PROFILE,
704
+ spec: JSONObject | None = None,
705
+ schema_version: str = "",
706
+ ) -> JSONObject:
707
+ return self.app_flow_validate(
708
+ profile=profile,
709
+ spec=spec or {},
710
+ schema_version=schema_version or None,
711
+ )
712
+
713
+ @mcp.tool()
714
+ def app_flow_list_versions(
715
+ profile: str = DEFAULT_PROFILE,
716
+ app_key: str = "",
717
+ ) -> JSONObject:
718
+ return self.app_flow_list_versions(profile=profile, app_key=app_key)
719
+
720
+ @mcp.tool()
721
+ def app_flow_restore_version(
722
+ profile: str = DEFAULT_PROFILE,
723
+ app_key: str = "",
724
+ version_id: str = "",
725
+ idempotency_key: str = "",
726
+ schema_version: str = "",
727
+ ) -> JSONObject:
728
+ return self.app_flow_restore_version(
729
+ profile=profile,
730
+ app_key=app_key,
731
+ version_id=version_id,
732
+ idempotency_key=idempotency_key or None,
733
+ schema_version=schema_version or None,
734
+ )
735
+
680
736
  @mcp.tool()
681
737
  def app_flow_apply(
682
738
  profile: str = DEFAULT_PROFILE,
@@ -686,7 +742,10 @@ class AiBuilderTools(ToolBase):
686
742
  idempotency_key: str = "",
687
743
  schema_version: str = "",
688
744
  patch_nodes: list[JSONObject] | None = None,
745
+ confirm_ticket: str = "",
746
+ base_version: int | None = None,
689
747
  ) -> JSONObject:
748
+ """执行应用相关逻辑。"""
690
749
  return self.app_flow_apply(
691
750
  profile=profile,
692
751
  app_key=app_key,
@@ -695,6 +754,8 @@ class AiBuilderTools(ToolBase):
695
754
  idempotency_key=idempotency_key or None,
696
755
  schema_version=schema_version or None,
697
756
  patch_nodes=patch_nodes,
757
+ confirm_ticket=confirm_ticket or None,
758
+ base_version=base_version,
698
759
  )
699
760
 
700
761
  @mcp.tool()
@@ -841,16 +902,30 @@ class AiBuilderTools(ToolBase):
841
902
  being_draft: bool = False,
842
903
  ) -> JSONObject:
843
904
  """Read a canonical draft or published AppForm resource."""
844
- return self._app_form.get(
905
+ result = self._app_form.get(
845
906
  profile=profile,
846
907
  app_key=app_key,
847
908
  schema_version=schema_version,
848
909
  being_draft=being_draft,
849
910
  )
911
+ if result.get("status") == "success":
912
+ _attach_uri(result, result, ws_id=self._selected_ws_id(profile), resource_type="app", app_key=app_key)
913
+ return result
850
914
 
851
915
  def app_form_apply(self, *, profile: str, spec: JSONObject) -> JSONObject:
852
916
  """Reconcile one complete declarative AppForm target."""
853
- return self._app_form.apply(profile=profile, spec=spec)
917
+ result = self._app_form.apply(profile=profile, spec=spec)
918
+ if result.get("appKey") and (
919
+ str(result.get("status") or "") == "success" or result.get("createdAppPreserved") is True
920
+ ):
921
+ _attach_uri(
922
+ result,
923
+ result,
924
+ ws_id=self._selected_ws_id(profile),
925
+ resource_type="app",
926
+ app_key=result.get("appKey"),
927
+ )
928
+ return result
854
929
 
855
930
  def app_form_evaluate_formula(
856
931
  self,
@@ -917,12 +992,24 @@ class AiBuilderTools(ToolBase):
917
992
  def package_list(self, *, profile: str, trial_status: str = "all", query: str = "") -> JSONObject:
918
993
  """执行分组与包相关逻辑。"""
919
994
  normalized_args = {"trial_status": trial_status, "query": query}
920
- return _safe_tool_call(
995
+ result = _safe_tool_call(
921
996
  lambda: self._facade.package_list(profile=profile, trial_status=trial_status, query=query),
922
997
  error_code="PACKAGE_LIST_FAILED",
923
998
  normalized_args=normalized_args,
924
999
  suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": trial_status, "query": query}},
925
1000
  )
1001
+ if result.get("status") == "success":
1002
+ ws_id = self._selected_ws_id(profile)
1003
+ for item in result.get("items") or []:
1004
+ if isinstance(item, dict):
1005
+ _attach_uri(
1006
+ item,
1007
+ result,
1008
+ ws_id=ws_id,
1009
+ resource_type="package",
1010
+ package_id=item.get("package_id"),
1011
+ )
1012
+ return result
926
1013
 
927
1014
  def package_resolve(self, *, profile: str, package_name: str) -> JSONObject:
928
1015
  """执行分组与包相关逻辑。"""
@@ -1062,12 +1149,23 @@ class AiBuilderTools(ToolBase):
1062
1149
  def package_get(self, *, profile: str, package_id: int | None = None) -> JSONObject:
1063
1150
  """执行分组与包相关逻辑。"""
1064
1151
  normalized_args = {"package_id": package_id}
1065
- return _publicize_package_fields(_safe_tool_call(
1152
+ result = _publicize_package_fields(_safe_tool_call(
1066
1153
  lambda: self._facade.package_get(profile=profile, package_id=package_id),
1067
1154
  error_code="PACKAGE_GET_FAILED",
1068
1155
  normalized_args=normalized_args,
1069
1156
  suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
1070
1157
  ))
1158
+ if result.get("status") == "success":
1159
+ ws_id = self._selected_ws_id(profile)
1160
+ _attach_uri(
1161
+ result,
1162
+ result,
1163
+ ws_id=ws_id,
1164
+ resource_type="package",
1165
+ package_id=result.get("package_id"),
1166
+ )
1167
+ _attach_package_item_uris(result.get("items"), result, ws_id=ws_id)
1168
+ return result
1071
1169
 
1072
1170
  def package_apply(
1073
1171
  self,
@@ -1097,7 +1195,7 @@ class AiBuilderTools(ToolBase):
1097
1195
  try:
1098
1196
  visibility_patch = VisibilityPatch.model_validate(visibility)
1099
1197
  except ValidationError as exc:
1100
- return _attach_builder_apply_envelope(
1198
+ return self._attach_apply_envelope(profile,
1101
1199
  "package_apply",
1102
1200
  _visibility_validation_failure(str(exc), tool_name="package_apply", exc=exc),
1103
1201
  )
@@ -1108,7 +1206,7 @@ class AiBuilderTools(ToolBase):
1108
1206
  creating=package_id is None and bool(str(package_name or "").strip()),
1109
1207
  )
1110
1208
  if icon_failure is not None:
1111
- return _attach_builder_apply_envelope("package_apply", icon_failure)
1209
+ return self._attach_apply_envelope(profile, "package_apply", icon_failure)
1112
1210
  normalized_args = {
1113
1211
  "package_id": package_id,
1114
1212
  **({"package_name": package_name} if str(package_name or "").strip() else {}),
@@ -1133,7 +1231,7 @@ class AiBuilderTools(ToolBase):
1133
1231
  normalized_args=normalized_args,
1134
1232
  suggested_next_call={"tool_name": "package_apply", "arguments": {"profile": profile, **normalized_args}},
1135
1233
  ))
1136
- return _attach_builder_apply_envelope("package_apply", result)
1234
+ return self._attach_apply_envelope(profile, "package_apply", result)
1137
1235
 
1138
1236
  def package_update(
1139
1237
  self,
@@ -1187,7 +1285,7 @@ class AiBuilderTools(ToolBase):
1187
1285
  "being_copy_data": being_copy_data,
1188
1286
  "solution_source": solution_source,
1189
1287
  }
1190
- return _safe_tool_call(
1288
+ result = _safe_tool_call(
1191
1289
  lambda: self._facade.solution_install(
1192
1290
  profile=profile,
1193
1291
  solution_key=solution_key,
@@ -1198,6 +1296,7 @@ class AiBuilderTools(ToolBase):
1198
1296
  normalized_args=normalized_args,
1199
1297
  suggested_next_call={"tool_name": "solution_install", "arguments": {"profile": profile, **normalized_args}},
1200
1298
  )
1299
+ return self._attach_apply_envelope(profile, "solution_install", result)
1201
1300
 
1202
1301
  def member_search(
1203
1302
  self,
@@ -1394,7 +1493,7 @@ class AiBuilderTools(ToolBase):
1394
1493
  try:
1395
1494
  request = CustomButtonsApplyRequest.model_validate(raw_request)
1396
1495
  except ValidationError as exc:
1397
- return _attach_builder_apply_envelope("app_custom_buttons_apply", _validation_failure(
1496
+ return self._attach_apply_envelope(profile, "app_custom_buttons_apply", _validation_failure(
1398
1497
  str(exc),
1399
1498
  tool_name="app_custom_buttons_apply",
1400
1499
  exc=exc,
@@ -1418,7 +1517,7 @@ class AiBuilderTools(ToolBase):
1418
1517
  },
1419
1518
  ))
1420
1519
  normalized_args = request.model_dump(mode="json")
1421
- return _attach_builder_apply_envelope("app_custom_buttons_apply", _safe_tool_call(
1520
+ return self._attach_apply_envelope(profile, "app_custom_buttons_apply", _safe_tool_call(
1422
1521
  lambda: self._facade.app_custom_buttons_apply(profile=profile, request=request),
1423
1522
  error_code="CUSTOM_BUTTONS_APPLY_FAILED",
1424
1523
  normalized_args=normalized_args,
@@ -1475,7 +1574,7 @@ class AiBuilderTools(ToolBase):
1475
1574
  try:
1476
1575
  request = AssociatedResourcesApplyRequest.model_validate(raw_request)
1477
1576
  except ValidationError as exc:
1478
- return _attach_builder_apply_envelope("app_associated_resources_apply", _validation_failure(
1577
+ return self._attach_apply_envelope(profile, "app_associated_resources_apply", _validation_failure(
1479
1578
  str(exc),
1480
1579
  tool_name="app_associated_resources_apply",
1481
1580
  exc=exc,
@@ -1504,7 +1603,7 @@ class AiBuilderTools(ToolBase):
1504
1603
  },
1505
1604
  ))
1506
1605
  normalized_args = request.model_dump(mode="json", exclude_none=True)
1507
- return _attach_builder_apply_envelope("app_associated_resources_apply", _safe_tool_call(
1606
+ return self._attach_apply_envelope(profile, "app_associated_resources_apply", _safe_tool_call(
1508
1607
  lambda: self._facade.app_associated_resources_apply(profile=profile, request=request),
1509
1608
  error_code="ASSOCIATED_RESOURCES_APPLY_FAILED",
1510
1609
  normalized_args=normalized_args,
@@ -1629,12 +1728,27 @@ class AiBuilderTools(ToolBase):
1629
1728
  read_one=lambda item_app_key: self.app_get(profile=profile, app_key=item_app_key, app_keys=None),
1630
1729
  )
1631
1730
  normalized_args = {"app_key": app_key}
1632
- return _publicize_package_fields(_safe_tool_call(
1731
+ result = _publicize_package_fields(_safe_tool_call(
1633
1732
  lambda: self._facade.app_get(profile=profile, app_key=app_key),
1634
1733
  error_code="APP_GET_FAILED",
1635
1734
  normalized_args=normalized_args,
1636
1735
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
1637
1736
  ))
1737
+ if result.get("status") == "success":
1738
+ ws_id = self._selected_ws_id(profile)
1739
+ _attach_uri(result, result, ws_id=ws_id, resource_type="app", app_key=app_key)
1740
+ for view in result.get("views") or []:
1741
+ if isinstance(view, dict) and (view.get("view_key") or view.get("viewKey")):
1742
+ _attach_uri(
1743
+ view,
1744
+ result,
1745
+ ws_id=ws_id,
1746
+ resource_type="view",
1747
+ app_key=app_key,
1748
+ view_kind="custom",
1749
+ view_key=view.get("view_key") or view.get("viewKey"),
1750
+ )
1751
+ return result
1638
1752
 
1639
1753
  def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
1640
1754
  """执行应用相关逻辑。"""
@@ -1727,12 +1841,27 @@ class AiBuilderTools(ToolBase):
1727
1841
  read_one=lambda item_app_key: self.app_get_views(profile=profile, app_key=item_app_key, app_keys=None),
1728
1842
  )
1729
1843
  normalized_args = {"app_key": app_key}
1730
- return _safe_tool_call(
1844
+ result = _safe_tool_call(
1731
1845
  lambda: self._facade.app_get_views(profile=profile, app_key=app_key),
1732
1846
  error_code="APP_GET_VIEWS_FAILED",
1733
1847
  normalized_args=normalized_args,
1734
1848
  suggested_next_call={"tool_name": "app_get_views", "arguments": {"profile": profile, "app_key": app_key}},
1735
1849
  )
1850
+ if result.get("status") == "success":
1851
+ ws_id = self._selected_ws_id(profile)
1852
+ _attach_uri(result, result, ws_id=ws_id, resource_type="app", app_key=app_key)
1853
+ for view in result.get("views") or []:
1854
+ if isinstance(view, dict) and (view.get("view_key") or view.get("viewKey")):
1855
+ _attach_uri(
1856
+ view,
1857
+ result,
1858
+ ws_id=ws_id,
1859
+ resource_type="view",
1860
+ app_key=app_key,
1861
+ view_kind="custom",
1862
+ view_key=view.get("view_key") or view.get("viewKey"),
1863
+ )
1864
+ return result
1736
1865
 
1737
1866
  def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
1738
1867
  """执行应用相关逻辑。"""
@@ -1779,6 +1908,50 @@ class AiBuilderTools(ToolBase):
1779
1908
  suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile, **normalized_args}},
1780
1909
  )
1781
1910
 
1911
+ def app_flow_validate(self, *, profile: str, spec: JSONObject, schema_version: str | None = None) -> JSONObject:
1912
+ """离线校验 WorkflowSpec(apply 前预检,不写后端)。"""
1913
+ normalized_args = {"spec": spec, "schema_version": schema_version}
1914
+ return _safe_tool_call(
1915
+ lambda: self._facade.flow_validate(profile=profile, spec=spec, schema_version=schema_version),
1916
+ error_code="APP_FLOW_VALIDATE_FAILED",
1917
+ normalized_args=normalized_args,
1918
+ suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile}},
1919
+ )
1920
+
1921
+ def app_flow_list_versions(self, *, profile: str, app_key: str) -> JSONObject:
1922
+ """列出某应用的 WorkflowSpec 版本快照(version_id/snapshot_type/时间)。"""
1923
+ normalized_args = {"app_key": app_key}
1924
+ return _safe_tool_call(
1925
+ lambda: self._facade.flow_versions(profile=profile, app_key=app_key),
1926
+ error_code="APP_FLOW_LIST_VERSIONS_FAILED",
1927
+ normalized_args=normalized_args,
1928
+ suggested_next_call={"tool_name": "app_flow_list_versions", "arguments": {"profile": profile, "app_key": app_key}},
1929
+ )
1930
+
1931
+ def app_flow_restore_version(
1932
+ self,
1933
+ *,
1934
+ profile: str,
1935
+ app_key: str,
1936
+ version_id: str,
1937
+ idempotency_key: str | None = None,
1938
+ schema_version: str | None = None,
1939
+ ) -> JSONObject:
1940
+ """回滚到指定版本快照,并回读当前 spec 与目标版本比对确认。"""
1941
+ normalized_args = {"app_key": app_key, "version_id": version_id}
1942
+ return _safe_tool_call(
1943
+ lambda: self._facade.flow_restore_version(
1944
+ profile=profile,
1945
+ app_key=app_key,
1946
+ version_id=version_id,
1947
+ idempotency_key=idempotency_key,
1948
+ schema_version=schema_version,
1949
+ ),
1950
+ error_code="APP_FLOW_RESTORE_VERSION_FAILED",
1951
+ normalized_args=normalized_args,
1952
+ suggested_next_call={"tool_name": "app_flow_list_versions", "arguments": {"profile": profile, "app_key": app_key}},
1953
+ )
1954
+
1782
1955
  def app_read_charts_summary(self, *, profile: str, app_key: str) -> JSONObject:
1783
1956
  """执行应用相关逻辑。"""
1784
1957
  normalized_args = {"app_key": app_key}
@@ -1871,12 +2044,27 @@ class AiBuilderTools(ToolBase):
1871
2044
  """执行视图相关逻辑。"""
1872
2045
  resolved_view_key = _normalize_builder_view_key(str(view_key or viewgraph_key or "").strip())
1873
2046
  normalized_args = {"view_key": resolved_view_key}
1874
- return _safe_tool_call(
2047
+ result = _safe_tool_call(
1875
2048
  lambda: self._facade.view_get(profile=profile, view_key=resolved_view_key),
1876
2049
  error_code="VIEW_GET_FAILED",
1877
2050
  normalized_args=normalized_args,
1878
2051
  suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, **normalized_args}},
1879
2052
  )
2053
+ if result.get("status") == "success":
2054
+ config = result.get("config") if isinstance(result.get("config"), dict) else {}
2055
+ base_info = result.get("base_info") if isinstance(result.get("base_info"), dict) else {}
2056
+ app_key = str(config.get("appKey") or config.get("formKey") or base_info.get("appKey") or base_info.get("formKey") or "").strip()
2057
+ if app_key:
2058
+ _attach_uri(
2059
+ result,
2060
+ result,
2061
+ ws_id=self._selected_ws_id(profile),
2062
+ resource_type="view",
2063
+ app_key=app_key,
2064
+ view_kind="custom",
2065
+ view_key=resolved_view_key,
2066
+ )
2067
+ return result
1880
2068
 
1881
2069
  def chart_get(
1882
2070
  self,
@@ -2165,7 +2353,7 @@ class AiBuilderTools(ToolBase):
2165
2353
  )
2166
2354
  failure = normalized_apps_payload.get("failure")
2167
2355
  if isinstance(failure, dict):
2168
- return _attach_builder_apply_envelope("app_schema_apply", failure)
2356
+ return self._attach_apply_envelope(profile, "app_schema_apply", failure)
2169
2357
  package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
2170
2358
  apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
2171
2359
  input_warnings = list(normalized_apps_payload.get("warnings") or [])
@@ -2181,7 +2369,7 @@ class AiBuilderTools(ToolBase):
2181
2369
  )
2182
2370
  failure = form_result.get("failure")
2183
2371
  if isinstance(failure, dict):
2184
- return _attach_builder_apply_envelope("app_schema_apply", failure)
2372
+ return self._attach_apply_envelope(profile, "app_schema_apply", failure)
2185
2373
  normalized_apps.append(form_result.get("payload") or app_item)
2186
2374
  apps = normalized_apps
2187
2375
  result = self._app_schema_apply_multi(
@@ -2195,7 +2383,7 @@ class AiBuilderTools(ToolBase):
2195
2383
  result_warnings = list(result.get("warnings") or [])
2196
2384
  result_warnings.extend(deepcopy(input_warnings))
2197
2385
  result["warnings"] = result_warnings
2198
- return _attach_builder_apply_envelope("app_schema_apply", result)
2386
+ return self._attach_apply_envelope(profile, "app_schema_apply", result)
2199
2387
  inline_form_layout_sections: list[JSONObject] = []
2200
2388
  if form:
2201
2389
  form_payload: JSONObject = {"form": form}
@@ -2208,7 +2396,7 @@ class AiBuilderTools(ToolBase):
2208
2396
  )
2209
2397
  failure = form_result.get("failure")
2210
2398
  if isinstance(failure, dict):
2211
- return _attach_builder_apply_envelope("app_schema_apply", failure)
2399
+ return self._attach_apply_envelope(profile, "app_schema_apply", failure)
2212
2400
  normalized_payload = form_result.get("payload") if isinstance(form_result.get("payload"), dict) else {}
2213
2401
  add_fields = list(normalized_payload.get("add_fields") or [])
2214
2402
  inline_form_layout_sections = list(form_result.get("layout_sections") or [])
@@ -2246,7 +2434,7 @@ class AiBuilderTools(ToolBase):
2246
2434
  inline_form_layout_sections=inline_form_layout_sections,
2247
2435
  ),
2248
2436
  )
2249
- return _attach_builder_apply_envelope("app_schema_apply", result)
2437
+ return self._attach_apply_envelope(profile, "app_schema_apply", result)
2250
2438
 
2251
2439
  def _app_schema_apply_multi(
2252
2440
  self,
@@ -3525,7 +3713,7 @@ class AiBuilderTools(ToolBase):
3525
3713
  sections=sections,
3526
3714
  ),
3527
3715
  )
3528
- return _attach_builder_apply_envelope("app_layout_apply", result)
3716
+ return self._attach_apply_envelope(profile, "app_layout_apply", result)
3529
3717
 
3530
3718
  def _app_layout_apply_once(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
3531
3719
  """执行内部辅助逻辑。"""
@@ -3580,6 +3768,8 @@ class AiBuilderTools(ToolBase):
3580
3768
  idempotency_key: str | None = None,
3581
3769
  schema_version: str | None = None,
3582
3770
  patch_nodes: list[JSONObject] | None = None,
3771
+ confirm_ticket: str | None = None,
3772
+ base_version: int | None = None,
3583
3773
  ) -> JSONObject:
3584
3774
  """执行应用相关逻辑。"""
3585
3775
  if patch_nodes is not None:
@@ -3589,8 +3779,9 @@ class AiBuilderTools(ToolBase):
3589
3779
  "patch_nodes": patch_nodes,
3590
3780
  "idempotency_key": idempotency_key,
3591
3781
  "schema_version": schema_version,
3782
+ "base_version": base_version,
3592
3783
  }
3593
- return _attach_builder_apply_envelope("app_flow_apply", _safe_tool_call(
3784
+ return self._attach_apply_envelope(profile, "app_flow_apply", _safe_tool_call(
3594
3785
  lambda: self._facade.flow_patch_nodes(
3595
3786
  profile=profile,
3596
3787
  app_key=app_key,
@@ -3610,8 +3801,10 @@ class AiBuilderTools(ToolBase):
3610
3801
  "spec": spec,
3611
3802
  "idempotency_key": idempotency_key,
3612
3803
  "schema_version": schema_version,
3804
+ "confirm_ticket": confirm_ticket,
3805
+ "base_version": base_version,
3613
3806
  }
3614
- return _attach_builder_apply_envelope("app_flow_apply", _safe_tool_call(
3807
+ return self._attach_apply_envelope(profile, "app_flow_apply", _safe_tool_call(
3615
3808
  lambda: self._facade.flow_apply(
3616
3809
  profile=profile,
3617
3810
  app_key=app_key,
@@ -3619,6 +3812,8 @@ class AiBuilderTools(ToolBase):
3619
3812
  publish=publish,
3620
3813
  idempotency_key=idempotency_key,
3621
3814
  schema_version=schema_version,
3815
+ confirm_ticket=confirm_ticket,
3816
+ base_version=base_version,
3622
3817
  ),
3623
3818
  error_code="FLOW_SPEC_APPLY_FAILED",
3624
3819
  normalized_args=normalized_args,
@@ -3647,9 +3842,9 @@ class AiBuilderTools(ToolBase):
3647
3842
  transitions=effective_transitions,
3648
3843
  ),
3649
3844
  )
3650
- return _attach_builder_apply_envelope("app_flow_apply", result)
3845
+ return self._attach_apply_envelope(profile, "app_flow_apply", result)
3651
3846
  normalized_args = {"app_key": app_key, "publish": publish}
3652
- return _attach_builder_apply_envelope(
3847
+ return self._attach_apply_envelope(profile,
3653
3848
  "app_flow_apply",
3654
3849
  {
3655
3850
  "status": "failed",
@@ -3784,7 +3979,7 @@ class AiBuilderTools(ToolBase):
3784
3979
  patch_views=patch_views or [],
3785
3980
  ),
3786
3981
  )
3787
- return _attach_builder_apply_envelope("app_views_apply", result)
3982
+ return self._attach_apply_envelope(profile, "app_views_apply", result)
3788
3983
 
3789
3984
  def _app_views_apply_view_batch(
3790
3985
  self,
@@ -3795,7 +3990,7 @@ class AiBuilderTools(ToolBase):
3795
3990
  ) -> JSONObject:
3796
3991
  started_at = time.perf_counter()
3797
3992
  if not isinstance(views, list) or not views:
3798
- return _attach_builder_apply_envelope(
3993
+ return self._attach_apply_envelope(profile,
3799
3994
  "app_views_apply",
3800
3995
  _config_failure(
3801
3996
  tool_name="app_views_apply",
@@ -4026,7 +4221,7 @@ class AiBuilderTools(ToolBase):
4026
4221
  view_write_parallel_wall_ms=view_write_parallel_wall_ms,
4027
4222
  total_ms=_elapsed_ms(started_at),
4028
4223
  )
4029
- return _attach_builder_apply_envelope("app_views_apply", payload)
4224
+ return self._attach_apply_envelope(profile, "app_views_apply", payload)
4030
4225
 
4031
4226
  def _app_views_apply_once(
4032
4227
  self,
@@ -4149,7 +4344,7 @@ class AiBuilderTools(ToolBase):
4149
4344
  }
4150
4345
  )
4151
4346
  except ValidationError as exc:
4152
- return _attach_builder_apply_envelope("app_charts_apply", _visibility_validation_failure(
4347
+ return self._attach_apply_envelope(profile, "app_charts_apply", _visibility_validation_failure(
4153
4348
  str(exc),
4154
4349
  tool_name="app_charts_apply",
4155
4350
  exc=exc,
@@ -4165,7 +4360,7 @@ class AiBuilderTools(ToolBase):
4165
4360
  },
4166
4361
  ))
4167
4362
  normalized_args = request.model_dump(mode="json")
4168
- return _attach_builder_apply_envelope("app_charts_apply", _safe_tool_call(
4363
+ return self._attach_apply_envelope(profile, "app_charts_apply", _safe_tool_call(
4169
4364
  lambda: self._facade.chart_apply(profile=profile, request=request),
4170
4365
  error_code="CHART_APPLY_FAILED",
4171
4366
  normalized_args=normalized_args,
@@ -4201,7 +4396,7 @@ class AiBuilderTools(ToolBase):
4201
4396
  "publish": publish,
4202
4397
  "patch_sections": patch_sections,
4203
4398
  }
4204
- return _attach_builder_apply_envelope("portal_apply", _publicize_package_fields(_safe_tool_call(
4399
+ return self._attach_apply_envelope(profile, "portal_apply", _publicize_package_fields(_safe_tool_call(
4205
4400
  lambda: self._facade.portal_patch_sections(
4206
4401
  profile=profile,
4207
4402
  dash_key=dash_key,
@@ -4248,7 +4443,7 @@ class AiBuilderTools(ToolBase):
4248
4443
  try:
4249
4444
  request = PortalApplyRequest.model_validate(request_payload)
4250
4445
  except ValidationError as exc:
4251
- return _attach_builder_apply_envelope("portal_apply", _visibility_validation_failure(
4446
+ return self._attach_apply_envelope(profile, "portal_apply", _visibility_validation_failure(
4252
4447
  str(exc),
4253
4448
  tool_name="portal_apply",
4254
4449
  exc=exc,
@@ -4279,14 +4474,14 @@ class AiBuilderTools(ToolBase):
4279
4474
  creating=not bool(str(request.dash_key or "").strip()),
4280
4475
  )
4281
4476
  if icon_failure is not None:
4282
- return _attach_builder_apply_envelope("portal_apply", icon_failure)
4477
+ return self._attach_apply_envelope(profile, "portal_apply", icon_failure)
4283
4478
  result = _publicize_package_fields(_safe_tool_call(
4284
4479
  lambda: self._facade.portal_apply(profile=profile, request=request),
4285
4480
  error_code="PORTAL_APPLY_FAILED",
4286
4481
  normalized_args=normalized_args,
4287
4482
  suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
4288
4483
  ))
4289
- return _attach_builder_apply_envelope("portal_apply", result)
4484
+ return self._attach_apply_envelope(profile, "portal_apply", result)
4290
4485
 
4291
4486
  def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
4292
4487
  """执行门户删除逻辑。"""
@@ -4297,7 +4492,7 @@ class AiBuilderTools(ToolBase):
4297
4492
  normalized_args=normalized_args,
4298
4493
  suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, **normalized_args}},
4299
4494
  ))
4300
- return _attach_builder_apply_envelope("portal_delete", result)
4495
+ return self._attach_apply_envelope(profile, "portal_delete", result)
4301
4496
 
4302
4497
  def app_publish_verify(
4303
4498
  self,
@@ -4336,7 +4531,7 @@ class AiBuilderTools(ToolBase):
4336
4531
  expected_package_tag_id=expected_package_id,
4337
4532
  ),
4338
4533
  ))
4339
- return _attach_builder_apply_envelope("app_publish_verify", result)
4534
+ return self._attach_apply_envelope(profile, "app_publish_verify", result)
4340
4535
 
4341
4536
  def _retry_after_self_lock_release(self, *, profile: str, result: JSONObject, retry_call) -> JSONObject:
4342
4537
  """执行内部辅助逻辑。"""
@@ -5613,7 +5808,7 @@ def _safe_tool_call(
5613
5808
  result = call()
5614
5809
  except (QingflowApiError, RuntimeError) as error:
5615
5810
  api_error = _coerce_api_error(error)
5616
- public_http_status = None if api_error.http_status == 404 else api_error.http_status
5811
+ # #162 D5:404 不再抹除 http_status,错误信封保留真实传输层事实
5617
5812
  result = {
5618
5813
  "status": "failed",
5619
5814
  "error_code": error_code,
@@ -5632,7 +5827,7 @@ def _safe_tool_call(
5632
5827
  "suggested_next_call": suggested_next_call,
5633
5828
  "request_id": api_error.request_id,
5634
5829
  "backend_code": api_error.backend_code,
5635
- "http_status": public_http_status,
5830
+ "http_status": api_error.http_status,
5636
5831
  "noop": False,
5637
5832
  "verification": {},
5638
5833
  }
@@ -5702,6 +5897,68 @@ def _publicize_package_fields(value):
5702
5897
  return public
5703
5898
 
5704
5899
 
5900
+ def _append_uri_warning(payload: JSONObject, warning: JSONObject | None) -> None:
5901
+ if not warning:
5902
+ return
5903
+ warnings = payload.setdefault("warnings", [])
5904
+ if isinstance(warnings, list) and not any(
5905
+ isinstance(item, dict) and item.get("code") == warning.get("code") for item in warnings
5906
+ ):
5907
+ warnings.append(warning)
5908
+
5909
+
5910
+ def _attach_uri(target: JSONObject, payload: JSONObject, *, ws_id: int, **identity: Any) -> None:
5911
+ uri, warning = try_build_resource_uri(ws_id=ws_id, **identity)
5912
+ if uri:
5913
+ target["uri"] = uri
5914
+ _append_uri_warning(payload, warning)
5915
+
5916
+
5917
+ def _attach_builder_resource_uris(payload: JSONObject, resources: list[JSONObject], *, ws_id: int) -> None:
5918
+ for resource in resources:
5919
+ resource_type = str(resource.get("resource_type") or "")
5920
+ ids = resource.get("ids") if isinstance(resource.get("ids"), dict) else {}
5921
+ if resource_type == "package":
5922
+ _attach_uri(
5923
+ resource,
5924
+ payload,
5925
+ ws_id=ws_id,
5926
+ resource_type="package",
5927
+ package_id=resource.get("id") or ids.get("package_id"),
5928
+ )
5929
+ elif resource_type == "app":
5930
+ _attach_uri(
5931
+ resource,
5932
+ payload,
5933
+ ws_id=ws_id,
5934
+ resource_type="app",
5935
+ app_key=resource.get("key") or ids.get("app_key"),
5936
+ )
5937
+ elif resource_type == "view":
5938
+ parent = resource.get("parent") if isinstance(resource.get("parent"), dict) else {}
5939
+ _attach_uri(
5940
+ resource,
5941
+ payload,
5942
+ ws_id=ws_id,
5943
+ resource_type="view",
5944
+ app_key=ids.get("app_key") or (parent.get("key") if parent.get("resource_type") == "app" else None),
5945
+ view_kind="custom",
5946
+ view_key=resource.get("key") or ids.get("view_key"),
5947
+ )
5948
+
5949
+
5950
+ def _attach_package_item_uris(items: Any, payload: JSONObject, *, ws_id: int) -> None:
5951
+ if not isinstance(items, list):
5952
+ return
5953
+ for item in items:
5954
+ if not isinstance(item, dict):
5955
+ continue
5956
+ if item.get("type") == "app" and item.get("app_key"):
5957
+ _attach_uri(item, payload, ws_id=ws_id, resource_type="app", app_key=item.get("app_key"))
5958
+ if item.get("type") == "group":
5959
+ _attach_package_item_uris(item.get("items"), payload, ws_id=ws_id)
5960
+
5961
+
5705
5962
  def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) -> JSONObject:
5706
5963
  public = deepcopy(contract)
5707
5964
  if tool_name not in BUILDER_APPLY_TOOL_NAMES:
@@ -5714,7 +5971,7 @@ def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) ->
5714
5971
  public["output_contract"] = {
5715
5972
  "schema_version": BUILDER_APPLY_SCHEMA_VERSION,
5716
5973
  "preferred_ui_fields": ["operation", "summary", "resources"],
5717
- "resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "icon_config", "error_code", "message"],
5974
+ "resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "uri", "icon_config", "error_code", "message"],
5718
5975
  "json_paths": _builder_apply_json_paths(),
5719
5976
  "legacy_fields_preserved": True,
5720
5977
  }
@@ -5766,6 +6023,7 @@ def _builder_apply_json_paths() -> JSONObject:
5766
6023
  "resource_keys": "$.resources[].key",
5767
6024
  "resource_ids": "$.resources[].id",
5768
6025
  "resource_statuses": "$.resources[].status",
6026
+ "resource_uris": "$.resources[].uri",
5769
6027
  }
5770
6028
 
5771
6029
 
@@ -5855,7 +6113,9 @@ def _builder_verification_truthy(value: object) -> bool:
5855
6113
 
5856
6114
  def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONObject]:
5857
6115
  resources: list[JSONObject]
5858
- if tool_name == "package_apply":
6116
+ if tool_name == "solution_install":
6117
+ resources = [deepcopy(item) for item in payload.get("resources") or [] if isinstance(item, dict)]
6118
+ elif tool_name == "package_apply":
5859
6119
  resources = _builder_package_resources(payload)
5860
6120
  elif tool_name == "app_schema_apply":
5861
6121
  resources = _builder_schema_resources(payload)
@@ -6620,6 +6880,10 @@ def _public_error_message(error_code: str, error: QingflowApiError) -> str:
6620
6880
  return "app is currently locked by another active editor session"
6621
6881
  if error.http_status != 404:
6622
6882
  return error.message
6883
+ # 404 优先保留后端原始 message(agent 排障依据);仅当原始 message 为空时
6884
+ # 才落多路由环境泛化兑底(#162 D5:不再默认泛化丢失真实原因)。
6885
+ if error.message:
6886
+ return f"{error.message} (HTTP 404)"
6623
6887
  mapping = {
6624
6888
  "PACKAGE_LIST_FAILED": "package list is unavailable in the current route",
6625
6889
  "PACKAGE_RESOLVE_FAILED": "package resolution is unavailable in the current route",
@@ -7811,6 +8075,75 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
7811
8075
  "patch_nodes": [{"id": "approve_1", "set": {"name": "主管审批"}}],
7812
8076
  },
7813
8077
  },
8078
+ "app_flow_validate": {
8079
+ "allowed_keys": [
8080
+ "spec",
8081
+ "schema_version",
8082
+ ],
8083
+ "aliases": {
8084
+ "schemaVersion": "schema_version",
8085
+ },
8086
+ "allowed_values": {},
8087
+ "dependency_hints": [
8088
+ "run app_flow_validate on the drafted WorkflowSpec before app_flow_apply to catch schema/structure issues without writing",
8089
+ ],
8090
+ "execution_notes": [
8091
+ "offline precheck: fetches the WorkflowSpec JSON Schema and runs schema + structural + semantic lint checks",
8092
+ "never writes: the only network call is the schema GET; publish=false is a draft write, not a precheck",
8093
+ "issues[] blocks apply; warnings[] (e.g. rejection-edge cycles) are advisory only",
8094
+ "server-side field-reference semantics are validated at apply time; a clean validate does not guarantee apply success",
8095
+ ],
8096
+ "minimal_example": {
8097
+ "profile": "default",
8098
+ "spec": {"schemaVersion": "1", "nodes": [], "edges": []},
8099
+ },
8100
+ },
8101
+ "app_flow_list_versions": {
8102
+ "allowed_keys": [
8103
+ "app_key",
8104
+ ],
8105
+ "aliases": {},
8106
+ "allowed_values": {},
8107
+ "dependency_hints": [
8108
+ "use app_flow_list_versions to discover version_id values before app_flow_restore_version or app_flow_get(version_id=...)",
8109
+ ],
8110
+ "execution_notes": [
8111
+ "returns items[] with version_id, snapshot_type, created_at_ms/created_at_iso, operator_name, spec_hash per version snapshot",
8112
+ "raw backend entries ride along in details.versions of the full envelope for troubleshooting",
8113
+ ],
8114
+ "minimal_example": {
8115
+ "profile": "default",
8116
+ "app_key": "APP_KEY",
8117
+ },
8118
+ },
8119
+ "app_flow_restore_version": {
8120
+ "allowed_keys": [
8121
+ "app_key",
8122
+ "version_id",
8123
+ "idempotency_key",
8124
+ "schema_version",
8125
+ ],
8126
+ "aliases": {
8127
+ "versionId": "version_id",
8128
+ "idempotencyKey": "idempotency_key",
8129
+ "schemaVersion": "schema_version",
8130
+ },
8131
+ "allowed_values": {},
8132
+ "dependency_hints": [
8133
+ "resolve version_id via app_flow_list_versions, optionally inspect it with app_flow_get(version_id=...), then restore",
8134
+ ],
8135
+ "execution_notes": [
8136
+ "draft-domain write: restores the draft workflow spec to the target version snapshot and does not auto-publish",
8137
+ "maintenance gate applies: restore to a version with more nodes than the threshold is blocked (FLOW_EDIT_UNDER_MAINTENANCE)",
8138
+ "readback verification compares the restored current spec with the target version; mismatch yields partial_success with FLOW_RESTORE_VERIFY_FAILED",
8139
+ "verify the outcome with app_flow_get afterwards when verification reports partial_success",
8140
+ ],
8141
+ "minimal_example": {
8142
+ "profile": "default",
8143
+ "app_key": "APP_KEY",
8144
+ "version_id": "v_1765000000_ab12cd34",
8145
+ },
8146
+ },
7814
8147
  "app_views_plan": {
7815
8148
  "allowed_keys": [
7816
8149
  "app_key",