@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.
- package/README.md +30 -0
- package/docs/local-agent-install.md +235 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +204 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +547 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +985 -0
- package/src/qingflow_mcp/builder_facade/service.py +8243 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +184 -0
- package/src/qingflow_mcp/cli/commands/common.py +47 -0
- package/src/qingflow_mcp/cli/commands/imports.py +86 -0
- package/src/qingflow_mcp/cli/commands/record.py +202 -0
- package/src/qingflow_mcp/cli/commands/task.py +87 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +269 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +147 -0
- package/src/qingflow_mcp/config.py +221 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/server.py +211 -0
- package/src/qingflow_mcp/server_app_builder.py +387 -0
- package/src/qingflow_mcp/server_app_user.py +317 -0
- package/src/qingflow_mcp/session_store.py +289 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2339 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +853 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
- package/src/qingflow_mcp/tools/app_tools.py +850 -0
- package/src/qingflow_mcp/tools/approval_tools.py +833 -0
- package/src/qingflow_mcp/tools/auth_tools.py +697 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
- package/src/qingflow_mcp/tools/directory_tools.py +648 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +385 -0
- package/src/qingflow_mcp/tools/import_tools.py +1971 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +240 -0
- package/src/qingflow_mcp/tools/portal_tools.py +131 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
- package/src/qingflow_mcp/tools/record_tools.py +12739 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
- package/src/qingflow_mcp/tools/task_tools.py +843 -0
- package/src/qingflow_mcp/tools/view_tools.py +280 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
- 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]}"
|