@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,23 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from .spec_models import SolutionSpec
5
+
6
+
7
+ def normalize_solution_spec(spec: SolutionSpec) -> SolutionSpec:
8
+ payload = deepcopy(spec.model_dump())
9
+ payload["package"]["name"] = payload["package"].get("name") or spec.solution_name
10
+ payload["preferences"]["multi_app"] = len(spec.entities) > 1 if spec.preferences.multi_app is None else spec.preferences.multi_app
11
+ if spec.portal.enabled and spec.portal.sections:
12
+ payload["portal"]["name"] = payload["portal"].get("name") or f"{spec.solution_name} 首页"
13
+
14
+ normalized_entities: list[dict[str, object]] = []
15
+ for ordinal, entity in enumerate(spec.entities, start=1):
16
+ entity_payload = deepcopy(entity.model_dump())
17
+ entity_payload["ordinal"] = entity_payload.get("ordinal") or ordinal
18
+ normalized_entities.append(entity_payload)
19
+ payload["entities"] = normalized_entities
20
+
21
+ if spec.navigation.enabled and not spec.navigation.items:
22
+ payload["navigation"]["enabled"] = False
23
+ return SolutionSpec.model_validate(payload)
@@ -0,0 +1,536 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import re
5
+ from typing import Any
6
+ from uuid import uuid4
7
+
8
+ from .spec_models import EntityKind, FieldType
9
+
10
+
11
+ @dataclass
12
+ class GeneratedAppBuild:
13
+ app_spec: dict[str, Any]
14
+ summary: dict[str, Any]
15
+
16
+
17
+ class RequirementsBuildError(ValueError):
18
+ def __init__(
19
+ self,
20
+ message: str,
21
+ *,
22
+ error_code: str = "REQUIREMENTS_INVALID",
23
+ recoverable: bool = True,
24
+ missing_required_fields: list[str] | None = None,
25
+ invalid_field_types: list[str] | None = None,
26
+ details: dict[str, Any] | None = None,
27
+ ) -> None:
28
+ super().__init__(message)
29
+ self.error_code = error_code
30
+ self.recoverable = recoverable
31
+ self.missing_required_fields = list(missing_required_fields or [])
32
+ self.invalid_field_types = list(invalid_field_types or [])
33
+ self.details = dict(details or {})
34
+
35
+
36
+ SAFE_DEFAULT_FIELD_BLUEPRINTS: list[dict[str, Any]] = [
37
+ {"field_id": "title", "label": "标题", "type": FieldType.text.value, "required": True},
38
+ {"field_id": "description", "label": "说明", "type": FieldType.long_text.value},
39
+ {"field_id": "quantity", "label": "数量", "type": FieldType.number.value},
40
+ {"field_id": "amount", "label": "金额", "type": FieldType.amount.value},
41
+ {"field_id": "event_date", "label": "日期", "type": FieldType.date.value},
42
+ {"field_id": "event_time", "label": "日期时间", "type": FieldType.datetime.value},
43
+ {"field_id": "owner", "label": "负责人", "type": FieldType.member.value},
44
+ {"field_id": "department", "label": "所属部门", "type": FieldType.department.value},
45
+ {
46
+ "field_id": "status",
47
+ "label": "状态",
48
+ "type": FieldType.single_select.value,
49
+ "options": ["待处理", "处理中", "已完成"],
50
+ },
51
+ {
52
+ "field_id": "tags",
53
+ "label": "标签",
54
+ "type": FieldType.multi_select.value,
55
+ "options": ["标签A", "标签B", "标签C"],
56
+ },
57
+ {"field_id": "phone", "label": "联系电话", "type": FieldType.phone.value},
58
+ {"field_id": "email", "label": "邮箱", "type": FieldType.email.value},
59
+ {"field_id": "address", "label": "地址", "type": FieldType.address.value},
60
+ {"field_id": "attachments", "label": "附件", "type": FieldType.attachment.value},
61
+ {"field_id": "enabled", "label": "是否启用", "type": FieldType.boolean.value, "options": ["是", "否"]},
62
+ ]
63
+
64
+ ADVANCED_FIELD_BLUEPRINTS: list[dict[str, Any]] = [
65
+ {
66
+ "field_id": "related_record",
67
+ "label": "关联记录",
68
+ "type": FieldType.relation.value,
69
+ "target_field_id": "title",
70
+ },
71
+ {
72
+ "field_id": "detail_items",
73
+ "label": "明细",
74
+ "type": FieldType.subtable.value,
75
+ "subfields": [
76
+ {"field_id": "item_name", "label": "子项名称", "type": FieldType.text.value, "required": True},
77
+ {"field_id": "item_amount", "label": "子项金额", "type": FieldType.amount.value},
78
+ ],
79
+ },
80
+ ]
81
+ SUPPORTED_FIELD_BLUEPRINTS: list[dict[str, Any]] = SAFE_DEFAULT_FIELD_BLUEPRINTS + ADVANCED_FIELD_BLUEPRINTS
82
+ FIELD_BLUEPRINTS_BY_TYPE = {str(item["type"]): item for item in SUPPORTED_FIELD_BLUEPRINTS}
83
+ SAFE_DEFAULT_FIELD_TYPES = [str(item["type"]) for item in SAFE_DEFAULT_FIELD_BLUEPRINTS]
84
+ ADVANCED_FIELD_TYPES = [str(item["type"]) for item in ADVANCED_FIELD_BLUEPRINTS]
85
+ ADVANCED_FIELD_LABELS = {
86
+ FieldType.relation.value: "relation",
87
+ FieldType.subtable.value: "subtable",
88
+ }
89
+
90
+ FIELD_TYPE_ALIASES: dict[str, tuple[str, ...]] = {
91
+ FieldType.text.value: ("单行文本", "文本", "标题", "名称"),
92
+ FieldType.long_text.value: ("多行文本", "备注", "描述", "说明"),
93
+ FieldType.number.value: ("数字", "数量", "计数", "整数"),
94
+ FieldType.amount.value: ("金额", "金额字段", "预算", "费用"),
95
+ FieldType.date.value: ("日期", "开始日期", "截止日期"),
96
+ FieldType.datetime.value: ("日期时间", "时间", "时间戳"),
97
+ FieldType.member.value: ("成员", "负责人", "审批人"),
98
+ FieldType.department.value: ("部门", "组织", "团队"),
99
+ FieldType.single_select.value: ("单选", "状态", "下拉单选"),
100
+ FieldType.multi_select.value: ("多选", "标签", "下拉多选"),
101
+ FieldType.phone.value: ("电话", "手机号", "手机"),
102
+ FieldType.email.value: ("邮箱", "电子邮箱", "email"),
103
+ FieldType.address.value: ("地址", "所在地", "location"),
104
+ FieldType.attachment.value: ("附件", "文件", "图片"),
105
+ FieldType.boolean.value: ("开关", "布尔", "是否"),
106
+ FieldType.relation.value: ("关联", "引用", "关联记录"),
107
+ FieldType.subtable.value: ("子表", "明细表", "明细", "table"),
108
+ }
109
+
110
+ ALL_FIELDS_HINTS = ("所有字段", "全字段", "全部字段", "字段完整", "所有字段类型", "常见字段类型")
111
+ LAYOUT_HINTS = {
112
+ "balanced": ("优美", "美观", "清晰", "有层次", "布局优美", "完整布局", "布局完整"),
113
+ "grouped": ("分组", "section", "grouped"),
114
+ "compact": ("紧凑", "compact"),
115
+ "flat": ("平铺", "flat"),
116
+ }
117
+ ENTITY_KIND_HINTS = {
118
+ EntityKind.master.value: ("主数据", "基础资料", "档案", "字典", "客户", "员工"),
119
+ EntityKind.transaction.value: ("申请", "单据", "审批", "事务", "流程"),
120
+ EntityKind.activity.value: ("跟进", "活动", "日志", "记录"),
121
+ }
122
+
123
+
124
+ def build_app_spec_from_requirements(
125
+ *,
126
+ title: str,
127
+ requirement_text: str,
128
+ package_name: str | None,
129
+ layout_style: str = "auto",
130
+ ) -> GeneratedAppBuild:
131
+ cleaned_title = title.strip()
132
+ cleaned_text = requirement_text.strip()
133
+ if not cleaned_title:
134
+ raise ValueError("title is required")
135
+ if not cleaned_text:
136
+ raise ValueError("requirement_text is required")
137
+
138
+ resolved_layout = _resolve_layout_style(cleaned_text, layout_style)
139
+ entity_kind = _resolve_entity_kind(cleaned_text)
140
+ entity_id = _slugify_identifier(cleaned_title, prefix="entity")
141
+ package_label = package_name.strip() if isinstance(package_name, str) and package_name.strip() else None
142
+
143
+ selected_types = _resolve_requested_field_types(cleaned_text)
144
+ advanced_requested = [field_type for field_type in selected_types if field_type in ADVANCED_FIELD_TYPES]
145
+ all_fields_mode = any(hint in cleaned_text for hint in ALL_FIELDS_HINTS)
146
+ if all_fields_mode:
147
+ selected_types = list(SAFE_DEFAULT_FIELD_TYPES)
148
+ elif not selected_types:
149
+ selected_types = [
150
+ FieldType.text.value,
151
+ FieldType.long_text.value,
152
+ FieldType.single_select.value,
153
+ FieldType.member.value,
154
+ FieldType.date.value,
155
+ FieldType.attachment.value,
156
+ ]
157
+
158
+ resolved_advanced_fields = _resolve_advanced_fields(requirement_text=cleaned_text, entity_id=entity_id)
159
+ advanced_admitted = [field["type"] for field in resolved_advanced_fields]
160
+ excluded_advanced_fields = [
161
+ ADVANCED_FIELD_LABELS.get(field_type, field_type)
162
+ for field_type in ADVANCED_FIELD_TYPES
163
+ if field_type not in advanced_admitted
164
+ ]
165
+
166
+ fields = _build_fields_for_types(selected_types=selected_types, advanced_fields=resolved_advanced_fields)
167
+ layout = _build_layout(fields=fields, layout_style=resolved_layout)
168
+ follow_up_actions = _infer_follow_up_actions(cleaned_text)
169
+ assumptions = _infer_assumptions(
170
+ cleaned_text,
171
+ all_fields_mode=all_fields_mode,
172
+ layout_style=resolved_layout,
173
+ excluded_advanced_fields=excluded_advanced_fields,
174
+ )
175
+
176
+ app_spec = {
177
+ "solution_name": cleaned_title,
178
+ "summary": cleaned_text,
179
+ "package": {"enabled": True, "name": package_label or cleaned_title},
180
+ "publish_policy": {"apps": True, "portal": False, "navigation": False},
181
+ "preferences": {"create_package": package_label is None, "create_portal": False, "create_navigation": False},
182
+ "assumptions": assumptions,
183
+ "metadata": {
184
+ "generator": {
185
+ "mode": "heuristic_requirements_interpreter",
186
+ "all_fields_mode": all_fields_mode,
187
+ "resolved_layout_style": resolved_layout,
188
+ "field_selection_policy": "safe_default",
189
+ "excluded_advanced_fields": excluded_advanced_fields,
190
+ }
191
+ },
192
+ "entities": [
193
+ {
194
+ "entity_id": entity_id,
195
+ "display_name": cleaned_title,
196
+ "kind": entity_kind,
197
+ "title_field_id": "title",
198
+ "fields": fields,
199
+ "form_layout": layout,
200
+ "sample_records": [],
201
+ "acceptance_criteria": [
202
+ "表单可以成功创建并发布",
203
+ "字段与布局满足需求文本中的显式要求",
204
+ ],
205
+ }
206
+ ],
207
+ }
208
+
209
+ summary = {
210
+ "title": cleaned_title,
211
+ "entity_id": entity_id,
212
+ "entity_kind": entity_kind,
213
+ "package_name": package_label,
214
+ "resolved_layout_style": resolved_layout,
215
+ "field_count": len(fields),
216
+ "field_labels": [field["label"] for field in fields],
217
+ "field_types": [field["type"] for field in fields],
218
+ "all_fields_mode": all_fields_mode,
219
+ "field_selection_policy": "safe_default",
220
+ "excluded_advanced_fields": excluded_advanced_fields,
221
+ "advanced_fields_requested": [ADVANCED_FIELD_LABELS.get(field_type, field_type) for field_type in advanced_requested],
222
+ "advanced_fields_admitted": [ADVANCED_FIELD_LABELS.get(field_type, field_type) for field_type in advanced_admitted],
223
+ "assumptions": assumptions,
224
+ "follow_up_actions": follow_up_actions,
225
+ }
226
+ return GeneratedAppBuild(app_spec=app_spec, summary=summary)
227
+
228
+
229
+ def _resolve_requested_field_types(requirement_text: str) -> list[str]:
230
+ selected: list[str] = []
231
+ for field_type, aliases in FIELD_TYPE_ALIASES.items():
232
+ if any(alias.lower() in requirement_text.lower() for alias in aliases):
233
+ selected.append(field_type)
234
+ if FieldType.text.value not in selected:
235
+ selected.insert(0, FieldType.text.value)
236
+ return selected
237
+
238
+
239
+ def _build_fields_for_types(*, selected_types: list[str], advanced_fields: list[dict[str, Any]]) -> list[dict[str, Any]]:
240
+ ordered = []
241
+ seen: set[str] = set()
242
+ for field_type in SAFE_DEFAULT_FIELD_TYPES:
243
+ if field_type not in selected_types:
244
+ continue
245
+ blueprint = FIELD_BLUEPRINTS_BY_TYPE[field_type]
246
+ field_id = str(blueprint["field_id"])
247
+ if field_id in seen:
248
+ continue
249
+ seen.add(field_id)
250
+ ordered.append(_clone_field_blueprint(blueprint))
251
+ for field in advanced_fields:
252
+ field_id = str(field["field_id"])
253
+ if field_id in seen:
254
+ continue
255
+ seen.add(field_id)
256
+ ordered.append(_clone_field_blueprint(field))
257
+ return ordered
258
+
259
+
260
+ def _build_layout(*, fields: list[dict[str, Any]], layout_style: str) -> dict[str, Any]:
261
+ field_ids = [str(field["field_id"]) for field in fields]
262
+ if layout_style == "flat":
263
+ return {"rows": [{"field_ids": [field_id]} for field_id in field_ids]}
264
+ if layout_style == "compact":
265
+ pairs = [field_ids[index : index + 2] for index in range(0, len(field_ids), 2)]
266
+ return {"rows": [{"field_ids": pair} for pair in pairs]}
267
+ if layout_style == "balanced":
268
+ return _build_balanced_layout(fields)
269
+
270
+ sections = []
271
+ grouped_ids = {
272
+ "basic": [item for item in field_ids if item in {"title", "description", "quantity", "amount", "event_date", "event_time"}],
273
+ "collaboration": [item for item in field_ids if item in {"owner", "department", "status", "tags", "enabled"}],
274
+ "contact": [item for item in field_ids if item in {"phone", "email", "address"}],
275
+ "extended": [item for item in field_ids if item in {"attachments", "related_record", "detail_items"}],
276
+ }
277
+ section_titles = {
278
+ "basic": "基础信息",
279
+ "collaboration": "协作信息",
280
+ "contact": "联系信息",
281
+ "extended": "扩展信息",
282
+ }
283
+ for section_id, ids in grouped_ids.items():
284
+ if not ids:
285
+ continue
286
+ rows = [{"field_ids": ids[index : index + 2]} for index in range(0, len(ids), 2)]
287
+ sections.append({"section_id": section_id, "title": section_titles[section_id], "rows": rows})
288
+ return {"sections": sections or [{"section_id": "basic", "title": "基础信息", "rows": [{"field_ids": field_ids}]}]}
289
+
290
+
291
+ def _resolve_layout_style(requirement_text: str, layout_style: str) -> str:
292
+ raw = (layout_style or "auto").strip().lower()
293
+ if raw in {"balanced", "grouped", "compact", "flat"}:
294
+ return raw
295
+ for candidate, aliases in LAYOUT_HINTS.items():
296
+ if any(alias.lower() in requirement_text.lower() for alias in aliases):
297
+ return candidate
298
+ return "grouped"
299
+
300
+
301
+ def _resolve_entity_kind(requirement_text: str) -> str:
302
+ for kind, aliases in ENTITY_KIND_HINTS.items():
303
+ if any(alias.lower() in requirement_text.lower() for alias in aliases):
304
+ return kind
305
+ return EntityKind.transaction.value
306
+
307
+
308
+ def _infer_follow_up_actions(requirement_text: str) -> list[str]:
309
+ actions: list[str] = []
310
+ lower_text = requirement_text.lower()
311
+ if any(token in lower_text for token in ("流程", "审批", "workflow", "approve")):
312
+ actions.append("如果需要流程,请在应用创建后继续调用 solution_build_flow。")
313
+ if any(token in lower_text for token in ("视图", "看板", "view", "board")):
314
+ actions.append("如果需要视图,请在应用创建后继续调用 solution_build_views。")
315
+ if any(token in lower_text for token in ("报表", "门户", "chart", "portal")):
316
+ actions.append("如果需要报表或门户,请在应用创建后继续调用 solution_build_analytics_portal。")
317
+ return actions
318
+
319
+
320
+ def _infer_assumptions(
321
+ requirement_text: str,
322
+ *,
323
+ all_fields_mode: bool,
324
+ layout_style: str,
325
+ excluded_advanced_fields: list[str],
326
+ ) -> list[str]:
327
+ assumptions = [f"布局风格已解析为 {layout_style}。"]
328
+ if all_fields_mode:
329
+ assumptions.append("已将“所有字段”解释为安全默认字段集合,不自动包含 relation/subtable。")
330
+ if excluded_advanced_fields:
331
+ assumptions.append(f"以下高级字段未自动纳入:{', '.join(excluded_advanced_fields)}。")
332
+ return assumptions
333
+
334
+
335
+ def _resolve_advanced_fields(*, requirement_text: str, entity_id: str) -> list[dict[str, Any]]:
336
+ advanced_fields: list[dict[str, Any]] = []
337
+ lower_text = requirement_text.lower()
338
+ if any(alias.lower() in lower_text for alias in FIELD_TYPE_ALIASES[FieldType.relation.value]):
339
+ relation_field = _build_relation_field(requirement_text=requirement_text, entity_id=entity_id)
340
+ advanced_fields.append(relation_field)
341
+ if any(alias.lower() in lower_text for alias in FIELD_TYPE_ALIASES[FieldType.subtable.value]):
342
+ subtable_field = _build_subtable_field(requirement_text=requirement_text)
343
+ advanced_fields.append(subtable_field)
344
+ return advanced_fields
345
+
346
+
347
+ def _build_relation_field(*, requirement_text: str, entity_id: str) -> dict[str, Any]:
348
+ target_phrase = _extract_relation_target(requirement_text)
349
+ if not target_phrase:
350
+ raise RequirementsBuildError(
351
+ "relation field requires an explicit target entity; specify what it should relate to",
352
+ missing_required_fields=["relation.target_entity_id"],
353
+ invalid_field_types=["relation"],
354
+ details={
355
+ "requested_field_type": "relation",
356
+ "suggested_requirement_hint": "例如:关联到客户档案。",
357
+ "suggested_patch": {
358
+ "entities": [
359
+ {
360
+ "entity_id": "target_entity",
361
+ "display_name": "目标应用",
362
+ "kind": EntityKind.master.value,
363
+ "title_field_id": "title",
364
+ "fields": [{"field_id": "title", "label": "标题", "type": FieldType.text.value, "required": True}],
365
+ "form_layout": {"rows": [{"field_ids": ["title"]}]},
366
+ }
367
+ ]
368
+ },
369
+ },
370
+ )
371
+ target_entity_id = entity_id if target_phrase in {"当前应用", "当前表单", "自身", "本应用"} else _slugify_identifier(target_phrase, prefix="entity")
372
+ field = _clone_field_blueprint(FIELD_BLUEPRINTS_BY_TYPE[FieldType.relation.value])
373
+ field["target_entity_id"] = target_entity_id
374
+ return field
375
+
376
+
377
+ def _build_subtable_field(*, requirement_text: str) -> dict[str, Any]:
378
+ subfields = _extract_subtable_subfields(requirement_text)
379
+ if not subfields:
380
+ raise RequirementsBuildError(
381
+ "subtable field requires explicit child field definitions; specify the row fields first",
382
+ missing_required_fields=["subtable.subfields"],
383
+ invalid_field_types=["subtable"],
384
+ details={
385
+ "requested_field_type": "subtable",
386
+ "suggested_requirement_hint": "例如:明细表字段包括 子项名称、数量、金额。",
387
+ "suggested_patch": {
388
+ "entities": [
389
+ {
390
+ "fields": [
391
+ {
392
+ "field_id": "detail_items",
393
+ "label": "明细",
394
+ "type": FieldType.subtable.value,
395
+ "subfields": [
396
+ {"field_id": "item_name", "label": "子项名称", "type": FieldType.text.value, "required": True},
397
+ {"field_id": "quantity", "label": "数量", "type": FieldType.number.value},
398
+ {"field_id": "amount", "label": "金额", "type": FieldType.amount.value},
399
+ ],
400
+ }
401
+ ]
402
+ }
403
+ ]
404
+ },
405
+ },
406
+ )
407
+ field = _clone_field_blueprint(FIELD_BLUEPRINTS_BY_TYPE[FieldType.subtable.value])
408
+ field["subfields"] = subfields
409
+ return field
410
+
411
+
412
+ def _extract_relation_target(requirement_text: str) -> str | None:
413
+ explicit_self = ("当前应用", "当前表单", "自身", "本应用")
414
+ if any(token in requirement_text for token in explicit_self):
415
+ return next(token for token in explicit_self if token in requirement_text)
416
+ patterns = (
417
+ r"(?:关联到|关联至|引用到|引用自)\s*([A-Za-z0-9_\-\u4e00-\u9fff]{2,32})",
418
+ r"(?:关联记录目标|关联目标)\s*[::]?\s*([A-Za-z0-9_\-\u4e00-\u9fff]{2,32})",
419
+ )
420
+ for pattern in patterns:
421
+ match = re.search(pattern, requirement_text)
422
+ if not match:
423
+ continue
424
+ candidate = match.group(1).strip()
425
+ if candidate and candidate not in {"记录", "应用", "表单", "单据"}:
426
+ return candidate
427
+ return None
428
+
429
+
430
+ def _extract_subtable_subfields(requirement_text: str) -> list[dict[str, Any]]:
431
+ patterns = (
432
+ r"(?:子表|明细表|明细)(?:字段|列)?(?:包括|包含|为|如下|:|:)\s*([^\n。;;]+)",
433
+ )
434
+ for pattern in patterns:
435
+ match = re.search(pattern, requirement_text)
436
+ if not match:
437
+ continue
438
+ body = match.group(1).strip()
439
+ parts = [part.strip() for part in re.split(r"[,,、/;;]+", body) if part.strip()]
440
+ if len(parts) < 2:
441
+ continue
442
+ subfields: list[dict[str, Any]] = []
443
+ for index, part in enumerate(parts, start=1):
444
+ resolved_types = _resolve_requested_field_types(part)
445
+ field_type = resolved_types[0] if resolved_types else FieldType.text.value
446
+ if field_type in ADVANCED_FIELD_TYPES:
447
+ field_type = FieldType.text.value
448
+ label = re.sub(r"(单行文本|多行文本|数字|金额|日期时间|日期|成员|部门|单选|多选|电话|手机号|邮箱|地址|附件|开关)$", "", part).strip("()() ")
449
+ if not label:
450
+ label = f"子项字段{index}"
451
+ subfield = {
452
+ "field_id": _slugify_identifier(label, prefix=f"sub_{index}"),
453
+ "label": label,
454
+ "type": field_type,
455
+ "required": index == 1,
456
+ }
457
+ if field_type == FieldType.single_select.value:
458
+ subfield["options"] = ["选项A", "选项B", "选项C"]
459
+ if field_type == FieldType.multi_select.value:
460
+ subfield["options"] = ["标签A", "标签B", "标签C"]
461
+ subfields.append(subfield)
462
+ return subfields
463
+ return []
464
+
465
+
466
+ def _build_balanced_layout(fields: list[dict[str, Any]]) -> dict[str, Any]:
467
+ sections: list[dict[str, Any]] = []
468
+ hero_rows: list[dict[str, Any]] = []
469
+ detail_fields: list[str] = []
470
+ coordination_fields: list[str] = []
471
+ contact_fields: list[str] = []
472
+ attachment_fields: list[str] = []
473
+ advanced_fields: list[str] = []
474
+
475
+ for field in fields:
476
+ field_id = str(field["field_id"])
477
+ field_type = str(field["type"])
478
+ if field_id == "title" or field_type == FieldType.long_text.value:
479
+ hero_rows.append({"field_ids": [field_id]})
480
+ continue
481
+ if field_type in {FieldType.phone.value, FieldType.email.value, FieldType.address.value}:
482
+ contact_fields.append(field_id)
483
+ continue
484
+ if field_type in {FieldType.attachment.value}:
485
+ attachment_fields.append(field_id)
486
+ continue
487
+ if field_type in {FieldType.relation.value, FieldType.subtable.value}:
488
+ advanced_fields.append(field_id)
489
+ continue
490
+ if field_type in {FieldType.member.value, FieldType.department.value, FieldType.single_select.value, FieldType.multi_select.value, FieldType.boolean.value}:
491
+ coordination_fields.append(field_id)
492
+ continue
493
+ detail_fields.append(field_id)
494
+
495
+ if hero_rows:
496
+ sections.append({"section_id": "hero", "title": "基础信息", "rows": hero_rows})
497
+ if detail_fields:
498
+ sections.append({"section_id": "details", "title": "业务字段", "rows": _pair_rows(detail_fields)})
499
+ if coordination_fields:
500
+ sections.append({"section_id": "coordination", "title": "协作信息", "rows": _pair_rows(coordination_fields)})
501
+ if contact_fields:
502
+ sections.append({"section_id": "contact", "title": "联系信息", "rows": _pair_rows(contact_fields)})
503
+ if attachment_fields:
504
+ sections.append({"section_id": "attachments", "title": "附件与扩展", "rows": [{"field_ids": [field_id]} for field_id in attachment_fields]})
505
+ if advanced_fields:
506
+ sections.append({"section_id": "advanced", "title": "高级字段", "rows": [{"field_ids": [field_id]} for field_id in advanced_fields]})
507
+ if not sections:
508
+ return {"sections": [{"section_id": "basic", "title": "基础信息", "rows": _pair_rows([str(field["field_id"]) for field in fields])}]}
509
+ return {"sections": sections}
510
+
511
+
512
+ def _pair_rows(field_ids: list[str]) -> list[dict[str, Any]]:
513
+ return [{"field_ids": field_ids[index : index + 2]} for index in range(0, len(field_ids), 2)]
514
+
515
+
516
+ def _clone_field_blueprint(blueprint: dict[str, Any]) -> dict[str, Any]:
517
+ field = {
518
+ "field_id": blueprint["field_id"],
519
+ "label": blueprint["label"],
520
+ "type": blueprint["type"],
521
+ "required": bool(blueprint.get("required", False)),
522
+ }
523
+ if blueprint.get("options"):
524
+ field["options"] = list(blueprint["options"])
525
+ if blueprint.get("target_field_id"):
526
+ field["target_field_id"] = blueprint["target_field_id"]
527
+ if blueprint.get("subfields"):
528
+ field["subfields"] = [_clone_field_blueprint(subfield) for subfield in blueprint["subfields"]]
529
+ return field
530
+
531
+
532
+ def _slugify_identifier(value: str, *, prefix: str) -> str:
533
+ normalized = re.sub(r"[^a-zA-Z0-9]+", "_", value).strip("_").lower()
534
+ if normalized:
535
+ return normalized[:48]
536
+ return f"{prefix}_{uuid4().hex[:8]}"