@josephyan/qingflow-cli 0.2.0-beta.64 → 0.2.0-beta.65

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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@0.2.0-beta.64
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.65
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.64 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.65 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.64",
3
+ "version": "0.2.0-beta.65",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b64"
7
+ version = "0.2.0b65"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -639,6 +639,7 @@ class CustomButtonPatch(StrictModel):
639
639
  background_color: str = Field(validation_alias=AliasChoices("background_color", "backgroundColor"))
640
640
  text_color: str = Field(validation_alias=AliasChoices("text_color", "textColor"))
641
641
  button_icon: str = Field(validation_alias=AliasChoices("button_icon", "buttonIcon"))
642
+ icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
642
643
  trigger_action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("trigger_action", "triggerAction"))
643
644
  trigger_link_url: str | None = Field(default=None, validation_alias=AliasChoices("trigger_link_url", "triggerLinkUrl"))
644
645
  trigger_add_data_config: CustomButtonAddDataConfigPatch | None = Field(
@@ -678,6 +679,7 @@ class ViewButtonBindingPatch(StrictModel):
678
679
  button_id: int = Field(validation_alias=AliasChoices("button_id", "buttonId", "id"))
679
680
  button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText"))
680
681
  button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
682
+ icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
681
683
  background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
682
684
  text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
683
685
  trigger_action: str | None = Field(default=None, validation_alias=AliasChoices("trigger_action", "triggerAction"))
@@ -994,6 +996,7 @@ FieldPatch.model_rebuild()
994
996
  class AppReadSummaryResponse(StrictModel):
995
997
  app_key: str
996
998
  title: str | None = None
999
+ app_icon: str | None = None
997
1000
  tag_ids: list[int] = Field(default_factory=list)
998
1001
  publish_status: int | None = None
999
1002
  field_count: int = 0
@@ -1051,6 +1054,8 @@ class SchemaPlanRequest(StrictModel):
1051
1054
  app_key: str = ""
1052
1055
  package_tag_id: int | None = None
1053
1056
  app_name: str = Field(default="", validation_alias=AliasChoices("app_name", "app_title", "title"))
1057
+ icon: str | None = None
1058
+ color: str | None = None
1054
1059
  create_if_missing: bool = False
1055
1060
  add_fields: list[FieldPatch] = Field(default_factory=list)
1056
1061
  update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
@@ -16,7 +16,7 @@ from ..list_type_labels import RECORD_LIST_TYPE_LABELS, SYSTEM_VIEW_ID_TO_LIST_T
16
16
  from ..solution.build_assembly_store import BuildAssemblyStore, default_artifacts, default_manifest
17
17
  from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
18
18
  from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
19
- from ..solution.compiler.icon_utils import encode_workspace_icon
19
+ from ..solution.compiler.icon_utils import encode_workspace_icon_with_defaults
20
20
  from ..solution.compiler.view_compiler import VIEW_TYPE_MAP
21
21
  from ..solution.executor import _build_viewgraph_questions, _compact_dict, extract_field_map
22
22
  from ..solution.spec_models import FieldType, FormLayoutRowSpec, FormLayoutSectionSpec, ViewSpec
@@ -1968,6 +1968,7 @@ class AiBuilderFacade:
1968
1968
  response = AppReadSummaryResponse(
1969
1969
  app_key=app_key,
1970
1970
  title=state["base"].get("formTitle"),
1971
+ app_icon=str(state["base"].get("appIcon") or "").strip() or None,
1971
1972
  tag_ids=_coerce_int_list(state["base"].get("tagIds")),
1972
1973
  publish_status=state["base"].get("appPublishStatus"),
1973
1974
  field_count=len(parsed["fields"]),
@@ -2674,6 +2675,7 @@ class AiBuilderFacade:
2674
2675
  "app": {
2675
2676
  "app_key": app_key,
2676
2677
  "title": base_result.get("formTitle"),
2678
+ "app_icon": str(base_result.get("appIcon") or "").strip() or None,
2677
2679
  "tag_ids": _coerce_int_list(base_result.get("tagIds")),
2678
2680
  "publish_status": base_result.get("appPublishStatus"),
2679
2681
  },
@@ -2706,6 +2708,8 @@ class AiBuilderFacade:
2706
2708
  app_key: str = "",
2707
2709
  package_tag_id: int | None = None,
2708
2710
  app_name: str = "",
2711
+ icon: str | None = None,
2712
+ color: str | None = None,
2709
2713
  create_if_missing: bool = False,
2710
2714
  publish: bool = True,
2711
2715
  add_fields: list[FieldPatch],
@@ -2716,6 +2720,8 @@ class AiBuilderFacade:
2716
2720
  "app_key": app_key,
2717
2721
  "package_tag_id": package_tag_id,
2718
2722
  "app_name": app_name,
2723
+ "icon": icon,
2724
+ "color": color,
2719
2725
  "create_if_missing": create_if_missing,
2720
2726
  "publish": publish,
2721
2727
  "add_fields": [patch.model_dump(mode="json") for patch in add_fields],
@@ -2766,6 +2772,8 @@ class AiBuilderFacade:
2766
2772
  profile=profile,
2767
2773
  app_name=app_name,
2768
2774
  package_tag_id=package_tag_id,
2775
+ icon=icon,
2776
+ color=color,
2769
2777
  )
2770
2778
  if resolved.get("status") == "failed":
2771
2779
  if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
@@ -2789,6 +2797,23 @@ class AiBuilderFacade:
2789
2797
  if permission_outcome.block is not None:
2790
2798
  return permission_outcome.block
2791
2799
  permission_outcomes.append(permission_outcome)
2800
+ visual_result = self._ensure_app_base_visuals(
2801
+ profile=profile,
2802
+ app_key=target.app_key,
2803
+ fallback_title=target.app_name,
2804
+ icon=icon,
2805
+ color=color,
2806
+ normalized_args=normalized_args,
2807
+ )
2808
+ if visual_result.get("status") == "failed":
2809
+ return finalize(visual_result)
2810
+ else:
2811
+ visual_result = {
2812
+ "status": "success",
2813
+ "updated": False,
2814
+ "app_icon": str(resolved.get("app_icon") or "").strip() or None,
2815
+ "request_id": None,
2816
+ }
2792
2817
  if bool(resolved.get("created")) and not requested_field_changes:
2793
2818
  return finalize({
2794
2819
  "status": "success",
@@ -2807,9 +2832,11 @@ class AiBuilderFacade:
2807
2832
  "fields_verified": True,
2808
2833
  "package_attached": None if package_tag_id is None else package_tag_id in target.tag_ids,
2809
2834
  "relation_field_limit_verified": True,
2835
+ "app_visuals_verified": True,
2810
2836
  "publish_skipped": True,
2811
2837
  },
2812
2838
  "app_key": target.app_key,
2839
+ "app_icon": str(resolved.get("app_icon") or visual_result.get("app_icon") or "").strip() or None,
2813
2840
  "created": True,
2814
2841
  "field_diff": {"added": [], "updated": [], "removed": []},
2815
2842
  "verified": True,
@@ -2931,17 +2958,22 @@ class AiBuilderFacade:
2931
2958
  "status": "success",
2932
2959
  "error_code": None,
2933
2960
  "recoverable": False,
2934
- "message": "schema already matches requested state",
2961
+ "message": "updated app visuals; schema already matches requested state" if bool(visual_result.get("updated")) else "schema already matches requested state",
2935
2962
  "normalized_args": normalized_args,
2936
2963
  "missing_fields": [],
2937
2964
  "allowed_values": {"field_types": [item.value for item in PublicFieldType]},
2938
2965
  "details": {},
2939
2966
  "request_id": None,
2940
2967
  "suggested_next_call": None if package_attached is not False else {"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": package_tag_id, "app_key": target.app_key}},
2941
- "noop": True,
2968
+ "noop": not bool(visual_result.get("updated")),
2942
2969
  "warnings": relation_warnings,
2943
- "verification": {"fields_verified": True, "relation_field_limit_verified": relation_limit_verified},
2970
+ "verification": {
2971
+ "fields_verified": True,
2972
+ "relation_field_limit_verified": relation_limit_verified,
2973
+ "app_visuals_verified": True,
2974
+ },
2944
2975
  "app_key": target.app_key,
2976
+ "app_icon": str(visual_result.get("app_icon") or "").strip() or None,
2945
2977
  "created": False,
2946
2978
  "field_diff": {"added": [], "updated": [], "removed": []},
2947
2979
  "verified": True,
@@ -3013,9 +3045,11 @@ class AiBuilderFacade:
3013
3045
  "verification": {
3014
3046
  "fields_verified": False,
3015
3047
  "package_attached": None,
3048
+ "app_visuals_verified": True,
3016
3049
  "relation_field_limit_verified": relation_limit_verified,
3017
3050
  },
3018
3051
  "app_key": target.app_key,
3052
+ "app_icon": str(visual_result.get("app_icon") or resolved.get("app_icon") or "").strip() or None,
3019
3053
  "created": bool(resolved.get("created")),
3020
3054
  "field_diff": {
3021
3055
  "added": added,
@@ -5482,9 +5516,17 @@ class AiBuilderFacade:
5482
5516
  profile: str,
5483
5517
  app_name: str,
5484
5518
  package_tag_id: int | None,
5519
+ icon: str | None,
5520
+ color: str | None,
5485
5521
  ) -> JSONObject:
5486
5522
  payload: JSONObject = {
5487
5523
  "appName": app_name or "未命名应用",
5524
+ "appIcon": encode_workspace_icon_with_defaults(
5525
+ icon=icon,
5526
+ color=color,
5527
+ title=app_name or "未命名应用",
5528
+ fallback_icon_name="template",
5529
+ ),
5488
5530
  "auth": default_member_auth(),
5489
5531
  "tagIds": [package_tag_id] if package_tag_id and package_tag_id > 0 else [],
5490
5532
  }
@@ -5530,6 +5572,7 @@ class AiBuilderFacade:
5530
5572
  "suggested_next_call": None,
5531
5573
  "app_key": new_app_key,
5532
5574
  "app_name": app_name or "未命名应用",
5575
+ "app_icon": payload.get("appIcon"),
5533
5576
  "tag_ids": [package_tag_id] if package_tag_id and package_tag_id > 0 else [],
5534
5577
  "created": True,
5535
5578
  }
@@ -5541,10 +5584,103 @@ class AiBuilderFacade:
5541
5584
  "suggested_next_call": None,
5542
5585
  "app_key": new_app_key,
5543
5586
  "app_name": base.get("formTitle") or app_name or "未命名应用",
5587
+ "app_icon": str(base.get("appIcon") or payload.get("appIcon") or "").strip() or None,
5544
5588
  "tag_ids": _coerce_int_list(base.get("tagIds")),
5545
5589
  "created": True,
5546
5590
  }
5547
5591
 
5592
+ def _build_app_base_update_payload(
5593
+ self,
5594
+ *,
5595
+ raw_base: dict[str, Any],
5596
+ form_title: str,
5597
+ app_icon: str,
5598
+ ) -> JSONObject | None:
5599
+ auth = deepcopy(raw_base.get("auth"))
5600
+ if not isinstance(auth, dict):
5601
+ return None
5602
+ payload: JSONObject = {
5603
+ "formTitle": form_title,
5604
+ "auth": auth,
5605
+ "appIcon": app_icon,
5606
+ }
5607
+ tag_ids = _coerce_int_list(raw_base.get("tagIds"))
5608
+ if tag_ids:
5609
+ payload["tagIds"] = tag_ids
5610
+ return payload
5611
+
5612
+ def _ensure_app_base_visuals(
5613
+ self,
5614
+ *,
5615
+ profile: str,
5616
+ app_key: str,
5617
+ fallback_title: str,
5618
+ icon: str | None,
5619
+ color: str | None,
5620
+ normalized_args: dict[str, Any],
5621
+ ) -> JSONObject:
5622
+ if not icon and not color:
5623
+ return {
5624
+ "status": "success",
5625
+ "updated": False,
5626
+ "app_icon": None,
5627
+ "request_id": None,
5628
+ }
5629
+ try:
5630
+ base_result = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
5631
+ except (QingflowApiError, RuntimeError) as error:
5632
+ api_error = _coerce_api_error(error)
5633
+ return _failed_from_api_error(
5634
+ "APP_VISUAL_READ_FAILED",
5635
+ api_error,
5636
+ normalized_args=normalized_args,
5637
+ details={"app_key": app_key},
5638
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
5639
+ )
5640
+ raw_base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
5641
+ effective_title = str(raw_base.get("formTitle") or fallback_title or "未命名应用").strip() or "未命名应用"
5642
+ existing_icon = str(raw_base.get("appIcon") or "").strip() or None
5643
+ desired_icon = encode_workspace_icon_with_defaults(
5644
+ icon=icon,
5645
+ color=color,
5646
+ title=effective_title,
5647
+ fallback_icon_name="template",
5648
+ existing_icon=existing_icon,
5649
+ )
5650
+ if desired_icon == existing_icon:
5651
+ return {
5652
+ "status": "success",
5653
+ "updated": False,
5654
+ "app_icon": desired_icon,
5655
+ "request_id": None,
5656
+ }
5657
+ payload = self._build_app_base_update_payload(raw_base=raw_base, form_title=effective_title, app_icon=desired_icon)
5658
+ if payload is None:
5659
+ return _failed(
5660
+ "APP_VISUAL_UPDATE_UNSUPPORTED",
5661
+ "app base info did not include editable auth payload required for icon update",
5662
+ normalized_args=normalized_args,
5663
+ details={"app_key": app_key},
5664
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
5665
+ )
5666
+ try:
5667
+ update_result = self.apps.app_update_base(profile=profile, app_key=app_key, payload=payload)
5668
+ except (QingflowApiError, RuntimeError) as error:
5669
+ api_error = _coerce_api_error(error)
5670
+ return _failed_from_api_error(
5671
+ "APP_VISUAL_UPDATE_FAILED",
5672
+ api_error,
5673
+ normalized_args=normalized_args,
5674
+ details={"app_key": app_key, "app_icon": desired_icon},
5675
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
5676
+ )
5677
+ return {
5678
+ "status": "success",
5679
+ "updated": True,
5680
+ "app_icon": desired_icon,
5681
+ "request_id": update_result.get("request_id") if isinstance(update_result, dict) else None,
5682
+ }
5683
+
5548
5684
  def _current_request_route(self, profile: str) -> JSONObject:
5549
5685
  session_profile = self.apps.sessions.get_profile(profile)
5550
5686
  backend_session = self.apps.sessions.get_backend_session(profile)
@@ -5727,6 +5863,8 @@ def _serialize_custom_button_payload(payload: CustomButtonPatch) -> dict[str, An
5727
5863
  "buttonIcon": data["button_icon"],
5728
5864
  "triggerAction": data["trigger_action"],
5729
5865
  }
5866
+ if str(data.get("icon_color") or "").strip():
5867
+ serialized["iconColor"] = data["icon_color"]
5730
5868
  if str(data.get("trigger_link_url") or "").strip():
5731
5869
  serialized["triggerLinkUrl"] = data["trigger_link_url"]
5732
5870
  trigger_add_data_config = data.get("trigger_add_data_config")
@@ -5816,6 +5954,7 @@ def _normalize_custom_button_summary(item: dict[str, Any]) -> dict[str, Any]:
5816
5954
  "button_id": _coerce_positive_int(item.get("button_id") or item.get("buttonId") or item.get("id")),
5817
5955
  "button_text": str(item.get("button_text") or item.get("buttonText") or "").strip() or None,
5818
5956
  "button_icon": str(item.get("button_icon") or item.get("buttonIcon") or "").strip() or None,
5957
+ "icon_color": str(item.get("icon_color") or item.get("iconColor") or "").strip() or None,
5819
5958
  "background_color": str(item.get("background_color") or item.get("backgroundColor") or "").strip() or None,
5820
5959
  "text_color": str(item.get("text_color") or item.get("textColor") or "").strip() or None,
5821
5960
  "used_in_chart_count": _coerce_nonnegative_int(item.get("used_in_chart_count") or item.get("userInChartCount")),
@@ -6572,11 +6711,12 @@ def _build_public_portal_base_payload(
6572
6711
  effective_name = str(dash_name or data.get("dashName") or "").strip() or "未命名门户"
6573
6712
  effective_hide_copyright = hide_copyright if hide_copyright is not None else bool(data.get("hideCopyright", False))
6574
6713
  if icon or color or not data.get("dashIcon"):
6575
- data["dashIcon"] = encode_workspace_icon(
6714
+ data["dashIcon"] = encode_workspace_icon_with_defaults(
6576
6715
  icon=icon,
6577
6716
  color=color,
6578
6717
  title=effective_name,
6579
6718
  fallback_icon_name="view-grid",
6719
+ existing_icon=str(data.get("dashIcon") or "").strip() or None,
6580
6720
  )
6581
6721
  data["dashName"] = effective_name
6582
6722
  data["auth"] = deepcopy(auth if auth is not None else data.get("auth") or default_member_auth())
@@ -8593,6 +8733,7 @@ def _normalize_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
8593
8733
  for public_key, source_key in (
8594
8734
  ("default_button_text", "defaultButtonText"),
8595
8735
  ("button_icon", "buttonIcon"),
8736
+ ("icon_color", "iconColor"),
8596
8737
  ("background_color", "backgroundColor"),
8597
8738
  ("text_color", "textColor"),
8598
8739
  ("trigger_link_url", "triggerLinkUrl"),
@@ -8622,17 +8763,21 @@ def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
8622
8763
  normalized_entries: list[dict[str, Any]] = []
8623
8764
  for entry in entries:
8624
8765
  normalized = _normalize_view_button_entry(entry)
8766
+ is_custom = normalized.get("button_type") == "CUSTOM"
8625
8767
  normalized_entries.append(
8626
8768
  {
8627
8769
  "button_type": normalized.get("button_type"),
8628
8770
  "config_type": normalized.get("config_type"),
8629
- "button_id": normalized.get("button_id") if normalized.get("button_type") == "CUSTOM" else None,
8771
+ "button_id": normalized.get("button_id") if is_custom else None,
8630
8772
  "button_text": normalized.get("button_text"),
8631
8773
  "button_icon": normalized.get("button_icon"),
8774
+ "icon_color": normalized.get("icon_color"),
8632
8775
  "background_color": normalized.get("background_color"),
8633
8776
  "text_color": normalized.get("text_color"),
8634
8777
  "trigger_action": normalized.get("trigger_action"),
8635
- "trigger_link_url": normalized.get("trigger_link_url"),
8778
+ # Custom button trigger details are verified by dedicated button CRUD readback.
8779
+ # View binding verification should focus on binding/display semantics and tolerate resource-owned URLs.
8780
+ "trigger_link_url": None if is_custom else normalized.get("trigger_link_url"),
8636
8781
  "being_main": bool(normalized.get("being_main", False)),
8637
8782
  "print_tpls": _normalize_print_tpls_for_compare(normalized.get("print_tpls")),
8638
8783
  "button_formula": str(normalized.get("button_formula") or ""),
@@ -8701,13 +8846,13 @@ def _normalize_expected_view_buttons_for_compare(
8701
8846
  for key in (
8702
8847
  "button_text",
8703
8848
  "button_icon",
8849
+ "icon_color",
8704
8850
  "background_color",
8705
8851
  "text_color",
8706
8852
  "trigger_action",
8707
- "trigger_link_url",
8708
8853
  ):
8709
8854
  value = detail.get(key)
8710
- if value not in {None, ""}:
8855
+ if enriched.get(key) in {None, ""} and value not in {None, ""}:
8711
8856
  enriched[key] = deepcopy(value)
8712
8857
  enriched_entries.append(enriched)
8713
8858
  return enriched_entries
@@ -8881,9 +9026,11 @@ def _serialize_view_button_binding(
8881
9026
  "buttonFormulaType": binding.button_formula_type,
8882
9027
  "printTpls": _serialize_print_tpl_ids(binding.print_tpls),
8883
9028
  }
8884
- if binding.button_type == PublicViewButtonType.system:
9029
+ if binding.button_type in {PublicViewButtonType.system, PublicViewButtonType.custom}:
8885
9030
  dto["buttonText"] = binding.button_text
8886
9031
  dto["buttonIcon"] = binding.button_icon
9032
+ if str(binding.icon_color or "").strip():
9033
+ dto["iconColor"] = binding.icon_color
8887
9034
  dto["backgroundColor"] = binding.background_color
8888
9035
  dto["textColor"] = binding.text_color
8889
9036
  dto["triggerAction"] = binding.trigger_action
@@ -93,6 +93,8 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
93
93
  schema_apply_apply.add_argument("--package-tag-id", type=int)
94
94
  schema_apply_apply.add_argument("--app-name", default="")
95
95
  schema_apply_apply.add_argument("--app-title", default="")
96
+ schema_apply_apply.add_argument("--icon")
97
+ schema_apply_apply.add_argument("--color")
96
98
  schema_apply_apply.add_argument("--create-if-missing", action="store_true")
97
99
  schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
98
100
  schema_apply_apply.add_argument("--add-fields-file")
@@ -198,6 +200,8 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
198
200
  package_tag_id=args.package_tag_id,
199
201
  app_name=args.app_name,
200
202
  app_title=args.app_title,
203
+ icon=args.icon,
204
+ color=args.color,
201
205
  create_if_missing=bool(args.create_if_missing),
202
206
  publish=bool(args.publish),
203
207
  add_fields=load_list_arg(args.add_fields_file, option_name="--add-fields-file"),
@@ -280,6 +280,8 @@ def build_builder_server() -> FastMCP:
280
280
  package_tag_id: int | None = None,
281
281
  app_name: str = "",
282
282
  app_title: str = "",
283
+ icon: str = "",
284
+ color: str = "",
283
285
  create_if_missing: bool = False,
284
286
  publish: bool = True,
285
287
  add_fields: list[dict] | None = None,
@@ -292,6 +294,8 @@ def build_builder_server() -> FastMCP:
292
294
  package_tag_id=package_tag_id,
293
295
  app_name=app_name,
294
296
  app_title=app_title,
297
+ icon=icon,
298
+ color=color,
295
299
  create_if_missing=create_if_missing,
296
300
  publish=publish,
297
301
  add_fields=add_fields or [],
@@ -4,6 +4,18 @@ import json
4
4
 
5
5
 
6
6
  DEFAULT_ICON_COLOR = "qing-orange"
7
+ DEFAULT_ICON_STYLE_POOL: tuple[tuple[str, str], ...] = (
8
+ ("briefcase", "qing-orange"),
9
+ ("calendar", "emerald"),
10
+ ("folder", "azure"),
11
+ ("shield-check", "indigo"),
12
+ ("user-group", "qing-purple"),
13
+ ("chart-square-bar", "blue"),
14
+ ("clock", "pink"),
15
+ ("document-text", "green"),
16
+ ("home", "orange"),
17
+ ("globe", "red"),
18
+ )
7
19
  SUPPORTED_EX_ICONS = {
8
20
  "ex-alert-outlined",
9
21
  "ex-clock-outlined",
@@ -78,6 +90,65 @@ def encode_workspace_icon(
78
90
  )
79
91
 
80
92
 
93
+ def choose_default_workspace_icon_style(*, title: str) -> tuple[str, str]:
94
+ seed_text = (title or "未命名").strip() or "未命名"
95
+ digest = 0
96
+ for index, char in enumerate(seed_text):
97
+ digest = (digest * 131 + ord(char) + index) % 1_000_003
98
+ return DEFAULT_ICON_STYLE_POOL[digest % len(DEFAULT_ICON_STYLE_POOL)]
99
+
100
+
101
+ def parse_workspace_icon(value: str | None) -> tuple[str | None, str | None, str | None]:
102
+ if not value:
103
+ return None, None, None
104
+ stripped = str(value).strip()
105
+ if not stripped:
106
+ return None, None, None
107
+ if _looks_like_icon_json(stripped):
108
+ try:
109
+ payload = json.loads(stripped)
110
+ except Exception:
111
+ return normalize_workspace_icon_name(stripped), None, None
112
+ icon_name = normalize_workspace_icon_name(payload.get("iconName"))
113
+ icon_color = str(payload.get("iconColor") or "").strip() or None
114
+ icon_text = str(payload.get("iconText") or "").strip() or None
115
+ return icon_name, icon_color, icon_text
116
+ return normalize_workspace_icon_name(stripped), None, None
117
+
118
+
119
+ def encode_workspace_icon_with_defaults(
120
+ *,
121
+ icon: str | None,
122
+ color: str | None,
123
+ title: str,
124
+ fallback_icon_name: str | None = None,
125
+ existing_icon: str | None = None,
126
+ ) -> str:
127
+ if icon and _looks_like_icon_json(icon):
128
+ return icon
129
+ if not icon and not color and existing_icon:
130
+ existing_payload = str(existing_icon).strip()
131
+ if existing_payload:
132
+ return existing_payload
133
+ existing_icon_name, existing_icon_color, _ = parse_workspace_icon(existing_icon)
134
+ default_icon, default_color = choose_default_workspace_icon_style(title=title)
135
+ resolved_icon = icon or existing_icon_name
136
+ resolved_color = color or existing_icon_color
137
+ if not resolved_icon:
138
+ if icon is None and color is None:
139
+ resolved_icon = default_icon
140
+ else:
141
+ resolved_icon = fallback_icon_name or default_icon
142
+ if not resolved_color:
143
+ resolved_color = default_color
144
+ return encode_workspace_icon(
145
+ icon=resolved_icon,
146
+ color=resolved_color,
147
+ title=title,
148
+ fallback_icon_name=fallback_icon_name,
149
+ )
150
+
151
+
81
152
  def normalize_workspace_icon_name(icon: str | None) -> str | None:
82
153
  if not icon:
83
154
  return None
@@ -228,6 +228,8 @@ class AiBuilderTools(ToolBase):
228
228
  package_tag_id: int | None = None,
229
229
  app_name: str = "",
230
230
  app_title: str = "",
231
+ icon: str = "",
232
+ color: str = "",
231
233
  create_if_missing: bool = False,
232
234
  publish: bool = True,
233
235
  add_fields: list[JSONObject] | None = None,
@@ -240,6 +242,8 @@ class AiBuilderTools(ToolBase):
240
242
  package_tag_id=package_tag_id,
241
243
  app_name=app_name,
242
244
  app_title=app_title,
245
+ icon=icon,
246
+ color=color,
243
247
  create_if_missing=create_if_missing,
244
248
  publish=publish,
245
249
  add_fields=add_fields or [],
@@ -713,6 +717,8 @@ class AiBuilderTools(ToolBase):
713
717
  app_key: str = "",
714
718
  package_tag_id: int | None = None,
715
719
  app_name: str = "",
720
+ icon: str = "",
721
+ color: str = "",
716
722
  create_if_missing: bool = False,
717
723
  add_fields: list[JSONObject],
718
724
  update_fields: list[JSONObject],
@@ -724,6 +730,8 @@ class AiBuilderTools(ToolBase):
724
730
  "app_key": app_key,
725
731
  "package_tag_id": package_tag_id,
726
732
  "app_name": app_name,
733
+ "icon": icon,
734
+ "color": color,
727
735
  "create_if_missing": create_if_missing,
728
736
  "add_fields": add_fields,
729
737
  "update_fields": update_fields,
@@ -742,6 +750,8 @@ class AiBuilderTools(ToolBase):
742
750
  "app_key": app_key,
743
751
  "package_tag_id": package_tag_id,
744
752
  "app_name": app_name,
753
+ "icon": icon,
754
+ "color": color,
745
755
  "create_if_missing": create_if_missing,
746
756
  "add_fields": [{"name": "字段名称", "type": "text"}],
747
757
  "update_fields": [],
@@ -889,6 +899,8 @@ class AiBuilderTools(ToolBase):
889
899
  package_tag_id: int | None = None,
890
900
  app_name: str = "",
891
901
  app_title: str = "",
902
+ icon: str = "",
903
+ color: str = "",
892
904
  create_if_missing: bool = False,
893
905
  publish: bool = True,
894
906
  add_fields: list[JSONObject],
@@ -901,6 +913,8 @@ class AiBuilderTools(ToolBase):
901
913
  package_tag_id=package_tag_id,
902
914
  app_name=app_name,
903
915
  app_title=app_title,
916
+ icon=icon,
917
+ color=color,
904
918
  create_if_missing=create_if_missing,
905
919
  publish=publish,
906
920
  add_fields=add_fields,
@@ -916,6 +930,8 @@ class AiBuilderTools(ToolBase):
916
930
  package_tag_id=package_tag_id,
917
931
  app_name=app_name,
918
932
  app_title=app_title,
933
+ icon=icon,
934
+ color=color,
919
935
  create_if_missing=create_if_missing,
920
936
  publish=publish,
921
937
  add_fields=add_fields,
@@ -932,6 +948,8 @@ class AiBuilderTools(ToolBase):
932
948
  package_tag_id: int | None = None,
933
949
  app_name: str = "",
934
950
  app_title: str = "",
951
+ icon: str = "",
952
+ color: str = "",
935
953
  create_if_missing: bool = False,
936
954
  publish: bool = True,
937
955
  add_fields: list[JSONObject],
@@ -945,6 +963,8 @@ class AiBuilderTools(ToolBase):
945
963
  app_key=app_key,
946
964
  package_tag_id=package_tag_id,
947
965
  app_name=effective_app_name,
966
+ icon=icon,
967
+ color=color,
948
968
  create_if_missing=create_if_missing,
949
969
  add_fields=add_fields,
950
970
  update_fields=update_fields,
@@ -976,6 +996,8 @@ class AiBuilderTools(ToolBase):
976
996
  "app_key": str(plan_args.get("app_key") or app_key),
977
997
  "package_tag_id": plan_args.get("package_tag_id", package_tag_id),
978
998
  "app_name": str(plan_args.get("app_name") or effective_app_name),
999
+ "icon": str(plan_args.get("icon") or icon),
1000
+ "color": str(plan_args.get("color") or color),
979
1001
  "create_if_missing": bool(plan_args.get("create_if_missing", create_if_missing)),
980
1002
  "publish": publish,
981
1003
  "add_fields": plan_args.get("add_fields") or [{"name": "字段名称", "type": "text", "required": False}],
@@ -988,6 +1010,8 @@ class AiBuilderTools(ToolBase):
988
1010
  "app_key": str(plan_args.get("app_key") or app_key),
989
1011
  "package_tag_id": plan_args.get("package_tag_id", package_tag_id),
990
1012
  "app_name": str(plan_args.get("app_name") or effective_app_name),
1013
+ "icon": str(plan_args.get("icon") or icon or ""),
1014
+ "color": str(plan_args.get("color") or color or ""),
991
1015
  "create_if_missing": bool(plan_args.get("create_if_missing", create_if_missing)),
992
1016
  "publish": publish,
993
1017
  "add_fields": [patch.model_dump(mode="json") for patch in parsed_add],
@@ -1000,6 +1024,8 @@ class AiBuilderTools(ToolBase):
1000
1024
  app_key=str(plan_args.get("app_key") or app_key),
1001
1025
  package_tag_id=plan_args.get("package_tag_id", package_tag_id),
1002
1026
  app_name=str(plan_args.get("app_name") or effective_app_name),
1027
+ icon=str(plan_args.get("icon") or icon or ""),
1028
+ color=str(plan_args.get("color") or color or ""),
1003
1029
  create_if_missing=bool(plan_args.get("create_if_missing", create_if_missing)),
1004
1030
  publish=publish,
1005
1031
  add_fields=parsed_add,
@@ -1744,6 +1770,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1744
1770
  "payload.backgroundColor": "payload.background_color",
1745
1771
  "payload.textColor": "payload.text_color",
1746
1772
  "payload.buttonIcon": "payload.button_icon",
1773
+ "payload.iconColor": "payload.icon_color",
1747
1774
  "payload.triggerAction": "payload.trigger_action",
1748
1775
  "payload.triggerLinkUrl": "payload.trigger_link_url",
1749
1776
  "payload.triggerAddDataConfig": "payload.trigger_add_data_config",
@@ -1762,6 +1789,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1762
1789
  "background_color": "#FFFFFF",
1763
1790
  "text_color": "#494F57",
1764
1791
  "button_icon": "ex-add-outlined",
1792
+ "icon_color": "#494F57",
1765
1793
  "trigger_action": "link",
1766
1794
  "trigger_link_url": "https://example.com",
1767
1795
  },
@@ -1775,6 +1803,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1775
1803
  "payload.backgroundColor": "payload.background_color",
1776
1804
  "payload.textColor": "payload.text_color",
1777
1805
  "payload.buttonIcon": "payload.button_icon",
1806
+ "payload.iconColor": "payload.icon_color",
1778
1807
  "payload.triggerAction": "payload.trigger_action",
1779
1808
  "payload.triggerLinkUrl": "payload.trigger_link_url",
1780
1809
  "payload.triggerAddDataConfig": "payload.trigger_add_data_config",
@@ -1794,6 +1823,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1794
1823
  "background_color": "#FFFFFF",
1795
1824
  "text_color": "#494F57",
1796
1825
  "button_icon": "ex-link-outlined",
1826
+ "icon_color": "#494F57",
1797
1827
  "trigger_action": "link",
1798
1828
  "trigger_link_url": "https://example.com/detail",
1799
1829
  },
@@ -1810,7 +1840,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1810
1840
  },
1811
1841
  },
1812
1842
  "app_schema_plan": {
1813
- "allowed_keys": ["app_key", "package_tag_id", "app_name", "create_if_missing", "add_fields", "update_fields", "remove_fields"],
1843
+ "allowed_keys": ["app_key", "package_tag_id", "app_name", "icon", "color", "create_if_missing", "add_fields", "update_fields", "remove_fields"],
1814
1844
  "aliases": {
1815
1845
  "app_title": "app_name",
1816
1846
  "title": "app_name",
@@ -1835,6 +1865,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1835
1865
  "profile": "default",
1836
1866
  "app_name": "研发项目管理",
1837
1867
  "package_tag_id": 1001,
1868
+ "icon": "template",
1869
+ "color": "emerald",
1838
1870
  "create_if_missing": True,
1839
1871
  "add_fields": [{"name": "项目名称", "type": "text"}],
1840
1872
  "update_fields": [],
@@ -1858,7 +1890,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1858
1890
  },
1859
1891
  },
1860
1892
  "app_schema_apply": {
1861
- "allowed_keys": ["app_key", "package_tag_id", "app_name", "create_if_missing", "publish", "add_fields", "update_fields", "remove_fields"],
1893
+ "allowed_keys": ["app_key", "package_tag_id", "app_name", "icon", "color", "create_if_missing", "publish", "add_fields", "update_fields", "remove_fields"],
1862
1894
  "aliases": {
1863
1895
  "app_title": "app_name",
1864
1896
  "title": "app_name",
@@ -1888,6 +1920,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1888
1920
  "profile": "default",
1889
1921
  "app_name": "研发项目管理",
1890
1922
  "package_tag_id": 1001,
1923
+ "icon": "template",
1924
+ "color": "emerald",
1891
1925
  "create_if_missing": True,
1892
1926
  "publish": True,
1893
1927
  "add_fields": [{"name": "项目名称", "type": "text"}],
@@ -147,6 +147,7 @@ class CustomButtonTools(ToolBase):
147
147
  "button_id": item.get("buttonId"),
148
148
  "button_text": item.get("buttonText"),
149
149
  "button_icon": item.get("buttonIcon"),
150
+ "icon_color": item.get("iconColor"),
150
151
  "background_color": item.get("backgroundColor"),
151
152
  "text_color": item.get("textColor"),
152
153
  "creator_user_info": {
@@ -165,6 +166,7 @@ class CustomButtonTools(ToolBase):
165
166
  "button_id": item.get("buttonId"),
166
167
  "button_text": item.get("buttonText"),
167
168
  "button_icon": item.get("buttonIcon"),
169
+ "icon_color": item.get("iconColor"),
168
170
  "background_color": item.get("backgroundColor"),
169
171
  "text_color": item.get("textColor"),
170
172
  "trigger_action": item.get("triggerAction"),