@josephyan/qingflow-app-user-mcp 0.2.0-beta.1 → 0.2.0-beta.11
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 +11 -2
- package/npm/lib/runtime.mjs +37 -0
- package/npm/scripts/postinstall.mjs +5 -1
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +158 -0
- package/skills/qingflow-app-user/agents/openai.yaml +4 -0
- package/skills/qingflow-app-user/references/data-gotchas.md +49 -0
- package/skills/qingflow-app-user/references/environments.md +63 -0
- package/skills/qingflow-app-user/references/record-patterns.md +69 -0
- package/skills/qingflow-app-user/references/workflow-usage.md +24 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +242 -0
- package/src/qingflow_mcp/builder_facade/service.py +2055 -195
- package/src/qingflow_mcp/server_app_builder.py +82 -4
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +1 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +21 -2
- package/src/qingflow_mcp/solution/executor.py +34 -7
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1001 -30
- package/src/qingflow_mcp/tools/app_tools.py +40 -2
- package/src/qingflow_mcp/tools/auth_tools.py +2 -1
- package/src/qingflow_mcp/tools/workflow_tools.py +78 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +6 -1
|
@@ -43,11 +43,30 @@ FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
|
|
|
43
43
|
"departments": PublicFieldType.department,
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
FIELD_TYPE_ID_ALIASES: dict[int, PublicFieldType] = {
|
|
47
|
+
2: PublicFieldType.text,
|
|
48
|
+
3: PublicFieldType.long_text,
|
|
49
|
+
4: PublicFieldType.date,
|
|
50
|
+
5: PublicFieldType.member,
|
|
51
|
+
6: PublicFieldType.email,
|
|
52
|
+
7: PublicFieldType.phone,
|
|
53
|
+
8: PublicFieldType.number,
|
|
54
|
+
10: PublicFieldType.boolean,
|
|
55
|
+
11: PublicFieldType.single_select,
|
|
56
|
+
12: PublicFieldType.multi_select,
|
|
57
|
+
13: PublicFieldType.attachment,
|
|
58
|
+
18: PublicFieldType.subtable,
|
|
59
|
+
21: PublicFieldType.address,
|
|
60
|
+
22: PublicFieldType.department,
|
|
61
|
+
25: PublicFieldType.relation,
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
|
|
47
65
|
class PublicViewType(str, Enum):
|
|
48
66
|
table = "table"
|
|
49
67
|
card = "card"
|
|
50
68
|
board = "board"
|
|
69
|
+
gantt = "gantt"
|
|
51
70
|
|
|
52
71
|
|
|
53
72
|
class LayoutApplyMode(str, Enum):
|
|
@@ -69,6 +88,7 @@ class FlowPreset(str, Enum):
|
|
|
69
88
|
class ViewsPreset(str, Enum):
|
|
70
89
|
default_table = "default_table"
|
|
71
90
|
status_board = "status_board"
|
|
91
|
+
default_gantt = "default_gantt"
|
|
72
92
|
|
|
73
93
|
|
|
74
94
|
class PublicFlowNodeType(str, Enum):
|
|
@@ -82,6 +102,143 @@ class PublicFlowNodeType(str, Enum):
|
|
|
82
102
|
end = "end"
|
|
83
103
|
|
|
84
104
|
|
|
105
|
+
class FlowConditionOperator(str, Enum):
|
|
106
|
+
eq = "eq"
|
|
107
|
+
neq = "neq"
|
|
108
|
+
in_ = "in"
|
|
109
|
+
contains = "contains"
|
|
110
|
+
gte = "gte"
|
|
111
|
+
lte = "lte"
|
|
112
|
+
is_empty = "is_empty"
|
|
113
|
+
not_empty = "not_empty"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ViewFilterOperator(str, Enum):
|
|
117
|
+
eq = "eq"
|
|
118
|
+
neq = "neq"
|
|
119
|
+
in_ = "in"
|
|
120
|
+
contains = "contains"
|
|
121
|
+
gte = "gte"
|
|
122
|
+
lte = "lte"
|
|
123
|
+
is_empty = "is_empty"
|
|
124
|
+
not_empty = "not_empty"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class FlowAssigneePatch(StrictModel):
|
|
128
|
+
role_ids: list[int] = Field(default_factory=list)
|
|
129
|
+
role_names: list[str] = Field(default_factory=list)
|
|
130
|
+
member_uids: list[int] = Field(default_factory=list)
|
|
131
|
+
member_emails: list[str] = Field(default_factory=list)
|
|
132
|
+
member_names: list[str] = Field(default_factory=list)
|
|
133
|
+
include_sub_departs: bool | None = None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class FlowNodePermissionsPatch(StrictModel):
|
|
137
|
+
editable_fields: list[str] = Field(default_factory=list)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class FlowConditionRulePatch(StrictModel):
|
|
141
|
+
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
142
|
+
operator: FlowConditionOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
143
|
+
values: list[Any] = Field(default_factory=list)
|
|
144
|
+
|
|
145
|
+
@model_validator(mode="before")
|
|
146
|
+
@classmethod
|
|
147
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
148
|
+
if not isinstance(value, dict):
|
|
149
|
+
return value
|
|
150
|
+
payload = dict(value)
|
|
151
|
+
if "value" in payload and "values" not in payload:
|
|
152
|
+
payload["values"] = [payload.pop("value")]
|
|
153
|
+
raw_operator = payload.get("operator", payload.get("op"))
|
|
154
|
+
if isinstance(raw_operator, str):
|
|
155
|
+
normalized = raw_operator.strip().lower()
|
|
156
|
+
operator_aliases = {
|
|
157
|
+
"equals": FlowConditionOperator.eq.value,
|
|
158
|
+
"equal": FlowConditionOperator.eq.value,
|
|
159
|
+
"=": FlowConditionOperator.eq.value,
|
|
160
|
+
"not_equals": FlowConditionOperator.neq.value,
|
|
161
|
+
"not_equal": FlowConditionOperator.neq.value,
|
|
162
|
+
"!=": FlowConditionOperator.neq.value,
|
|
163
|
+
">=": FlowConditionOperator.gte.value,
|
|
164
|
+
"<=": FlowConditionOperator.lte.value,
|
|
165
|
+
"any_of": FlowConditionOperator.in_.value,
|
|
166
|
+
"one_of": FlowConditionOperator.in_.value,
|
|
167
|
+
"between_any": FlowConditionOperator.in_.value,
|
|
168
|
+
"empty": FlowConditionOperator.is_empty.value,
|
|
169
|
+
"is blank": FlowConditionOperator.is_empty.value,
|
|
170
|
+
"blank": FlowConditionOperator.is_empty.value,
|
|
171
|
+
"not_empty": FlowConditionOperator.not_empty.value,
|
|
172
|
+
"not blank": FlowConditionOperator.not_empty.value,
|
|
173
|
+
}
|
|
174
|
+
if normalized in operator_aliases:
|
|
175
|
+
payload["operator"] = operator_aliases[normalized]
|
|
176
|
+
elif "operator" not in payload:
|
|
177
|
+
payload["operator"] = normalized
|
|
178
|
+
payload.pop("op", None)
|
|
179
|
+
return payload
|
|
180
|
+
|
|
181
|
+
@model_validator(mode="after")
|
|
182
|
+
def validate_shape(self) -> "FlowConditionRulePatch":
|
|
183
|
+
if self.operator in {FlowConditionOperator.is_empty, FlowConditionOperator.not_empty}:
|
|
184
|
+
self.values = []
|
|
185
|
+
return self
|
|
186
|
+
if not self.values:
|
|
187
|
+
raise ValueError("condition rule requires values")
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ViewFilterRulePatch(StrictModel):
|
|
192
|
+
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
193
|
+
operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
194
|
+
values: list[Any] = Field(default_factory=list)
|
|
195
|
+
|
|
196
|
+
@model_validator(mode="before")
|
|
197
|
+
@classmethod
|
|
198
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
199
|
+
if not isinstance(value, dict):
|
|
200
|
+
return value
|
|
201
|
+
payload = dict(value)
|
|
202
|
+
if "value" in payload and "values" not in payload:
|
|
203
|
+
payload["values"] = [payload.pop("value")]
|
|
204
|
+
raw_operator = payload.get("operator", payload.get("op"))
|
|
205
|
+
if isinstance(raw_operator, str):
|
|
206
|
+
normalized = raw_operator.strip().lower()
|
|
207
|
+
operator_aliases = {
|
|
208
|
+
"equals": ViewFilterOperator.eq.value,
|
|
209
|
+
"equal": ViewFilterOperator.eq.value,
|
|
210
|
+
"=": ViewFilterOperator.eq.value,
|
|
211
|
+
"not_equals": ViewFilterOperator.neq.value,
|
|
212
|
+
"not_equal": ViewFilterOperator.neq.value,
|
|
213
|
+
"!=": ViewFilterOperator.neq.value,
|
|
214
|
+
">=": ViewFilterOperator.gte.value,
|
|
215
|
+
"<=": ViewFilterOperator.lte.value,
|
|
216
|
+
"any_of": ViewFilterOperator.in_.value,
|
|
217
|
+
"one_of": ViewFilterOperator.in_.value,
|
|
218
|
+
"between_any": ViewFilterOperator.in_.value,
|
|
219
|
+
"empty": ViewFilterOperator.is_empty.value,
|
|
220
|
+
"is blank": ViewFilterOperator.is_empty.value,
|
|
221
|
+
"blank": ViewFilterOperator.is_empty.value,
|
|
222
|
+
"not_empty": ViewFilterOperator.not_empty.value,
|
|
223
|
+
"not blank": ViewFilterOperator.not_empty.value,
|
|
224
|
+
}
|
|
225
|
+
if normalized in operator_aliases:
|
|
226
|
+
payload["operator"] = operator_aliases[normalized]
|
|
227
|
+
elif "operator" not in payload:
|
|
228
|
+
payload["operator"] = normalized
|
|
229
|
+
payload.pop("op", None)
|
|
230
|
+
return payload
|
|
231
|
+
|
|
232
|
+
@model_validator(mode="after")
|
|
233
|
+
def validate_shape(self) -> "ViewFilterRulePatch":
|
|
234
|
+
if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
|
|
235
|
+
self.values = []
|
|
236
|
+
return self
|
|
237
|
+
if not self.values:
|
|
238
|
+
raise ValueError("view filter rule requires values")
|
|
239
|
+
return self
|
|
240
|
+
|
|
241
|
+
|
|
85
242
|
class FieldSelector(StrictModel):
|
|
86
243
|
field_id: str | None = None
|
|
87
244
|
que_id: int | None = None
|
|
@@ -179,8 +336,54 @@ class FlowNodePatch(StrictModel):
|
|
|
179
336
|
id: str
|
|
180
337
|
type: PublicFlowNodeType
|
|
181
338
|
name: str
|
|
339
|
+
assignees: FlowAssigneePatch = Field(default_factory=FlowAssigneePatch)
|
|
340
|
+
permissions: FlowNodePermissionsPatch = Field(default_factory=FlowNodePermissionsPatch)
|
|
341
|
+
conditions: list[FlowConditionRulePatch] = Field(default_factory=list)
|
|
342
|
+
condition_groups: list[list[FlowConditionRulePatch]] = Field(default_factory=list)
|
|
182
343
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
183
344
|
|
|
345
|
+
@model_validator(mode="before")
|
|
346
|
+
@classmethod
|
|
347
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
348
|
+
if not isinstance(value, dict):
|
|
349
|
+
return value
|
|
350
|
+
payload = dict(value)
|
|
351
|
+
assignees = dict(payload.get("assignees") or {})
|
|
352
|
+
permissions = dict(payload.get("permissions") or {})
|
|
353
|
+
|
|
354
|
+
for key in ("role_ids", "role_names", "member_uids", "member_emails", "member_names", "include_sub_departs"):
|
|
355
|
+
if key in payload and key not in assignees:
|
|
356
|
+
assignees[key] = payload.pop(key)
|
|
357
|
+
for key in ("editable_fields",):
|
|
358
|
+
if key in payload and key not in permissions:
|
|
359
|
+
permissions[key] = payload.pop(key)
|
|
360
|
+
if "filters" in payload and "conditions" not in payload:
|
|
361
|
+
payload["conditions"] = payload.pop("filters")
|
|
362
|
+
if "rules" in payload and "conditions" not in payload:
|
|
363
|
+
payload["conditions"] = payload.pop("rules")
|
|
364
|
+
if "conditionRules" in payload and "condition_groups" not in payload:
|
|
365
|
+
payload["condition_groups"] = payload.pop("conditionRules")
|
|
366
|
+
if "conditionGroups" in payload and "condition_groups" not in payload:
|
|
367
|
+
payload["condition_groups"] = payload.pop("conditionGroups")
|
|
368
|
+
if "owners" in payload and "member_names" not in assignees:
|
|
369
|
+
assignees["member_names"] = payload.pop("owners")
|
|
370
|
+
if "approvers" in payload and "role_names" not in assignees:
|
|
371
|
+
assignees["role_names"] = payload.pop("approvers")
|
|
372
|
+
if assignees:
|
|
373
|
+
payload["assignees"] = assignees
|
|
374
|
+
if permissions:
|
|
375
|
+
payload["permissions"] = permissions
|
|
376
|
+
return payload
|
|
377
|
+
|
|
378
|
+
@model_validator(mode="after")
|
|
379
|
+
def validate_branch_conditions(self) -> "FlowNodePatch":
|
|
380
|
+
if self.conditions:
|
|
381
|
+
self.condition_groups = [list(self.conditions), *self.condition_groups]
|
|
382
|
+
self.conditions = []
|
|
383
|
+
if self.type != PublicFlowNodeType.condition and self.condition_groups:
|
|
384
|
+
raise ValueError("condition_groups are only allowed on condition nodes")
|
|
385
|
+
return self
|
|
386
|
+
|
|
184
387
|
|
|
185
388
|
class FlowTransitionPatch(StrictModel):
|
|
186
389
|
source: str = Field(alias="from")
|
|
@@ -192,6 +395,10 @@ class ViewUpsertPatch(StrictModel):
|
|
|
192
395
|
type: PublicViewType
|
|
193
396
|
columns: list[str] = Field(default_factory=list)
|
|
194
397
|
group_by: str | None = None
|
|
398
|
+
filters: list[ViewFilterRulePatch] = Field(default_factory=list)
|
|
399
|
+
start_field: str | None = Field(default=None, validation_alias=AliasChoices("start_field", "startField"))
|
|
400
|
+
end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
|
|
401
|
+
title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
|
|
195
402
|
|
|
196
403
|
@model_validator(mode="before")
|
|
197
404
|
@classmethod
|
|
@@ -201,6 +408,14 @@ class ViewUpsertPatch(StrictModel):
|
|
|
201
408
|
payload = dict(value)
|
|
202
409
|
if "fields" in payload and "columns" not in payload:
|
|
203
410
|
payload["columns"] = payload.pop("fields")
|
|
411
|
+
if "column_names" in payload and "columns" not in payload:
|
|
412
|
+
payload["columns"] = payload.pop("column_names")
|
|
413
|
+
if "columnNames" in payload and "columns" not in payload:
|
|
414
|
+
payload["columns"] = payload.pop("columnNames")
|
|
415
|
+
if "filter_rules" in payload and "filters" not in payload:
|
|
416
|
+
payload["filters"] = payload.pop("filter_rules")
|
|
417
|
+
if "filterRules" in payload and "filters" not in payload:
|
|
418
|
+
payload["filters"] = payload.pop("filterRules")
|
|
204
419
|
raw_type = payload.get("type")
|
|
205
420
|
if isinstance(raw_type, str):
|
|
206
421
|
normalized = raw_type.strip().lower()
|
|
@@ -210,6 +425,8 @@ class ViewUpsertPatch(StrictModel):
|
|
|
210
425
|
payload["type"] = "card"
|
|
211
426
|
elif normalized == "kanban":
|
|
212
427
|
payload["type"] = "board"
|
|
428
|
+
elif normalized == "ganttview":
|
|
429
|
+
payload["type"] = "gantt"
|
|
213
430
|
return payload
|
|
214
431
|
|
|
215
432
|
@model_validator(mode="after")
|
|
@@ -218,6 +435,8 @@ class ViewUpsertPatch(StrictModel):
|
|
|
218
435
|
raise ValueError("table/card views require columns")
|
|
219
436
|
if self.type == PublicViewType.board and not self.group_by:
|
|
220
437
|
raise ValueError("board view requires group_by")
|
|
438
|
+
if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
|
|
439
|
+
raise ValueError("gantt view requires start_field and end_field")
|
|
221
440
|
return self
|
|
222
441
|
|
|
223
442
|
|
|
@@ -303,6 +522,22 @@ class FlowPlanRequest(StrictModel):
|
|
|
303
522
|
payload = dict(value)
|
|
304
523
|
if str(payload.get("mode") or "").strip().lower() == "overwrite":
|
|
305
524
|
payload["mode"] = "replace"
|
|
525
|
+
raw_preset = payload.get("preset")
|
|
526
|
+
if raw_preset is None and isinstance(payload.get("base_preset"), str):
|
|
527
|
+
raw_preset = payload["base_preset"]
|
|
528
|
+
payload["preset"] = raw_preset
|
|
529
|
+
if isinstance(raw_preset, str):
|
|
530
|
+
normalized_preset = raw_preset.strip().lower()
|
|
531
|
+
preset_aliases = {
|
|
532
|
+
"default_approval": FlowPreset.basic_approval.value,
|
|
533
|
+
"approval": FlowPreset.basic_approval.value,
|
|
534
|
+
"basic approval": FlowPreset.basic_approval.value,
|
|
535
|
+
"default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
536
|
+
"default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
|
|
537
|
+
"fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
538
|
+
}
|
|
539
|
+
if normalized_preset in preset_aliases:
|
|
540
|
+
payload["preset"] = preset_aliases[normalized_preset]
|
|
306
541
|
return payload
|
|
307
542
|
|
|
308
543
|
|
|
@@ -332,7 +567,14 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
332
567
|
if not isinstance(value, dict):
|
|
333
568
|
return value
|
|
334
569
|
payload = dict(value)
|
|
570
|
+
if "fields" in payload and "subfields" not in payload:
|
|
571
|
+
payload["subfields"] = payload.pop("fields")
|
|
335
572
|
raw_type = payload.get("type")
|
|
573
|
+
if isinstance(raw_type, int):
|
|
574
|
+
normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
|
|
575
|
+
if normalized_from_id is not None:
|
|
576
|
+
payload["type"] = normalized_from_id.value
|
|
577
|
+
return payload
|
|
336
578
|
if isinstance(raw_type, str):
|
|
337
579
|
normalized = FIELD_TYPE_ALIASES.get(raw_type.strip().lower())
|
|
338
580
|
if normalized is not None:
|