@josephyan/qingflow-cli 0.2.0-beta.55

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 (79) hide show
  1. package/README.md +30 -0
  2. package/docs/local-agent-install.md +235 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +204 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +5 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +547 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +985 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +8243 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +184 -0
  21. package/src/qingflow_mcp/cli/commands/common.py +47 -0
  22. package/src/qingflow_mcp/cli/commands/imports.py +86 -0
  23. package/src/qingflow_mcp/cli/commands/record.py +202 -0
  24. package/src/qingflow_mcp/cli/commands/task.py +87 -0
  25. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  26. package/src/qingflow_mcp/cli/context.py +48 -0
  27. package/src/qingflow_mcp/cli/formatters.py +269 -0
  28. package/src/qingflow_mcp/cli/json_io.py +50 -0
  29. package/src/qingflow_mcp/cli/main.py +147 -0
  30. package/src/qingflow_mcp/config.py +221 -0
  31. package/src/qingflow_mcp/errors.py +66 -0
  32. package/src/qingflow_mcp/import_store.py +121 -0
  33. package/src/qingflow_mcp/json_types.py +18 -0
  34. package/src/qingflow_mcp/list_type_labels.py +76 -0
  35. package/src/qingflow_mcp/server.py +211 -0
  36. package/src/qingflow_mcp/server_app_builder.py +387 -0
  37. package/src/qingflow_mcp/server_app_user.py +317 -0
  38. package/src/qingflow_mcp/session_store.py +289 -0
  39. package/src/qingflow_mcp/solution/__init__.py +6 -0
  40. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  41. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  42. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  43. package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
  44. package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
  45. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  46. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  47. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  48. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  49. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  50. package/src/qingflow_mcp/solution/design_session.py +222 -0
  51. package/src/qingflow_mcp/solution/design_store.py +100 -0
  52. package/src/qingflow_mcp/solution/executor.py +2339 -0
  53. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  54. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  55. package/src/qingflow_mcp/solution/run_store.py +244 -0
  56. package/src/qingflow_mcp/solution/spec_models.py +853 -0
  57. package/src/qingflow_mcp/tools/__init__.py +1 -0
  58. package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
  59. package/src/qingflow_mcp/tools/app_tools.py +850 -0
  60. package/src/qingflow_mcp/tools/approval_tools.py +833 -0
  61. package/src/qingflow_mcp/tools/auth_tools.py +697 -0
  62. package/src/qingflow_mcp/tools/base.py +81 -0
  63. package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
  64. package/src/qingflow_mcp/tools/directory_tools.py +648 -0
  65. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  66. package/src/qingflow_mcp/tools/file_tools.py +385 -0
  67. package/src/qingflow_mcp/tools/import_tools.py +1971 -0
  68. package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
  69. package/src/qingflow_mcp/tools/package_tools.py +240 -0
  70. package/src/qingflow_mcp/tools/portal_tools.py +131 -0
  71. package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
  72. package/src/qingflow_mcp/tools/record_tools.py +12739 -0
  73. package/src/qingflow_mcp/tools/role_tools.py +94 -0
  74. package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
  75. package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
  76. package/src/qingflow_mcp/tools/task_tools.py +843 -0
  77. package/src/qingflow_mcp/tools/view_tools.py +280 -0
  78. package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
  79. package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
@@ -0,0 +1,466 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from typing import Any
5
+
6
+ from ..spec_models import EntitySpec, FieldType, FormLayoutRowSpec, FormLayoutSectionSpec
7
+ from .icon_utils import encode_workspace_icon
8
+
9
+
10
+ QUESTION_TYPE_MAP = {
11
+ FieldType.text: 2,
12
+ FieldType.long_text: 3,
13
+ FieldType.number: 8,
14
+ FieldType.amount: 8,
15
+ FieldType.date: 4,
16
+ FieldType.datetime: 4,
17
+ FieldType.member: 5,
18
+ FieldType.department: 22,
19
+ FieldType.single_select: 11,
20
+ FieldType.multi_select: 12,
21
+ FieldType.phone: 7,
22
+ FieldType.email: 6,
23
+ FieldType.address: 21,
24
+ FieldType.attachment: 13,
25
+ FieldType.boolean: 10,
26
+ FieldType.relation: 25,
27
+ FieldType.subtable: 18,
28
+ }
29
+
30
+ OPTION_THEME_PALETTE = [
31
+ {
32
+ "hoverBorderColor": "#D16243",
33
+ "optionColor": "#FCE5DE",
34
+ "textColor": "#571C0C",
35
+ "themeId": 6,
36
+ "tickColor": "#D16243",
37
+ },
38
+ {
39
+ "hoverBorderColor": "#63AD0E",
40
+ "optionColor": "#E6F7D2",
41
+ "textColor": "#244201",
42
+ "themeId": 8,
43
+ "tickColor": "#63AD0E",
44
+ },
45
+ {
46
+ "hoverBorderColor": "#26ACD1",
47
+ "optionColor": "#DCF5FC",
48
+ "textColor": "#054557",
49
+ "themeId": 10,
50
+ "tickColor": "#26ACD1",
51
+ },
52
+ ]
53
+
54
+
55
+ def compile_entity_form(
56
+ entity: EntitySpec,
57
+ *,
58
+ include_package: bool,
59
+ ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None, dict[str, dict[str, Any]], dict[str, str]]:
60
+ field_specs = {field.field_id: field.model_dump(mode="json") for field in entity.fields}
61
+ field_labels = {field.field_id: field.label for field in entity.fields}
62
+ app_create_payload = {
63
+ "appName": entity.display_name,
64
+ "appIcon": encode_workspace_icon(
65
+ icon=entity.icon,
66
+ color=entity.color,
67
+ title=entity.display_name,
68
+ fallback_icon_name="template",
69
+ ),
70
+ "auth": default_member_auth(),
71
+ # Reserve the first slot inside a package for the package portal/homepage.
72
+ "ordinal": (entity.ordinal or 1) + (1 if include_package else 0),
73
+ }
74
+ if include_package:
75
+ app_create_payload["tagIds"] = ["__PACKAGE_TAG_ID__"]
76
+
77
+ questions_by_field_id: dict[str, dict[str, Any]] = {}
78
+ temp_id = -10000
79
+ for field in entity.fields:
80
+ question, next_temp_id = build_question(field.model_dump(mode="json"), temp_id)
81
+ questions_by_field_id[field.field_id] = question
82
+ temp_id = next_temp_id
83
+
84
+ base_questions = build_form_questions(entity, questions_by_field_id, include_reference=False)
85
+ base_payload = default_form_payload(entity.display_name, base_questions)
86
+
87
+ reference_payload = None
88
+ if any(field.type == FieldType.relation for field in entity.fields):
89
+ reference_questions = build_form_questions(entity, questions_by_field_id, include_reference=True)
90
+ reference_payload = default_form_payload(entity.display_name, reference_questions)
91
+ return app_create_payload, base_payload, reference_payload, field_specs, field_labels
92
+
93
+
94
+ def default_member_auth() -> dict[str, Any]:
95
+ return {
96
+ "type": "WORKSPACE",
97
+ "contactAuth": {
98
+ "type": "WORKSPACE_ALL",
99
+ "authMembers": {
100
+ "member": [],
101
+ "depart": [],
102
+ "role": [],
103
+ "dynamic": [],
104
+ "includeSubDeparts": None,
105
+ },
106
+ },
107
+ "externalMemberAuth": {
108
+ "type": "NOT",
109
+ "authMembers": {
110
+ "member": [],
111
+ "depart": [],
112
+ "role": [],
113
+ "dynamic": [],
114
+ "includeSubDeparts": None,
115
+ },
116
+ },
117
+ }
118
+
119
+
120
+ def default_form_payload(form_title: str, form_questions: list[list[dict[str, Any]]]) -> dict[str, Any]:
121
+ return {
122
+ "formTitle": form_title,
123
+ "serialNumType": 1,
124
+ "formTheme": 0,
125
+ "editVersionNo": 1,
126
+ "questionRelations": [],
127
+ "formQues": form_questions,
128
+ }
129
+
130
+
131
+ def build_form_questions(
132
+ entity: EntitySpec,
133
+ questions_by_field_id: dict[str, dict[str, Any]],
134
+ *,
135
+ include_reference: bool,
136
+ ) -> list[list[dict[str, Any]]]:
137
+ used_field_ids: set[str] = set()
138
+ lines: list[list[dict[str, Any]]] = []
139
+ layout = entity.form_layout
140
+ if layout is not None:
141
+ for row in layout.rows:
142
+ row_questions = _row_questions(row, questions_by_field_id, include_reference=include_reference, used_field_ids=used_field_ids)
143
+ if row_questions:
144
+ lines.append(row_questions)
145
+ for section in layout.sections:
146
+ section_question = _section_question(
147
+ section,
148
+ questions_by_field_id,
149
+ include_reference=include_reference,
150
+ used_field_ids=used_field_ids,
151
+ )
152
+ if section_question is not None:
153
+ lines.append([section_question])
154
+ else:
155
+ for field in entity.fields:
156
+ if field.type == FieldType.relation and not include_reference:
157
+ continue
158
+ lines.append([deepcopy(questions_by_field_id[field.field_id])])
159
+ used_field_ids.add(field.field_id)
160
+
161
+ for field in entity.fields:
162
+ if field.field_id in used_field_ids:
163
+ continue
164
+ if field.type == FieldType.relation and not include_reference:
165
+ continue
166
+ lines.append([deepcopy(questions_by_field_id[field.field_id])])
167
+ return lines
168
+
169
+
170
+ def build_question(field: dict[str, Any], temp_id: int) -> tuple[dict[str, Any], int]:
171
+ que_type = QUESTION_TYPE_MAP[field["type"]]
172
+ config = field.get("config") or {}
173
+ question = {
174
+ "queId": 0,
175
+ "queTempId": temp_id,
176
+ "queType": que_type,
177
+ "queTitle": field["label"],
178
+ "queWidth": int(config.get("que_width") or config.get("width") or 100),
179
+ "scanType": 1,
180
+ "status": 1,
181
+ "required": field["required"],
182
+ "queHint": field.get("description") or "",
183
+ "linkedQuestions": {},
184
+ "logicalShow": True,
185
+ "queDefaultValue": None,
186
+ "queDefaultType": 1,
187
+ "subQueWidth": 2,
188
+ "innerQuestions": [],
189
+ "beingHide": False,
190
+ "beingDesensitized": False,
191
+ }
192
+ next_temp_id = temp_id - 1
193
+ field_type = FieldType(field["type"])
194
+ if field_type in (FieldType.number, FieldType.amount, FieldType.text, FieldType.long_text, FieldType.phone, FieldType.email):
195
+ question.update(
196
+ {
197
+ "minWordCount": -1,
198
+ "maxWordCount": -1,
199
+ "canDecimal": field_type == FieldType.amount,
200
+ "numberFormat": 1,
201
+ "currencyType": 1,
202
+ }
203
+ )
204
+ if field_type == FieldType.date:
205
+ question["dateType"] = 0
206
+ if field_type == FieldType.datetime:
207
+ question["dateType"] = 1
208
+ if field_type == FieldType.member:
209
+ question.update({"memberDftValue": None, "memberSelectScopeType": 1, "memberSelectScope": {"member": []}})
210
+ if field_type == FieldType.department:
211
+ question.update({"deptSelectScopeType": 1, "deptSelectScope": {"depart": []}})
212
+ if field_type in (FieldType.single_select, FieldType.multi_select, FieldType.boolean):
213
+ options = field.get("options") or (["是", "否"] if field_type == FieldType.boolean else ["未命名1", "未命名2", "未命名3"])
214
+ question.update(
215
+ {
216
+ "options": build_options(options, start_temp_id=next_temp_id - len(options) + 1),
217
+ "unnamedOptions": options,
218
+ "optDirection": 0,
219
+ "beingShowColor": True,
220
+ }
221
+ )
222
+ if field_type == FieldType.multi_select:
223
+ question["optSelectMode"] = 1
224
+ next_temp_id -= len(options)
225
+ if field_type == FieldType.attachment:
226
+ question.update(
227
+ {
228
+ "fileBeingCaptureOnly": 0,
229
+ "fileSize": 20,
230
+ "fileType": [],
231
+ "pluginStatus": False,
232
+ "imageCompress": False,
233
+ "queDefaultValues": {"queId": temp_id, "queTitle": field["label"], "queType": que_type, "values": []},
234
+ }
235
+ )
236
+ if field_type == FieldType.address:
237
+ question.update({"addressPrecision": 1, "queDefaultValues": {"queId": temp_id, "queTitle": field["label"], "queType": que_type, "values": [], "tableValues": []}})
238
+ if field_type == FieldType.relation:
239
+ question.update(
240
+ {
241
+ "referenceConfig": build_reference_config(field, temp_id),
242
+ "queOriginType": 25,
243
+ "queDefaultType": 2,
244
+ }
245
+ )
246
+ if field_type == FieldType.subtable:
247
+ sub_questions: list[dict[str, Any]] = []
248
+ for subfield in field.get("subfields", []):
249
+ sub_question, next_temp_id = build_question(subfield, next_temp_id)
250
+ sub_questions.append(sub_question)
251
+ question["subQuestions"] = sub_questions
252
+ question["innerQuestions"] = [deepcopy(sub_questions)]
253
+ question["queDefaultValues"] = {"queId": temp_id, "queTitle": field["label"], "queType": que_type, "values": [], "tableValues": []}
254
+ return question, next_temp_id
255
+
256
+
257
+ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any]:
258
+ config = field.get("config") or {}
259
+ display_field_id = field.get("target_field_id") or "title"
260
+ display_field_que_id = field.get("target_field_que_id") or config.get("target_field_que_id") or 0
261
+ display_field_label = config.get("target_field_label") or "__TARGET_FIELD_LABEL__"
262
+ refer_field_ids = list(config.get("refer_field_ids") or [display_field_id])
263
+ refer_field_que_ids = list(config.get("refer_field_que_ids") or [])
264
+ refer_field_labels = config.get("refer_field_labels") or [display_field_label]
265
+ refer_field_types = config.get("refer_field_types")
266
+ refer_questions = []
267
+ for ordinal, field_id in enumerate(refer_field_ids, start=1):
268
+ label = refer_field_labels[ordinal - 1] if ordinal - 1 < len(refer_field_labels) else field_id
269
+ que_id = refer_field_que_ids[ordinal - 1] if ordinal - 1 < len(refer_field_que_ids) else 0
270
+ raw_type = _reference_field_type_value(
271
+ refer_field_types,
272
+ field_id=field_id,
273
+ ordinal=ordinal,
274
+ default=config.get("target_field_type") if field_id == display_field_id else None,
275
+ )
276
+ refer_questions.append(
277
+ {
278
+ "queId": que_id,
279
+ "queTitle": label,
280
+ "queType": _normalize_reference_que_type(raw_type) or "2",
281
+ "queAuth": 1,
282
+ "ordinal": ordinal,
283
+ "quoteId": temp_id,
284
+ "_field_id": field_id,
285
+ }
286
+ )
287
+ auth_field_ids = list(config.get("auth_field_ids") or refer_field_ids)
288
+ auth_field_que_ids = list(config.get("auth_field_que_ids") or [])
289
+ auth_ques = deepcopy(config.get("refer_auth_ques") or [])
290
+ if not auth_ques:
291
+ auth_ques = []
292
+ for ordinal, field_id in enumerate(auth_field_ids, start=1):
293
+ que_id = auth_field_que_ids[ordinal - 1] if ordinal - 1 < len(auth_field_que_ids) else 0
294
+ auth_ques.append({"queId": que_id, "queAuth": 1, "_field_id": field_id})
295
+ return {
296
+ "referAppKey": "__TARGET_APP_KEY__",
297
+ "referQueId": display_field_que_id,
298
+ "customButtonText": config.get("custom_button_text") or "选择数据",
299
+ "beingTableSource": False,
300
+ "referQuestions": refer_questions,
301
+ "referMatchRules": deepcopy(config.get("refer_match_rules") or []),
302
+ "referFillRules": deepcopy(config.get("refer_fill_rules") or []),
303
+ "referAuthQues": auth_ques,
304
+ "canAddData": bool(config.get("can_add_data", False)),
305
+ "dataAdditionButtonText": config.get("data_addition_button_text") or "新增数据",
306
+ "canViewProcessLog": bool(config.get("can_view_process_log", True)),
307
+ "optionalDataNum": int(config.get("optional_data_num", 1)),
308
+ "beingDataLogVisible": bool(config.get("being_data_log_visible", True)),
309
+ "beingDefaultFormulaAutoFillEnabled": bool(config.get("being_default_formula_auto_fill_enabled", False)),
310
+ "defaultValueMatchRules": deepcopy(config.get("default_value_match_rules") or []),
311
+ "configShowForm": config.get("config_show_form") or "TABLE",
312
+ "configSortFieldId": None,
313
+ "configAsc": config.get("config_asc", True),
314
+ "dataShowForm": config.get("data_show_form") or "CARD",
315
+ "defaultRow": config.get("default_row"),
316
+ "fieldNameShow": config.get("field_name_show", True),
317
+ "dataSortFieldId": None,
318
+ "dataSortAsc": config.get("data_sort_asc"),
319
+ "_targetFieldId": display_field_id,
320
+ "_targetEntityId": field.get("target_entity_id"),
321
+ }
322
+
323
+
324
+ def build_options(options: list[str], *, start_temp_id: int) -> list[dict[str, Any]]:
325
+ items: list[dict[str, Any]] = []
326
+ current = start_temp_id
327
+ for index, option in enumerate(options):
328
+ items.append(
329
+ {
330
+ "otherValue": None,
331
+ "beingDefault": False,
332
+ "optId": 0,
333
+ "optValue": option,
334
+ "optUrl": "",
335
+ "beingOtherOpt": False,
336
+ "beingRequired": False,
337
+ "applyLimit": -1,
338
+ "currentApply": 0,
339
+ "linkQueIds": [],
340
+ "emptyText": "",
341
+ "tempId": current,
342
+ "optionThemeColor": deepcopy(OPTION_THEME_PALETTE[index % len(OPTION_THEME_PALETTE)]),
343
+ }
344
+ )
345
+ current += 1
346
+ return items
347
+
348
+
349
+ def _row_questions(
350
+ row: FormLayoutRowSpec,
351
+ questions_by_field_id: dict[str, dict[str, Any]],
352
+ *,
353
+ include_reference: bool,
354
+ used_field_ids: set[str],
355
+ ) -> list[dict[str, Any]]:
356
+ questions: list[dict[str, Any]] = []
357
+ for field_id in row.field_ids:
358
+ question = deepcopy(questions_by_field_id[field_id])
359
+ if question.get("queType") == 25 and not include_reference:
360
+ continue
361
+ used_field_ids.add(field_id)
362
+ questions.append(question)
363
+ widths = _resolve_row_widths(row, questions)
364
+ for question, width in zip(questions, widths):
365
+ question["queWidth"] = width
366
+ return questions
367
+
368
+
369
+ def _section_question(
370
+ section: FormLayoutSectionSpec,
371
+ questions_by_field_id: dict[str, dict[str, Any]],
372
+ *,
373
+ include_reference: bool,
374
+ used_field_ids: set[str],
375
+ ) -> dict[str, Any] | None:
376
+ inner_rows: list[list[dict[str, Any]]] = []
377
+ for row in section.rows:
378
+ row_questions = _row_questions(row, questions_by_field_id, include_reference=include_reference, used_field_ids=used_field_ids)
379
+ if row_questions:
380
+ inner_rows.append(row_questions)
381
+ if not inner_rows:
382
+ return None
383
+ config = section.config or {}
384
+ return {
385
+ "queId": 0,
386
+ "queTempId": -(20000 + sum(ord(ch) for ch in section.section_id)),
387
+ "queType": 24,
388
+ "queTitle": section.title,
389
+ "queWidth": int(config.get("que_width") or 100),
390
+ "scanType": 1,
391
+ "status": 1,
392
+ "required": False,
393
+ "queHint": config.get("description") or "",
394
+ "linkedQuestions": {},
395
+ "logicalShow": True,
396
+ "queDefaultValue": None,
397
+ "queDefaultType": 1,
398
+ "subQueWidth": 2,
399
+ "innerQuestions": inner_rows,
400
+ "beingHide": False,
401
+ "beingDesensitized": False,
402
+ "sectionId": config.get("section_id"),
403
+ }
404
+
405
+
406
+ def _resolve_row_widths(row: FormLayoutRowSpec, questions: list[dict[str, Any]]) -> list[int]:
407
+ if not questions:
408
+ return []
409
+ configured_widths = row.config.get("field_widths") if isinstance(row.config.get("field_widths"), dict) else {}
410
+ widths: list[int] = []
411
+ remaining_slots = 0
412
+ assigned_total = 0
413
+ for field_id, question in zip(row.field_ids, questions):
414
+ width = configured_widths.get(field_id)
415
+ if width is None:
416
+ widths.append(-1)
417
+ remaining_slots += 1
418
+ continue
419
+ resolved_width = int(width)
420
+ widths.append(resolved_width)
421
+ assigned_total += resolved_width
422
+ if remaining_slots <= 0:
423
+ return widths
424
+ remaining_total = max(0, 100 - assigned_total)
425
+ auto_widths = _auto_row_widths(remaining_slots, total=remaining_total)
426
+ auto_index = 0
427
+ for index, width in enumerate(widths):
428
+ if width >= 0:
429
+ continue
430
+ widths[index] = auto_widths[auto_index]
431
+ auto_index += 1
432
+ return widths
433
+
434
+
435
+ def _auto_row_widths(count: int, *, total: int = 100) -> list[int]:
436
+ if count <= 0:
437
+ return []
438
+ base = total // count
439
+ remainder = total % count
440
+ return [base + (1 if index < remainder else 0) for index in range(count)]
441
+
442
+
443
+ def _reference_field_type_value(raw_types: Any, *, field_id: str, ordinal: int, default: Any) -> Any:
444
+ if isinstance(raw_types, dict):
445
+ return raw_types.get(field_id, default)
446
+ if isinstance(raw_types, list):
447
+ index = ordinal - 1
448
+ if 0 <= index < len(raw_types):
449
+ return raw_types[index]
450
+ return default
451
+
452
+
453
+ def _normalize_reference_que_type(raw_type: Any) -> str | None:
454
+ if raw_type is None:
455
+ return None
456
+ if isinstance(raw_type, int):
457
+ return str(raw_type)
458
+ value = str(raw_type).strip()
459
+ if not value:
460
+ return None
461
+ if value.isdigit():
462
+ return value
463
+ try:
464
+ return str(QUESTION_TYPE_MAP[FieldType(value)])
465
+ except ValueError:
466
+ return None
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+
6
+ DEFAULT_ICON_COLOR = "qing-orange"
7
+ SUPPORTED_EX_ICONS = {
8
+ "ex-alert-outlined",
9
+ "ex-clock-outlined",
10
+ "ex-folder-outlined",
11
+ "ex-form-outlined",
12
+ "ex-home-outlined",
13
+ "ex-user-outlined",
14
+ }
15
+
16
+ ICON_ALIAS_MAP = {
17
+ "archive-box": "ex-form-outlined",
18
+ "beaker": "ex-form-outlined",
19
+ "briefcase": "ex-folder-outlined",
20
+ "building-office": "ex-form-outlined",
21
+ "calendar": "ex-clock-outlined",
22
+ "chat": "ex-clock-outlined",
23
+ "chat-bubble-left-right": "ex-clock-outlined",
24
+ "chart-bar": "ex-form-outlined",
25
+ "chart-square-bar": "ex-form-outlined",
26
+ "check-badge": "ex-alert-outlined",
27
+ "check-circle": "ex-alert-outlined",
28
+ "clipboard-check": "ex-form-outlined",
29
+ "clock": "ex-clock-outlined",
30
+ "cube": "ex-form-outlined",
31
+ "document-text": "ex-form-outlined",
32
+ "folder": "ex-folder-outlined",
33
+ "home": "ex-home-outlined",
34
+ "office-building": "ex-form-outlined",
35
+ "shield-check": "ex-alert-outlined",
36
+ "template": "ex-form-outlined",
37
+ "user": "ex-user-outlined",
38
+ "user-group": "ex-user-outlined",
39
+ "users": "ex-user-outlined",
40
+ "view-grid": "ex-home-outlined",
41
+ "warning": "ex-alert-outlined",
42
+ }
43
+
44
+
45
+ def encode_workspace_icon(
46
+ *,
47
+ icon: str | None,
48
+ color: str | None,
49
+ title: str,
50
+ fallback_icon_name: str | None = None,
51
+ ) -> str:
52
+ if icon and _looks_like_icon_json(icon):
53
+ return icon
54
+ normalized_icon_name = normalize_workspace_icon_name(icon or fallback_icon_name)
55
+ if normalized_icon_name and normalized_icon_name.startswith("ex-") and not color:
56
+ return normalized_icon_name
57
+ icon_name = normalized_icon_name
58
+ icon_color = color or DEFAULT_ICON_COLOR
59
+ if icon_name:
60
+ return json.dumps(
61
+ {
62
+ "icon": None,
63
+ "iconColor": icon_color,
64
+ "iconName": icon_name,
65
+ "iconText": None,
66
+ },
67
+ ensure_ascii=False,
68
+ separators=(",", ":"),
69
+ )
70
+ icon_text = (title or "未")[:1]
71
+ return json.dumps(
72
+ {
73
+ "iconColor": icon_color,
74
+ "iconText": icon_text,
75
+ },
76
+ ensure_ascii=False,
77
+ separators=(",", ":"),
78
+ )
79
+
80
+
81
+ def normalize_workspace_icon_name(icon: str | None) -> str | None:
82
+ if not icon:
83
+ return None
84
+ if _looks_like_icon_json(icon):
85
+ try:
86
+ payload = json.loads(icon)
87
+ except Exception:
88
+ return "ex-form-outlined"
89
+ return normalize_workspace_icon_name(payload.get("iconName")) or "ex-form-outlined"
90
+ if icon in SUPPORTED_EX_ICONS:
91
+ return icon
92
+ normalized = icon.strip().lower()
93
+ if normalized in ICON_ALIAS_MAP:
94
+ return ICON_ALIAS_MAP[normalized]
95
+ for token in normalized.replace("_", "-").split("-"):
96
+ if token in ICON_ALIAS_MAP:
97
+ return ICON_ALIAS_MAP[token]
98
+ if "folder" in normalized or "package" in normalized:
99
+ return "ex-folder-outlined"
100
+ if "home" in normalized or "portal" in normalized or "dashboard" in normalized:
101
+ return "ex-home-outlined"
102
+ if "user" in normalized or "member" in normalized or "contact" in normalized:
103
+ return "ex-user-outlined"
104
+ if "clock" in normalized or "time" in normalized or "schedule" in normalized or "log" in normalized:
105
+ return "ex-clock-outlined"
106
+ if "risk" in normalized or "alert" in normalized or "warn" in normalized or "safety" in normalized:
107
+ return "ex-alert-outlined"
108
+ return "ex-form-outlined"
109
+
110
+
111
+ def _looks_like_icon_json(value: str) -> bool:
112
+ stripped = value.strip()
113
+ return stripped.startswith("{") and stripped.endswith("}")
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..spec_models import NavigationItemSpec, NavigationTargetType, SolutionSpec
6
+ from .icon_utils import normalize_workspace_icon_name
7
+
8
+
9
+ def compile_navigation(spec: SolutionSpec) -> list[dict[str, Any]]:
10
+ if not spec.preferences.create_navigation or not spec.navigation.enabled:
11
+ return []
12
+ compiled_items = [_compile_item(item) for item in spec.navigation.items]
13
+ if not compiled_items:
14
+ return []
15
+ package_item = next(
16
+ (
17
+ item
18
+ for item in compiled_items
19
+ if item["payload"].get("targetType") == "TAG"
20
+ ),
21
+ None,
22
+ )
23
+ if package_item is None:
24
+ return compiled_items[:1]
25
+ package_item["children"] = []
26
+ return [package_item]
27
+
28
+
29
+ def _compile_item(item: NavigationItemSpec) -> dict[str, Any]:
30
+ default_icon = _default_navigation_icon(item.target_type)
31
+ payload: dict[str, Any] = {
32
+ "item_id": item.item_id,
33
+ "payload": {
34
+ "name": item.title,
35
+ "navigationIcon": normalize_workspace_icon_name(item.icon or default_icon) or default_icon,
36
+ "auth": {"type": "WORKSPACE"},
37
+ **item.config,
38
+ },
39
+ "children": [_compile_item(child) for child in item.children],
40
+ }
41
+ if item.target_type == NavigationTargetType.package:
42
+ payload["payload"].update({"targetType": "TAG", "tagId": "__PACKAGE_TAG_ID__"})
43
+ elif item.target_type == NavigationTargetType.app:
44
+ payload["payload"].update({"targetType": "APP", "appRef": {"entity_id": item.entity_id}})
45
+ elif item.target_type == NavigationTargetType.portal:
46
+ payload["payload"].update({"targetType": "DASH", "dashKey": "__PORTAL_DASH_KEY__"})
47
+ else:
48
+ raise ValueError("navigation target_type currently supports only: package, app, portal")
49
+ return payload
50
+
51
+
52
+ def _default_navigation_icon(target_type: NavigationTargetType) -> str:
53
+ if target_type == NavigationTargetType.package:
54
+ return "ex-folder-outlined"
55
+ if target_type == NavigationTargetType.portal:
56
+ return "ex-home-outlined"
57
+ return "ex-form-outlined"
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from ..spec_models import SolutionSpec
4
+ from .icon_utils import encode_workspace_icon
5
+
6
+
7
+ def compile_package(spec: SolutionSpec) -> dict | None:
8
+ if not spec.preferences.create_package or not spec.package.enabled:
9
+ return None
10
+ return {
11
+ "tagName": spec.package.name or spec.solution_name,
12
+ "tagIcon": encode_workspace_icon(
13
+ icon=spec.package.icon,
14
+ color=spec.package.color,
15
+ title=spec.package.name or spec.solution_name,
16
+ fallback_icon_name="files-folder",
17
+ ),
18
+ "ordinal": spec.package.ordinal,
19
+ }