@qingflow-tech/qingflow-cli 1.1.47-beta.1 → 1.1.47-beta.2

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.
@@ -9,6 +9,7 @@ from .config import DEFAULT_PROFILE
9
9
  from .response_trim import BUILDER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
10
10
  from .session_store import SessionStore
11
11
  from .tools.ai_builder_tools import AiBuilderTools
12
+ from .tools.login_link_tools import LOGIN_LINK_DESCRIPTION, LoginLinkTools
12
13
  from .tools.auth_tools import AuthTools
13
14
  from .tools.feedback_tools import FeedbackTools
14
15
  from .tools.file_tools import FileTools
@@ -41,6 +42,7 @@ def build_builder_server() -> FastMCP:
41
42
  "member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
42
43
  "then app_form_apply for complete app form targets, explicit app_delete for application deletion, and app_flow_apply/app_views_apply/app_associated_resources_apply/app_charts_apply/portal_apply/portal_delete for non-form patches. app_custom_buttons_apply is maintenance-only for explicit standalone button work outside normal view design. These apply/delete tools perform validation, normalization, writes, and readback verification where applicable. AppForm publishes only after a draft change or when the published form lags behind; views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal delete separates DELETE execution from readback verification, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
43
44
  "Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
45
+ "For page handoff, prefer stable resource uri values in read/apply outputs. Do not issue tickets for intermediate reads. If the target browser state is unknown, issue one fresh get_login_link ticket per delivered resource uri; only deliver the stable uri directly after the user confirms the same browser, web origin, account, and workspace are already logged in. A prior ticket, an automated browser session, or another resource ticket is not that evidence, and consumed tickets cannot be reused. Do not open a returned URL unless the user asks; if ticket issuance fails, state that login is required and provide the stable uri as the fallback. "
44
46
  "For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
45
47
  "For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
46
48
  "For builder write/apply results, separate write execution from final readback verification: status=partial_success with write_executed=true and safe_to_retry=false means the write was sent or succeeded, while final readback, publish verification, or metadata confirmation is pending or permission-restricted. Report it as written but unverified; do not repeat the same write blindly. "
@@ -64,6 +66,7 @@ def build_builder_server() -> FastMCP:
64
66
  workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
65
67
  files = wrap_trimmed_methods(FileTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
66
68
  ai_builder = wrap_trimmed_methods(AiBuilderTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
69
+ login_link = LoginLinkTools(sessions, backend)
67
70
  feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
68
71
 
69
72
  @server.tool()
@@ -90,6 +93,10 @@ def build_builder_server() -> FastMCP:
90
93
  def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict:
91
94
  return auth.auth_logout(profile=profile, forget_persisted=forget_persisted)
92
95
 
96
+ @server.tool(description=LOGIN_LINK_DESCRIPTION)
97
+ def get_login_link(profile: str = DEFAULT_PROFILE, redirect_path: str = "") -> dict:
98
+ return login_link.get_login_link(profile=profile, redirect_path=redirect_path)
99
+
93
100
  @server.tool()
94
101
  def workspace_list(
95
102
  profile: str = DEFAULT_PROFILE,
@@ -33,11 +33,12 @@ def build_user_server(*, stateless_http: bool = False) -> FastMCP:
33
33
 
34
34
  ## 页面交接
35
35
 
36
- 用户需要查看结果、继续编辑、处理业务或进入工作区页面时,使用 `get_login_link`;纯数据查询不自动签发链接。
36
+ 用户需要查看结果、继续编辑、处理业务或进入工作区页面时,优先使用业务输出中的稳定 `uri`。纯数据查询和中间读取不签发链接。
37
37
  目标路径由业务工具或领域 Skill 根据真实前端路由确定,不由票据工具根据业务标识推导。
38
38
  已知目标传可选 `redirect_path`;明确指定页面但路径未知时继续定位或说明限制,不猜测,也不静默改为首页。
39
39
  搜索未命中时继续搜索 `login` 或查看服务工具列表,不擅自打开固定正式站点;鉴权或工具失败时如实说明。
40
- 临近交付时再签发,使用返回的完整 URL。要求“给我链接”时仅交付,不提前消费;要求“打开”时才调用浏览器。
40
+ 目标浏览器登录态未知时,默认对每个要交付的资源 `uri` 分别调用一次 `get_login_link`;只有用户确认同一浏览器、网页域名、账号和工作区已经登录时才直接交付稳定 `uri`。调用过票据工具、自动化浏览器已登录或之前签发过票据均不能证明用户浏览器已登录,已消费链接也不能复用。
41
+ 临近交付时再签发,使用返回的完整 URL。要求“给我链接”时仅交付,不提前消费;要求“打开”时才调用浏览器。签发失败时明确说明需要登录,并提供对应稳定 `uri` 作为备用入口。
41
42
  浏览器不可用时提供可点击链接;用“查看记录”“继续编辑”等文案,不展示工作区数字 ID 或原始 JSON。
42
43
  链接约 5 分钟内一次性有效,不能作为长期书签;安全验证及原有业务权限不变。
43
44
 
@@ -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(
@@ -841,16 +862,30 @@ class AiBuilderTools(ToolBase):
841
862
  being_draft: bool = False,
842
863
  ) -> JSONObject:
843
864
  """Read a canonical draft or published AppForm resource."""
844
- return self._app_form.get(
865
+ result = self._app_form.get(
845
866
  profile=profile,
846
867
  app_key=app_key,
847
868
  schema_version=schema_version,
848
869
  being_draft=being_draft,
849
870
  )
871
+ if result.get("status") == "success":
872
+ _attach_uri(result, result, ws_id=self._selected_ws_id(profile), resource_type="app", app_key=app_key)
873
+ return result
850
874
 
851
875
  def app_form_apply(self, *, profile: str, spec: JSONObject) -> JSONObject:
852
876
  """Reconcile one complete declarative AppForm target."""
853
- return self._app_form.apply(profile=profile, spec=spec)
877
+ result = self._app_form.apply(profile=profile, spec=spec)
878
+ if result.get("appKey") and (
879
+ str(result.get("status") or "") == "success" or result.get("createdAppPreserved") is True
880
+ ):
881
+ _attach_uri(
882
+ result,
883
+ result,
884
+ ws_id=self._selected_ws_id(profile),
885
+ resource_type="app",
886
+ app_key=result.get("appKey"),
887
+ )
888
+ return result
854
889
 
855
890
  def app_form_evaluate_formula(
856
891
  self,
@@ -917,12 +952,24 @@ class AiBuilderTools(ToolBase):
917
952
  def package_list(self, *, profile: str, trial_status: str = "all", query: str = "") -> JSONObject:
918
953
  """执行分组与包相关逻辑。"""
919
954
  normalized_args = {"trial_status": trial_status, "query": query}
920
- return _safe_tool_call(
955
+ result = _safe_tool_call(
921
956
  lambda: self._facade.package_list(profile=profile, trial_status=trial_status, query=query),
922
957
  error_code="PACKAGE_LIST_FAILED",
923
958
  normalized_args=normalized_args,
924
959
  suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": trial_status, "query": query}},
925
960
  )
961
+ if result.get("status") == "success":
962
+ ws_id = self._selected_ws_id(profile)
963
+ for item in result.get("items") or []:
964
+ if isinstance(item, dict):
965
+ _attach_uri(
966
+ item,
967
+ result,
968
+ ws_id=ws_id,
969
+ resource_type="package",
970
+ package_id=item.get("package_id"),
971
+ )
972
+ return result
926
973
 
927
974
  def package_resolve(self, *, profile: str, package_name: str) -> JSONObject:
928
975
  """执行分组与包相关逻辑。"""
@@ -1062,12 +1109,23 @@ class AiBuilderTools(ToolBase):
1062
1109
  def package_get(self, *, profile: str, package_id: int | None = None) -> JSONObject:
1063
1110
  """执行分组与包相关逻辑。"""
1064
1111
  normalized_args = {"package_id": package_id}
1065
- return _publicize_package_fields(_safe_tool_call(
1112
+ result = _publicize_package_fields(_safe_tool_call(
1066
1113
  lambda: self._facade.package_get(profile=profile, package_id=package_id),
1067
1114
  error_code="PACKAGE_GET_FAILED",
1068
1115
  normalized_args=normalized_args,
1069
1116
  suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
1070
1117
  ))
1118
+ if result.get("status") == "success":
1119
+ ws_id = self._selected_ws_id(profile)
1120
+ _attach_uri(
1121
+ result,
1122
+ result,
1123
+ ws_id=ws_id,
1124
+ resource_type="package",
1125
+ package_id=result.get("package_id"),
1126
+ )
1127
+ _attach_package_item_uris(result.get("items"), result, ws_id=ws_id)
1128
+ return result
1071
1129
 
1072
1130
  def package_apply(
1073
1131
  self,
@@ -1097,7 +1155,7 @@ class AiBuilderTools(ToolBase):
1097
1155
  try:
1098
1156
  visibility_patch = VisibilityPatch.model_validate(visibility)
1099
1157
  except ValidationError as exc:
1100
- return _attach_builder_apply_envelope(
1158
+ return self._attach_apply_envelope(profile,
1101
1159
  "package_apply",
1102
1160
  _visibility_validation_failure(str(exc), tool_name="package_apply", exc=exc),
1103
1161
  )
@@ -1108,7 +1166,7 @@ class AiBuilderTools(ToolBase):
1108
1166
  creating=package_id is None and bool(str(package_name or "").strip()),
1109
1167
  )
1110
1168
  if icon_failure is not None:
1111
- return _attach_builder_apply_envelope("package_apply", icon_failure)
1169
+ return self._attach_apply_envelope(profile, "package_apply", icon_failure)
1112
1170
  normalized_args = {
1113
1171
  "package_id": package_id,
1114
1172
  **({"package_name": package_name} if str(package_name or "").strip() else {}),
@@ -1133,7 +1191,7 @@ class AiBuilderTools(ToolBase):
1133
1191
  normalized_args=normalized_args,
1134
1192
  suggested_next_call={"tool_name": "package_apply", "arguments": {"profile": profile, **normalized_args}},
1135
1193
  ))
1136
- return _attach_builder_apply_envelope("package_apply", result)
1194
+ return self._attach_apply_envelope(profile, "package_apply", result)
1137
1195
 
1138
1196
  def package_update(
1139
1197
  self,
@@ -1187,7 +1245,7 @@ class AiBuilderTools(ToolBase):
1187
1245
  "being_copy_data": being_copy_data,
1188
1246
  "solution_source": solution_source,
1189
1247
  }
1190
- return _safe_tool_call(
1248
+ result = _safe_tool_call(
1191
1249
  lambda: self._facade.solution_install(
1192
1250
  profile=profile,
1193
1251
  solution_key=solution_key,
@@ -1198,6 +1256,7 @@ class AiBuilderTools(ToolBase):
1198
1256
  normalized_args=normalized_args,
1199
1257
  suggested_next_call={"tool_name": "solution_install", "arguments": {"profile": profile, **normalized_args}},
1200
1258
  )
1259
+ return self._attach_apply_envelope(profile, "solution_install", result)
1201
1260
 
1202
1261
  def member_search(
1203
1262
  self,
@@ -1394,7 +1453,7 @@ class AiBuilderTools(ToolBase):
1394
1453
  try:
1395
1454
  request = CustomButtonsApplyRequest.model_validate(raw_request)
1396
1455
  except ValidationError as exc:
1397
- return _attach_builder_apply_envelope("app_custom_buttons_apply", _validation_failure(
1456
+ return self._attach_apply_envelope(profile, "app_custom_buttons_apply", _validation_failure(
1398
1457
  str(exc),
1399
1458
  tool_name="app_custom_buttons_apply",
1400
1459
  exc=exc,
@@ -1418,7 +1477,7 @@ class AiBuilderTools(ToolBase):
1418
1477
  },
1419
1478
  ))
1420
1479
  normalized_args = request.model_dump(mode="json")
1421
- return _attach_builder_apply_envelope("app_custom_buttons_apply", _safe_tool_call(
1480
+ return self._attach_apply_envelope(profile, "app_custom_buttons_apply", _safe_tool_call(
1422
1481
  lambda: self._facade.app_custom_buttons_apply(profile=profile, request=request),
1423
1482
  error_code="CUSTOM_BUTTONS_APPLY_FAILED",
1424
1483
  normalized_args=normalized_args,
@@ -1475,7 +1534,7 @@ class AiBuilderTools(ToolBase):
1475
1534
  try:
1476
1535
  request = AssociatedResourcesApplyRequest.model_validate(raw_request)
1477
1536
  except ValidationError as exc:
1478
- return _attach_builder_apply_envelope("app_associated_resources_apply", _validation_failure(
1537
+ return self._attach_apply_envelope(profile, "app_associated_resources_apply", _validation_failure(
1479
1538
  str(exc),
1480
1539
  tool_name="app_associated_resources_apply",
1481
1540
  exc=exc,
@@ -1504,7 +1563,7 @@ class AiBuilderTools(ToolBase):
1504
1563
  },
1505
1564
  ))
1506
1565
  normalized_args = request.model_dump(mode="json", exclude_none=True)
1507
- return _attach_builder_apply_envelope("app_associated_resources_apply", _safe_tool_call(
1566
+ return self._attach_apply_envelope(profile, "app_associated_resources_apply", _safe_tool_call(
1508
1567
  lambda: self._facade.app_associated_resources_apply(profile=profile, request=request),
1509
1568
  error_code="ASSOCIATED_RESOURCES_APPLY_FAILED",
1510
1569
  normalized_args=normalized_args,
@@ -1629,12 +1688,27 @@ class AiBuilderTools(ToolBase):
1629
1688
  read_one=lambda item_app_key: self.app_get(profile=profile, app_key=item_app_key, app_keys=None),
1630
1689
  )
1631
1690
  normalized_args = {"app_key": app_key}
1632
- return _publicize_package_fields(_safe_tool_call(
1691
+ result = _publicize_package_fields(_safe_tool_call(
1633
1692
  lambda: self._facade.app_get(profile=profile, app_key=app_key),
1634
1693
  error_code="APP_GET_FAILED",
1635
1694
  normalized_args=normalized_args,
1636
1695
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
1637
1696
  ))
1697
+ if result.get("status") == "success":
1698
+ ws_id = self._selected_ws_id(profile)
1699
+ _attach_uri(result, result, ws_id=ws_id, resource_type="app", app_key=app_key)
1700
+ for view in result.get("views") or []:
1701
+ if isinstance(view, dict) and (view.get("view_key") or view.get("viewKey")):
1702
+ _attach_uri(
1703
+ view,
1704
+ result,
1705
+ ws_id=ws_id,
1706
+ resource_type="view",
1707
+ app_key=app_key,
1708
+ view_kind="custom",
1709
+ view_key=view.get("view_key") or view.get("viewKey"),
1710
+ )
1711
+ return result
1638
1712
 
1639
1713
  def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
1640
1714
  """执行应用相关逻辑。"""
@@ -1727,12 +1801,27 @@ class AiBuilderTools(ToolBase):
1727
1801
  read_one=lambda item_app_key: self.app_get_views(profile=profile, app_key=item_app_key, app_keys=None),
1728
1802
  )
1729
1803
  normalized_args = {"app_key": app_key}
1730
- return _safe_tool_call(
1804
+ result = _safe_tool_call(
1731
1805
  lambda: self._facade.app_get_views(profile=profile, app_key=app_key),
1732
1806
  error_code="APP_GET_VIEWS_FAILED",
1733
1807
  normalized_args=normalized_args,
1734
1808
  suggested_next_call={"tool_name": "app_get_views", "arguments": {"profile": profile, "app_key": app_key}},
1735
1809
  )
1810
+ if result.get("status") == "success":
1811
+ ws_id = self._selected_ws_id(profile)
1812
+ _attach_uri(result, result, ws_id=ws_id, resource_type="app", app_key=app_key)
1813
+ for view in result.get("views") or []:
1814
+ if isinstance(view, dict) and (view.get("view_key") or view.get("viewKey")):
1815
+ _attach_uri(
1816
+ view,
1817
+ result,
1818
+ ws_id=ws_id,
1819
+ resource_type="view",
1820
+ app_key=app_key,
1821
+ view_kind="custom",
1822
+ view_key=view.get("view_key") or view.get("viewKey"),
1823
+ )
1824
+ return result
1736
1825
 
1737
1826
  def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
1738
1827
  """执行应用相关逻辑。"""
@@ -1871,12 +1960,27 @@ class AiBuilderTools(ToolBase):
1871
1960
  """执行视图相关逻辑。"""
1872
1961
  resolved_view_key = _normalize_builder_view_key(str(view_key or viewgraph_key or "").strip())
1873
1962
  normalized_args = {"view_key": resolved_view_key}
1874
- return _safe_tool_call(
1963
+ result = _safe_tool_call(
1875
1964
  lambda: self._facade.view_get(profile=profile, view_key=resolved_view_key),
1876
1965
  error_code="VIEW_GET_FAILED",
1877
1966
  normalized_args=normalized_args,
1878
1967
  suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, **normalized_args}},
1879
1968
  )
1969
+ if result.get("status") == "success":
1970
+ config = result.get("config") if isinstance(result.get("config"), dict) else {}
1971
+ base_info = result.get("base_info") if isinstance(result.get("base_info"), dict) else {}
1972
+ app_key = str(config.get("appKey") or config.get("formKey") or base_info.get("appKey") or base_info.get("formKey") or "").strip()
1973
+ if app_key:
1974
+ _attach_uri(
1975
+ result,
1976
+ result,
1977
+ ws_id=self._selected_ws_id(profile),
1978
+ resource_type="view",
1979
+ app_key=app_key,
1980
+ view_kind="custom",
1981
+ view_key=resolved_view_key,
1982
+ )
1983
+ return result
1880
1984
 
1881
1985
  def chart_get(
1882
1986
  self,
@@ -2165,7 +2269,7 @@ class AiBuilderTools(ToolBase):
2165
2269
  )
2166
2270
  failure = normalized_apps_payload.get("failure")
2167
2271
  if isinstance(failure, dict):
2168
- return _attach_builder_apply_envelope("app_schema_apply", failure)
2272
+ return self._attach_apply_envelope(profile, "app_schema_apply", failure)
2169
2273
  package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
2170
2274
  apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
2171
2275
  input_warnings = list(normalized_apps_payload.get("warnings") or [])
@@ -2181,7 +2285,7 @@ class AiBuilderTools(ToolBase):
2181
2285
  )
2182
2286
  failure = form_result.get("failure")
2183
2287
  if isinstance(failure, dict):
2184
- return _attach_builder_apply_envelope("app_schema_apply", failure)
2288
+ return self._attach_apply_envelope(profile, "app_schema_apply", failure)
2185
2289
  normalized_apps.append(form_result.get("payload") or app_item)
2186
2290
  apps = normalized_apps
2187
2291
  result = self._app_schema_apply_multi(
@@ -2195,7 +2299,7 @@ class AiBuilderTools(ToolBase):
2195
2299
  result_warnings = list(result.get("warnings") or [])
2196
2300
  result_warnings.extend(deepcopy(input_warnings))
2197
2301
  result["warnings"] = result_warnings
2198
- return _attach_builder_apply_envelope("app_schema_apply", result)
2302
+ return self._attach_apply_envelope(profile, "app_schema_apply", result)
2199
2303
  inline_form_layout_sections: list[JSONObject] = []
2200
2304
  if form:
2201
2305
  form_payload: JSONObject = {"form": form}
@@ -2208,7 +2312,7 @@ class AiBuilderTools(ToolBase):
2208
2312
  )
2209
2313
  failure = form_result.get("failure")
2210
2314
  if isinstance(failure, dict):
2211
- return _attach_builder_apply_envelope("app_schema_apply", failure)
2315
+ return self._attach_apply_envelope(profile, "app_schema_apply", failure)
2212
2316
  normalized_payload = form_result.get("payload") if isinstance(form_result.get("payload"), dict) else {}
2213
2317
  add_fields = list(normalized_payload.get("add_fields") or [])
2214
2318
  inline_form_layout_sections = list(form_result.get("layout_sections") or [])
@@ -2246,7 +2350,7 @@ class AiBuilderTools(ToolBase):
2246
2350
  inline_form_layout_sections=inline_form_layout_sections,
2247
2351
  ),
2248
2352
  )
2249
- return _attach_builder_apply_envelope("app_schema_apply", result)
2353
+ return self._attach_apply_envelope(profile, "app_schema_apply", result)
2250
2354
 
2251
2355
  def _app_schema_apply_multi(
2252
2356
  self,
@@ -3525,7 +3629,7 @@ class AiBuilderTools(ToolBase):
3525
3629
  sections=sections,
3526
3630
  ),
3527
3631
  )
3528
- return _attach_builder_apply_envelope("app_layout_apply", result)
3632
+ return self._attach_apply_envelope(profile, "app_layout_apply", result)
3529
3633
 
3530
3634
  def _app_layout_apply_once(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
3531
3635
  """执行内部辅助逻辑。"""
@@ -3590,7 +3694,7 @@ class AiBuilderTools(ToolBase):
3590
3694
  "idempotency_key": idempotency_key,
3591
3695
  "schema_version": schema_version,
3592
3696
  }
3593
- return _attach_builder_apply_envelope("app_flow_apply", _safe_tool_call(
3697
+ return self._attach_apply_envelope(profile, "app_flow_apply", _safe_tool_call(
3594
3698
  lambda: self._facade.flow_patch_nodes(
3595
3699
  profile=profile,
3596
3700
  app_key=app_key,
@@ -3611,7 +3715,7 @@ class AiBuilderTools(ToolBase):
3611
3715
  "idempotency_key": idempotency_key,
3612
3716
  "schema_version": schema_version,
3613
3717
  }
3614
- return _attach_builder_apply_envelope("app_flow_apply", _safe_tool_call(
3718
+ return self._attach_apply_envelope(profile, "app_flow_apply", _safe_tool_call(
3615
3719
  lambda: self._facade.flow_apply(
3616
3720
  profile=profile,
3617
3721
  app_key=app_key,
@@ -3647,9 +3751,9 @@ class AiBuilderTools(ToolBase):
3647
3751
  transitions=effective_transitions,
3648
3752
  ),
3649
3753
  )
3650
- return _attach_builder_apply_envelope("app_flow_apply", result)
3754
+ return self._attach_apply_envelope(profile, "app_flow_apply", result)
3651
3755
  normalized_args = {"app_key": app_key, "publish": publish}
3652
- return _attach_builder_apply_envelope(
3756
+ return self._attach_apply_envelope(profile,
3653
3757
  "app_flow_apply",
3654
3758
  {
3655
3759
  "status": "failed",
@@ -3784,7 +3888,7 @@ class AiBuilderTools(ToolBase):
3784
3888
  patch_views=patch_views or [],
3785
3889
  ),
3786
3890
  )
3787
- return _attach_builder_apply_envelope("app_views_apply", result)
3891
+ return self._attach_apply_envelope(profile, "app_views_apply", result)
3788
3892
 
3789
3893
  def _app_views_apply_view_batch(
3790
3894
  self,
@@ -3795,7 +3899,7 @@ class AiBuilderTools(ToolBase):
3795
3899
  ) -> JSONObject:
3796
3900
  started_at = time.perf_counter()
3797
3901
  if not isinstance(views, list) or not views:
3798
- return _attach_builder_apply_envelope(
3902
+ return self._attach_apply_envelope(profile,
3799
3903
  "app_views_apply",
3800
3904
  _config_failure(
3801
3905
  tool_name="app_views_apply",
@@ -4026,7 +4130,7 @@ class AiBuilderTools(ToolBase):
4026
4130
  view_write_parallel_wall_ms=view_write_parallel_wall_ms,
4027
4131
  total_ms=_elapsed_ms(started_at),
4028
4132
  )
4029
- return _attach_builder_apply_envelope("app_views_apply", payload)
4133
+ return self._attach_apply_envelope(profile, "app_views_apply", payload)
4030
4134
 
4031
4135
  def _app_views_apply_once(
4032
4136
  self,
@@ -4149,7 +4253,7 @@ class AiBuilderTools(ToolBase):
4149
4253
  }
4150
4254
  )
4151
4255
  except ValidationError as exc:
4152
- return _attach_builder_apply_envelope("app_charts_apply", _visibility_validation_failure(
4256
+ return self._attach_apply_envelope(profile, "app_charts_apply", _visibility_validation_failure(
4153
4257
  str(exc),
4154
4258
  tool_name="app_charts_apply",
4155
4259
  exc=exc,
@@ -4165,7 +4269,7 @@ class AiBuilderTools(ToolBase):
4165
4269
  },
4166
4270
  ))
4167
4271
  normalized_args = request.model_dump(mode="json")
4168
- return _attach_builder_apply_envelope("app_charts_apply", _safe_tool_call(
4272
+ return self._attach_apply_envelope(profile, "app_charts_apply", _safe_tool_call(
4169
4273
  lambda: self._facade.chart_apply(profile=profile, request=request),
4170
4274
  error_code="CHART_APPLY_FAILED",
4171
4275
  normalized_args=normalized_args,
@@ -4201,7 +4305,7 @@ class AiBuilderTools(ToolBase):
4201
4305
  "publish": publish,
4202
4306
  "patch_sections": patch_sections,
4203
4307
  }
4204
- return _attach_builder_apply_envelope("portal_apply", _publicize_package_fields(_safe_tool_call(
4308
+ return self._attach_apply_envelope(profile, "portal_apply", _publicize_package_fields(_safe_tool_call(
4205
4309
  lambda: self._facade.portal_patch_sections(
4206
4310
  profile=profile,
4207
4311
  dash_key=dash_key,
@@ -4248,7 +4352,7 @@ class AiBuilderTools(ToolBase):
4248
4352
  try:
4249
4353
  request = PortalApplyRequest.model_validate(request_payload)
4250
4354
  except ValidationError as exc:
4251
- return _attach_builder_apply_envelope("portal_apply", _visibility_validation_failure(
4355
+ return self._attach_apply_envelope(profile, "portal_apply", _visibility_validation_failure(
4252
4356
  str(exc),
4253
4357
  tool_name="portal_apply",
4254
4358
  exc=exc,
@@ -4279,14 +4383,14 @@ class AiBuilderTools(ToolBase):
4279
4383
  creating=not bool(str(request.dash_key or "").strip()),
4280
4384
  )
4281
4385
  if icon_failure is not None:
4282
- return _attach_builder_apply_envelope("portal_apply", icon_failure)
4386
+ return self._attach_apply_envelope(profile, "portal_apply", icon_failure)
4283
4387
  result = _publicize_package_fields(_safe_tool_call(
4284
4388
  lambda: self._facade.portal_apply(profile=profile, request=request),
4285
4389
  error_code="PORTAL_APPLY_FAILED",
4286
4390
  normalized_args=normalized_args,
4287
4391
  suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
4288
4392
  ))
4289
- return _attach_builder_apply_envelope("portal_apply", result)
4393
+ return self._attach_apply_envelope(profile, "portal_apply", result)
4290
4394
 
4291
4395
  def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
4292
4396
  """执行门户删除逻辑。"""
@@ -4297,7 +4401,7 @@ class AiBuilderTools(ToolBase):
4297
4401
  normalized_args=normalized_args,
4298
4402
  suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, **normalized_args}},
4299
4403
  ))
4300
- return _attach_builder_apply_envelope("portal_delete", result)
4404
+ return self._attach_apply_envelope(profile, "portal_delete", result)
4301
4405
 
4302
4406
  def app_publish_verify(
4303
4407
  self,
@@ -4336,7 +4440,7 @@ class AiBuilderTools(ToolBase):
4336
4440
  expected_package_tag_id=expected_package_id,
4337
4441
  ),
4338
4442
  ))
4339
- return _attach_builder_apply_envelope("app_publish_verify", result)
4443
+ return self._attach_apply_envelope(profile, "app_publish_verify", result)
4340
4444
 
4341
4445
  def _retry_after_self_lock_release(self, *, profile: str, result: JSONObject, retry_call) -> JSONObject:
4342
4446
  """执行内部辅助逻辑。"""
@@ -5702,6 +5806,68 @@ def _publicize_package_fields(value):
5702
5806
  return public
5703
5807
 
5704
5808
 
5809
+ def _append_uri_warning(payload: JSONObject, warning: JSONObject | None) -> None:
5810
+ if not warning:
5811
+ return
5812
+ warnings = payload.setdefault("warnings", [])
5813
+ if isinstance(warnings, list) and not any(
5814
+ isinstance(item, dict) and item.get("code") == warning.get("code") for item in warnings
5815
+ ):
5816
+ warnings.append(warning)
5817
+
5818
+
5819
+ def _attach_uri(target: JSONObject, payload: JSONObject, *, ws_id: int, **identity: Any) -> None:
5820
+ uri, warning = try_build_resource_uri(ws_id=ws_id, **identity)
5821
+ if uri:
5822
+ target["uri"] = uri
5823
+ _append_uri_warning(payload, warning)
5824
+
5825
+
5826
+ def _attach_builder_resource_uris(payload: JSONObject, resources: list[JSONObject], *, ws_id: int) -> None:
5827
+ for resource in resources:
5828
+ resource_type = str(resource.get("resource_type") or "")
5829
+ ids = resource.get("ids") if isinstance(resource.get("ids"), dict) else {}
5830
+ if resource_type == "package":
5831
+ _attach_uri(
5832
+ resource,
5833
+ payload,
5834
+ ws_id=ws_id,
5835
+ resource_type="package",
5836
+ package_id=resource.get("id") or ids.get("package_id"),
5837
+ )
5838
+ elif resource_type == "app":
5839
+ _attach_uri(
5840
+ resource,
5841
+ payload,
5842
+ ws_id=ws_id,
5843
+ resource_type="app",
5844
+ app_key=resource.get("key") or ids.get("app_key"),
5845
+ )
5846
+ elif resource_type == "view":
5847
+ parent = resource.get("parent") if isinstance(resource.get("parent"), dict) else {}
5848
+ _attach_uri(
5849
+ resource,
5850
+ payload,
5851
+ ws_id=ws_id,
5852
+ resource_type="view",
5853
+ app_key=ids.get("app_key") or (parent.get("key") if parent.get("resource_type") == "app" else None),
5854
+ view_kind="custom",
5855
+ view_key=resource.get("key") or ids.get("view_key"),
5856
+ )
5857
+
5858
+
5859
+ def _attach_package_item_uris(items: Any, payload: JSONObject, *, ws_id: int) -> None:
5860
+ if not isinstance(items, list):
5861
+ return
5862
+ for item in items:
5863
+ if not isinstance(item, dict):
5864
+ continue
5865
+ if item.get("type") == "app" and item.get("app_key"):
5866
+ _attach_uri(item, payload, ws_id=ws_id, resource_type="app", app_key=item.get("app_key"))
5867
+ if item.get("type") == "group":
5868
+ _attach_package_item_uris(item.get("items"), payload, ws_id=ws_id)
5869
+
5870
+
5705
5871
  def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) -> JSONObject:
5706
5872
  public = deepcopy(contract)
5707
5873
  if tool_name not in BUILDER_APPLY_TOOL_NAMES:
@@ -5714,7 +5880,7 @@ def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) ->
5714
5880
  public["output_contract"] = {
5715
5881
  "schema_version": BUILDER_APPLY_SCHEMA_VERSION,
5716
5882
  "preferred_ui_fields": ["operation", "summary", "resources"],
5717
- "resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "icon_config", "error_code", "message"],
5883
+ "resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "uri", "icon_config", "error_code", "message"],
5718
5884
  "json_paths": _builder_apply_json_paths(),
5719
5885
  "legacy_fields_preserved": True,
5720
5886
  }
@@ -5766,6 +5932,7 @@ def _builder_apply_json_paths() -> JSONObject:
5766
5932
  "resource_keys": "$.resources[].key",
5767
5933
  "resource_ids": "$.resources[].id",
5768
5934
  "resource_statuses": "$.resources[].status",
5935
+ "resource_uris": "$.resources[].uri",
5769
5936
  }
5770
5937
 
5771
5938
 
@@ -5855,7 +6022,9 @@ def _builder_verification_truthy(value: object) -> bool:
5855
6022
 
5856
6023
  def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONObject]:
5857
6024
  resources: list[JSONObject]
5858
- if tool_name == "package_apply":
6025
+ if tool_name == "solution_install":
6026
+ resources = [deepcopy(item) for item in payload.get("resources") or [] if isinstance(item, dict)]
6027
+ elif tool_name == "package_apply":
5859
6028
  resources = _builder_package_resources(payload)
5860
6029
  elif tool_name == "app_schema_apply":
5861
6030
  resources = _builder_schema_resources(payload)