@qingflow-tech/qingflow-app-builder-mcp 1.0.12 → 1.0.14
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +11 -0
- package/src/qingflow_mcp/builder_facade/service.py +26 -0
- package/src/qingflow_mcp/cli/commands/record.py +52 -8
- package/src/qingflow_mcp/response_trim.py +6 -0
- package/src/qingflow_mcp/server_app_user.py +4 -0
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -1
- package/src/qingflow_mcp/tools/export_tools.py +14 -1
- package/src/qingflow_mcp/tools/import_tools.py +43 -9
- package/src/qingflow_mcp/tools/record_tools.py +25 -11
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.14
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.14 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -1610,6 +1610,7 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1610
1610
|
if not isinstance(value, dict):
|
|
1611
1611
|
return value
|
|
1612
1612
|
payload = dict(value)
|
|
1613
|
+
default_target_app_key = str(payload.get("app_key", payload.get("appKey", "")) or "").strip()
|
|
1613
1614
|
if "upsertResources" in payload and "upsert_resources" not in payload:
|
|
1614
1615
|
payload["upsert_resources"] = payload.pop("upsertResources")
|
|
1615
1616
|
if "patchResources" in payload and "patch_resources" not in payload:
|
|
@@ -1622,6 +1623,16 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1622
1623
|
payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
|
|
1623
1624
|
if "viewConfigs" in payload and "view_configs" not in payload:
|
|
1624
1625
|
payload["view_configs"] = payload.pop("viewConfigs")
|
|
1626
|
+
if default_target_app_key and isinstance(payload.get("upsert_resources"), list):
|
|
1627
|
+
normalized_resources = []
|
|
1628
|
+
for item in payload["upsert_resources"]:
|
|
1629
|
+
if isinstance(item, dict) and not any(
|
|
1630
|
+
str(item.get(key) or "").strip()
|
|
1631
|
+
for key in ("target_app_key", "targetAppKey", "app_key", "appKey")
|
|
1632
|
+
):
|
|
1633
|
+
item = {**item, "target_app_key": default_target_app_key}
|
|
1634
|
+
normalized_resources.append(item)
|
|
1635
|
+
payload["upsert_resources"] = normalized_resources
|
|
1625
1636
|
return payload
|
|
1626
1637
|
|
|
1627
1638
|
@model_validator(mode="after")
|
|
@@ -17721,8 +17721,23 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
17721
17721
|
field["config"] = deepcopy(payload["code_block_config"])
|
|
17722
17722
|
question_rebuild_required = True
|
|
17723
17723
|
if "code_block_binding" in payload:
|
|
17724
|
+
existing_code_block_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {})
|
|
17725
|
+
next_code_block_binding = _normalize_code_block_binding(payload["code_block_binding"])
|
|
17724
17726
|
field["code_block_binding"] = payload["code_block_binding"]
|
|
17725
17727
|
field["_explicit_code_block_binding"] = True
|
|
17728
|
+
if (
|
|
17729
|
+
"code_block_config" not in payload
|
|
17730
|
+
and existing_code_block_config is not None
|
|
17731
|
+
and next_code_block_binding is not None
|
|
17732
|
+
):
|
|
17733
|
+
existing_code = _normalize_code_block_output_assignment(
|
|
17734
|
+
_strip_code_block_generated_input_prelude(str(existing_code_block_config.get("code_content") or ""))
|
|
17735
|
+
)
|
|
17736
|
+
next_code = _normalize_code_block_output_assignment(str(next_code_block_binding.get("code") or ""))
|
|
17737
|
+
if existing_code != next_code:
|
|
17738
|
+
existing_code_block_config["code_content"] = next_code
|
|
17739
|
+
field["code_block_config"] = existing_code_block_config
|
|
17740
|
+
field["config"] = deepcopy(existing_code_block_config)
|
|
17726
17741
|
question_rebuild_required = True
|
|
17727
17742
|
if "auto_trigger" in payload:
|
|
17728
17743
|
field["auto_trigger"] = payload["auto_trigger"]
|
|
@@ -18228,6 +18243,15 @@ def _compile_code_block_binding_fields(
|
|
|
18228
18243
|
"alias_id": _coerce_positive_int(output_item.get("alias_id")),
|
|
18229
18244
|
}
|
|
18230
18245
|
)
|
|
18246
|
+
current_field_refs: set[int] = set()
|
|
18247
|
+
for field in next_fields:
|
|
18248
|
+
if not isinstance(field, dict):
|
|
18249
|
+
continue
|
|
18250
|
+
field_ref = _coerce_positive_int(field.get("que_id"))
|
|
18251
|
+
if field_ref is None:
|
|
18252
|
+
field_ref = _coerce_any_int(field.get("que_temp_id"))
|
|
18253
|
+
if field_ref is not None:
|
|
18254
|
+
current_field_refs.add(field_ref)
|
|
18231
18255
|
carried_relations: list[dict[str, Any]] = []
|
|
18232
18256
|
for relation in existing_relations:
|
|
18233
18257
|
if not isinstance(relation, dict):
|
|
@@ -18238,6 +18262,8 @@ def _compile_code_block_binding_fields(
|
|
|
18238
18262
|
alias_config = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
|
|
18239
18263
|
source_ref = _coerce_positive_int(alias_config.get("queId"))
|
|
18240
18264
|
target_ref = _coerce_positive_int(relation.get("queId"))
|
|
18265
|
+
if source_ref not in current_field_refs or target_ref not in current_field_refs:
|
|
18266
|
+
continue
|
|
18241
18267
|
if (source_ref is not None and source_ref in affected_source_refs) or (target_ref is not None and target_ref in affected_target_refs):
|
|
18242
18268
|
continue
|
|
18243
18269
|
carried_relations.append(deepcopy(relation))
|
|
@@ -80,11 +80,33 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
80
80
|
|
|
81
81
|
list_parser = record_subparsers.add_parser("list", help="列出记录")
|
|
82
82
|
list_parser.add_argument("--app-key", required=True)
|
|
83
|
-
list_parser.add_argument(
|
|
84
|
-
|
|
83
|
+
list_parser.add_argument(
|
|
84
|
+
"--column",
|
|
85
|
+
dest="columns",
|
|
86
|
+
action="append",
|
|
87
|
+
type=int,
|
|
88
|
+
default=[],
|
|
89
|
+
metavar="FIELD_ID",
|
|
90
|
+
help="只返回这些 field_id;可重复传",
|
|
91
|
+
)
|
|
92
|
+
list_parser.add_argument(
|
|
93
|
+
"--columns-file",
|
|
94
|
+
help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
|
|
95
|
+
)
|
|
85
96
|
list_parser.add_argument("--query")
|
|
86
|
-
list_parser.add_argument(
|
|
87
|
-
|
|
97
|
+
list_parser.add_argument(
|
|
98
|
+
"--query-field",
|
|
99
|
+
dest="query_fields",
|
|
100
|
+
action="append",
|
|
101
|
+
type=int,
|
|
102
|
+
default=[],
|
|
103
|
+
metavar="FIELD_ID",
|
|
104
|
+
help="全文搜索范围 field_id;可重复传",
|
|
105
|
+
)
|
|
106
|
+
list_parser.add_argument(
|
|
107
|
+
"--query-fields-file",
|
|
108
|
+
help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
|
|
109
|
+
)
|
|
88
110
|
list_parser.add_argument("--where-file")
|
|
89
111
|
list_parser.add_argument("--order-by-file")
|
|
90
112
|
list_parser.add_argument("--page", type=int, default=1)
|
|
@@ -96,8 +118,19 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
96
118
|
|
|
97
119
|
access_parser = record_subparsers.add_parser("access", help="访问记录并写入本地 CSV 分片")
|
|
98
120
|
access_parser.add_argument("--app-key", required=True)
|
|
99
|
-
access_parser.add_argument(
|
|
100
|
-
|
|
121
|
+
access_parser.add_argument(
|
|
122
|
+
"--column",
|
|
123
|
+
dest="columns",
|
|
124
|
+
action="append",
|
|
125
|
+
type=int,
|
|
126
|
+
default=[],
|
|
127
|
+
metavar="FIELD_ID",
|
|
128
|
+
help="导出这些 field_id 到 CSV;可重复传",
|
|
129
|
+
)
|
|
130
|
+
access_parser.add_argument(
|
|
131
|
+
"--columns-file",
|
|
132
|
+
help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
|
|
133
|
+
)
|
|
101
134
|
access_parser.add_argument("--where-file")
|
|
102
135
|
access_parser.add_argument("--order-by-file")
|
|
103
136
|
access_parser.add_argument("--view-id", required=True)
|
|
@@ -106,8 +139,19 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
106
139
|
get = record_subparsers.add_parser("get", help="读取单条记录")
|
|
107
140
|
get.add_argument("--app-key", required=True)
|
|
108
141
|
get.add_argument("--record-id", required=True)
|
|
109
|
-
get.add_argument(
|
|
110
|
-
|
|
142
|
+
get.add_argument(
|
|
143
|
+
"--column",
|
|
144
|
+
dest="columns",
|
|
145
|
+
action="append",
|
|
146
|
+
type=int,
|
|
147
|
+
default=[],
|
|
148
|
+
metavar="FIELD_ID",
|
|
149
|
+
help="聚焦这些 field_id;可重复传",
|
|
150
|
+
)
|
|
151
|
+
get.add_argument(
|
|
152
|
+
"--columns-file",
|
|
153
|
+
help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
|
|
154
|
+
)
|
|
111
155
|
get.add_argument("--view-id")
|
|
112
156
|
get.set_defaults(handler=_handle_get, format_hint="record_get")
|
|
113
157
|
|
|
@@ -918,6 +918,12 @@ def _compact_import_column(item: dict[str, Any]) -> dict[str, Any]:
|
|
|
918
918
|
compact["options"] = options
|
|
919
919
|
if bool(item.get("accepts_natural_input")):
|
|
920
920
|
compact["accepts_natural_input"] = True
|
|
921
|
+
import_value_format = item.get("import_value_format")
|
|
922
|
+
if isinstance(import_value_format, str) and import_value_format:
|
|
923
|
+
compact["import_value_format"] = import_value_format
|
|
924
|
+
format_hint = item.get("format_hint")
|
|
925
|
+
if isinstance(format_hint, str) and format_hint:
|
|
926
|
+
compact["format_hint"] = format_hint
|
|
921
927
|
if bool(item.get("requires_upload")):
|
|
922
928
|
compact["requires_upload"] = True
|
|
923
929
|
target_app_key = item.get("target_app_key")
|
|
@@ -269,6 +269,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
269
269
|
columns: list[dict | int] | None = None,
|
|
270
270
|
where: list[dict] | None = None,
|
|
271
271
|
order_by: list[dict] | None = None,
|
|
272
|
+
record_id: str | int | None = None,
|
|
272
273
|
record_ids: list[str | int] | None = None,
|
|
273
274
|
include_workflow_log: bool = False,
|
|
274
275
|
) -> dict:
|
|
@@ -279,6 +280,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
279
280
|
columns=columns or [],
|
|
280
281
|
where=where or [],
|
|
281
282
|
order_by=order_by or [],
|
|
283
|
+
record_id=record_id,
|
|
282
284
|
record_ids=record_ids or [],
|
|
283
285
|
include_workflow_log=include_workflow_log,
|
|
284
286
|
)
|
|
@@ -310,6 +312,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
310
312
|
columns: list[dict | int] | None = None,
|
|
311
313
|
where: list[dict] | None = None,
|
|
312
314
|
order_by: list[dict] | None = None,
|
|
315
|
+
record_id: str | int | None = None,
|
|
313
316
|
record_ids: list[str | int] | None = None,
|
|
314
317
|
include_workflow_log: bool = False,
|
|
315
318
|
download_to_path: str | None = None,
|
|
@@ -322,6 +325,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
322
325
|
columns=columns or [],
|
|
323
326
|
where=where or [],
|
|
324
327
|
order_by=order_by or [],
|
|
328
|
+
record_id=record_id,
|
|
325
329
|
record_ids=record_ids or [],
|
|
326
330
|
include_workflow_log=include_workflow_log,
|
|
327
331
|
download_to_path=download_to_path,
|
|
@@ -176,17 +176,21 @@ class SessionStore:
|
|
|
176
176
|
return memory_session
|
|
177
177
|
if not session_profile:
|
|
178
178
|
return None
|
|
179
|
-
token =
|
|
180
|
-
if not token:
|
|
181
|
-
token =
|
|
179
|
+
token = session_profile.token
|
|
180
|
+
if not token and session_profile.persisted:
|
|
181
|
+
token = self._get_secret(self._token_key(profile))
|
|
182
182
|
if not token:
|
|
183
183
|
return None
|
|
184
|
-
login_token =
|
|
185
|
-
|
|
184
|
+
login_token = session_profile.login_token
|
|
185
|
+
if not login_token and session_profile.persisted:
|
|
186
|
+
login_token = self._get_secret(self._login_token_key(profile))
|
|
187
|
+
credential = session_profile.credential
|
|
188
|
+
if not credential and session_profile.persisted:
|
|
189
|
+
credential = self._get_secret(self._credential_key(profile))
|
|
186
190
|
backend_session = BackendSession(
|
|
187
191
|
token=token,
|
|
188
|
-
login_token=login_token
|
|
189
|
-
credential=credential
|
|
192
|
+
login_token=login_token,
|
|
193
|
+
credential=credential,
|
|
190
194
|
profile=profile,
|
|
191
195
|
base_url=session_profile.base_url,
|
|
192
196
|
qf_version=session_profile.qf_version,
|
|
@@ -104,11 +104,12 @@ class CodeBlockTools(RecordTools):
|
|
|
104
104
|
|
|
105
105
|
@mcp.tool()
|
|
106
106
|
def record_code_block_schema_get(
|
|
107
|
+
profile: str = DEFAULT_PROFILE,
|
|
107
108
|
app_key: str = "",
|
|
108
109
|
output_profile: str = "normal",
|
|
109
110
|
) -> JSONObject:
|
|
110
111
|
return self.record_code_block_schema_get_public(
|
|
111
|
-
profile=
|
|
112
|
+
profile=profile,
|
|
112
113
|
app_key=app_key,
|
|
113
114
|
output_profile=output_profile,
|
|
114
115
|
)
|
|
@@ -65,6 +65,7 @@ class ExportTools(ToolBase):
|
|
|
65
65
|
columns: list[JSONObject | int] | None = None,
|
|
66
66
|
where: list[JSONObject] | None = None,
|
|
67
67
|
order_by: list[JSONObject] | None = None,
|
|
68
|
+
record_id: str | int | None = None,
|
|
68
69
|
record_ids: list[str | int] | None = None,
|
|
69
70
|
include_workflow_log: bool = False,
|
|
70
71
|
) -> dict[str, Any]:
|
|
@@ -75,6 +76,7 @@ class ExportTools(ToolBase):
|
|
|
75
76
|
columns=columns or [],
|
|
76
77
|
where=where or [],
|
|
77
78
|
order_by=order_by or [],
|
|
79
|
+
record_id=record_id,
|
|
78
80
|
record_ids=record_ids or [],
|
|
79
81
|
include_workflow_log=include_workflow_log,
|
|
80
82
|
)
|
|
@@ -109,6 +111,7 @@ class ExportTools(ToolBase):
|
|
|
109
111
|
columns: list[JSONObject | int] | None = None,
|
|
110
112
|
where: list[JSONObject] | None = None,
|
|
111
113
|
order_by: list[JSONObject] | None = None,
|
|
114
|
+
record_id: str | int | None = None,
|
|
112
115
|
record_ids: list[str | int] | None = None,
|
|
113
116
|
include_workflow_log: bool = False,
|
|
114
117
|
download_to_path: str | None = None,
|
|
@@ -121,6 +124,7 @@ class ExportTools(ToolBase):
|
|
|
121
124
|
columns=columns or [],
|
|
122
125
|
where=where or [],
|
|
123
126
|
order_by=order_by or [],
|
|
127
|
+
record_id=record_id,
|
|
124
128
|
record_ids=record_ids or [],
|
|
125
129
|
include_workflow_log=include_workflow_log,
|
|
126
130
|
download_to_path=download_to_path,
|
|
@@ -137,6 +141,7 @@ class ExportTools(ToolBase):
|
|
|
137
141
|
columns: list[JSONObject | int] | None = None,
|
|
138
142
|
where: list[JSONObject] | None = None,
|
|
139
143
|
order_by: list[JSONObject] | None = None,
|
|
144
|
+
record_id: str | int | None = None,
|
|
140
145
|
record_ids: list[str | int] | None = None,
|
|
141
146
|
include_workflow_log: bool = False,
|
|
142
147
|
) -> dict[str, Any]:
|
|
@@ -145,7 +150,7 @@ class ExportTools(ToolBase):
|
|
|
145
150
|
normalized_columns = _normalize_export_columns(columns or [])
|
|
146
151
|
normalized_where = self._record_tools._normalize_record_list_where(where or [])
|
|
147
152
|
normalized_order_by = self._record_tools._normalize_record_list_order_by(order_by or [])
|
|
148
|
-
normalized_record_ids = _normalize_export_record_ids(record_ids or [])
|
|
153
|
+
normalized_record_ids = _normalize_export_record_ids(_merge_single_export_record_id(record_id, record_ids or []))
|
|
149
154
|
if not normalized_app_key:
|
|
150
155
|
return self._failed_export_result(
|
|
151
156
|
error_code="EXPORT_START_FAILED",
|
|
@@ -467,6 +472,7 @@ class ExportTools(ToolBase):
|
|
|
467
472
|
columns: list[JSONObject | int] | None = None,
|
|
468
473
|
where: list[JSONObject] | None = None,
|
|
469
474
|
order_by: list[JSONObject] | None = None,
|
|
475
|
+
record_id: str | int | None = None,
|
|
470
476
|
record_ids: list[str | int] | None = None,
|
|
471
477
|
include_workflow_log: bool = False,
|
|
472
478
|
download_to_path: str | None = None,
|
|
@@ -496,6 +502,7 @@ class ExportTools(ToolBase):
|
|
|
496
502
|
columns=columns or [],
|
|
497
503
|
where=where or [],
|
|
498
504
|
order_by=order_by or [],
|
|
505
|
+
record_id=record_id,
|
|
499
506
|
record_ids=record_ids or [],
|
|
500
507
|
include_workflow_log=include_workflow_log,
|
|
501
508
|
)
|
|
@@ -1665,6 +1672,12 @@ def _normalize_export_record_ids(record_ids: list[str | int]) -> list[int]:
|
|
|
1665
1672
|
return normalized
|
|
1666
1673
|
|
|
1667
1674
|
|
|
1675
|
+
def _merge_single_export_record_id(record_id: str | int | None, record_ids: list[str | int]) -> list[str | int]:
|
|
1676
|
+
if record_id is None or str(record_id).strip() == "":
|
|
1677
|
+
return list(record_ids)
|
|
1678
|
+
return [record_id, *record_ids]
|
|
1679
|
+
|
|
1680
|
+
|
|
1668
1681
|
def _effective_total(page: JSONObject, *, page_size: int) -> int:
|
|
1669
1682
|
rows = page.get("list")
|
|
1670
1683
|
returned_rows = len(rows) if isinstance(rows, list) else 0
|
|
@@ -75,11 +75,12 @@ class ImportTools(ToolBase):
|
|
|
75
75
|
"""注册当前工具到 MCP 服务。"""
|
|
76
76
|
@mcp.tool()
|
|
77
77
|
def record_import_schema_get(
|
|
78
|
+
profile: str = DEFAULT_PROFILE,
|
|
78
79
|
app_key: str = "",
|
|
79
80
|
output_profile: str = "normal",
|
|
80
81
|
) -> dict[str, Any]:
|
|
81
82
|
return self.record_import_schema_get(
|
|
82
|
-
profile=
|
|
83
|
+
profile=profile,
|
|
83
84
|
app_key=app_key,
|
|
84
85
|
output_profile=output_profile,
|
|
85
86
|
)
|
|
@@ -224,8 +225,15 @@ class ImportTools(ToolBase):
|
|
|
224
225
|
}
|
|
225
226
|
if isinstance(column.get("options"), list) and column.get("options"):
|
|
226
227
|
payload["options"] = column["options"]
|
|
227
|
-
if
|
|
228
|
-
payload["
|
|
228
|
+
if column["write_kind"] == "member":
|
|
229
|
+
payload["import_value_format"] = "member_email"
|
|
230
|
+
payload["format_hint"] = "Import files must use member email values."
|
|
231
|
+
elif column["write_kind"] == "relation":
|
|
232
|
+
payload["import_value_format"] = "target_apply_id"
|
|
233
|
+
payload["format_hint"] = "Import files must use the target record apply_id."
|
|
234
|
+
elif column["write_kind"] == "department":
|
|
235
|
+
payload["import_value_format"] = "department_name"
|
|
236
|
+
payload["format_hint"] = "Import files may use a department name within the field candidate scope."
|
|
229
237
|
if bool(column.get("requires_upload")):
|
|
230
238
|
payload["requires_upload"] = True
|
|
231
239
|
if isinstance(column.get("target_app_key"), str):
|
|
@@ -660,7 +668,8 @@ class ImportTools(ToolBase):
|
|
|
660
668
|
applied_repairs: list[str] = []
|
|
661
669
|
skipped_repairs: list[str] = []
|
|
662
670
|
if "normalize_headers" in normalized_repairs:
|
|
663
|
-
|
|
671
|
+
repair_header_columns = _repair_header_columns_from_stored_precheck(stored, expected_columns)
|
|
672
|
+
if _repair_headers(sheet, repair_header_columns):
|
|
664
673
|
applied_repairs.append("normalize_headers")
|
|
665
674
|
else:
|
|
666
675
|
skipped_repairs.append("normalize_headers")
|
|
@@ -744,11 +753,17 @@ class ImportTools(ToolBase):
|
|
|
744
753
|
return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verification_id does not belong to the requested app")
|
|
745
754
|
if not bool(stored.get("can_import")):
|
|
746
755
|
return self._failed_start_result(error_code="IMPORT_VERIFICATION_FAILED", message="verification_id is not importable", extra={"accepted": False})
|
|
747
|
-
|
|
756
|
+
source_local_precheck = stored.get("source_local_precheck")
|
|
757
|
+
source_precheck_passed = isinstance(source_local_precheck, dict) and bool(source_local_precheck.get("can_import"))
|
|
758
|
+
if source_precheck_passed:
|
|
759
|
+
current_path = Path(str(stored.get("source_file_path") or stored["file_path"]))
|
|
760
|
+
expected_sha256 = stored.get("file_sha256")
|
|
761
|
+
else:
|
|
762
|
+
current_path = Path(str(stored.get("verified_file_path") or stored.get("source_file_path") or stored["file_path"]))
|
|
763
|
+
expected_sha256 = stored.get("verified_file_sha256") or stored.get("file_sha256")
|
|
748
764
|
if not current_path.is_file():
|
|
749
|
-
return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verified file no longer exists")
|
|
765
|
+
return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verified import file no longer exists")
|
|
750
766
|
current_sha256 = _sha256_file(current_path)
|
|
751
|
-
expected_sha256 = stored.get("verified_file_sha256") or stored.get("file_sha256")
|
|
752
767
|
if current_sha256 != expected_sha256:
|
|
753
768
|
return self._failed_start_result(
|
|
754
769
|
error_code="IMPORT_FILE_CHANGED_AFTER_VERIFY",
|
|
@@ -1142,6 +1157,9 @@ class ImportTools(ToolBase):
|
|
|
1142
1157
|
"can_import": True,
|
|
1143
1158
|
"extension": extension,
|
|
1144
1159
|
"error_code": None,
|
|
1160
|
+
"expected_header_titles": list(allowed_header_titles)
|
|
1161
|
+
if allowed_header_titles
|
|
1162
|
+
else [str(item["title"]) for item in expected_columns],
|
|
1145
1163
|
}
|
|
1146
1164
|
if extension not in SUPPORTED_IMPORT_EXTENSIONS:
|
|
1147
1165
|
base_result["issues"].append(_issue("UNSUPPORTED_FILE_FORMAT", "Only .xlsx and .xls files are supported in import v1.", severity="error"))
|
|
@@ -2204,6 +2222,20 @@ def _sheet_header_map(sheet) -> dict[str, int]: # type: ignore[no-untyped-def]
|
|
|
2204
2222
|
return mapping
|
|
2205
2223
|
|
|
2206
2224
|
|
|
2225
|
+
def _repair_header_columns_from_stored_precheck(stored: JSONObject, expected_columns: list[JSONObject]) -> list[JSONObject]:
|
|
2226
|
+
for key in ("source_local_precheck", "local_precheck"):
|
|
2227
|
+
precheck = stored.get(key)
|
|
2228
|
+
if not isinstance(precheck, dict):
|
|
2229
|
+
continue
|
|
2230
|
+
titles = precheck.get("expected_header_titles")
|
|
2231
|
+
if not isinstance(titles, list):
|
|
2232
|
+
continue
|
|
2233
|
+
normalized_titles = [title for title in (_normalize_optional_text(item) for item in titles) if title]
|
|
2234
|
+
if normalized_titles:
|
|
2235
|
+
return [{"title": title} for title in normalized_titles]
|
|
2236
|
+
return expected_columns
|
|
2237
|
+
|
|
2238
|
+
|
|
2207
2239
|
def _repair_headers(sheet, expected_columns: list[JSONObject]) -> bool: # type: ignore[no-untyped-def]
|
|
2208
2240
|
changed = False
|
|
2209
2241
|
expected_by_key = {_normalize_header_key(item["title"]): item["title"] for item in expected_columns}
|
|
@@ -2222,9 +2254,11 @@ def _repair_headers(sheet, expected_columns: list[JSONObject]) -> bool: # type:
|
|
|
2222
2254
|
# Fallback for template-based files where headers were edited into non-canonical
|
|
2223
2255
|
# values but column order is still intact. Keep any extra trailing system columns.
|
|
2224
2256
|
for index, column in enumerate(expected_columns, start=1):
|
|
2225
|
-
if index > len(header_cells):
|
|
2226
|
-
break
|
|
2227
2257
|
expected_title = str(column["title"]).strip()
|
|
2258
|
+
if index > len(header_cells):
|
|
2259
|
+
sheet.cell(row=1, column=index, value=expected_title)
|
|
2260
|
+
changed = True
|
|
2261
|
+
continue
|
|
2228
2262
|
current_title = _normalize_optional_text(header_cells[index - 1].value)
|
|
2229
2263
|
if current_title != expected_title:
|
|
2230
2264
|
header_cells[index - 1].value = expected_title
|
|
@@ -350,11 +350,12 @@ class RecordTools(ToolBase):
|
|
|
350
350
|
"""注册当前工具到 MCP 服务。"""
|
|
351
351
|
@mcp.tool()
|
|
352
352
|
def record_insert_schema_get(
|
|
353
|
+
profile: str = DEFAULT_PROFILE,
|
|
353
354
|
app_key: str = "",
|
|
354
355
|
output_profile: str = "normal",
|
|
355
356
|
) -> JSONObject:
|
|
356
357
|
return self.record_insert_schema_get_public(
|
|
357
|
-
profile=
|
|
358
|
+
profile=profile,
|
|
358
359
|
app_key=app_key,
|
|
359
360
|
output_profile=output_profile,
|
|
360
361
|
)
|
|
@@ -464,6 +465,7 @@ class RecordTools(ToolBase):
|
|
|
464
465
|
)
|
|
465
466
|
)
|
|
466
467
|
def record_access(
|
|
468
|
+
profile: str = DEFAULT_PROFILE,
|
|
467
469
|
app_key: str = "",
|
|
468
470
|
view_id: str = "",
|
|
469
471
|
columns: list[JSONObject | int] | None = None,
|
|
@@ -471,7 +473,7 @@ class RecordTools(ToolBase):
|
|
|
471
473
|
order_by: list[JSONObject] | None = None,
|
|
472
474
|
) -> JSONObject:
|
|
473
475
|
return self.record_access(
|
|
474
|
-
profile=
|
|
476
|
+
profile=profile,
|
|
475
477
|
app_key=app_key,
|
|
476
478
|
view_id=view_id,
|
|
477
479
|
columns=columns or [],
|
|
@@ -515,12 +517,13 @@ class RecordTools(ToolBase):
|
|
|
515
517
|
|
|
516
518
|
@mcp.tool()
|
|
517
519
|
def record_browse_schema_get(
|
|
520
|
+
profile: str = DEFAULT_PROFILE,
|
|
518
521
|
app_key: str = "",
|
|
519
522
|
view_id: str = "",
|
|
520
523
|
output_profile: str = "normal",
|
|
521
524
|
) -> JSONObject:
|
|
522
525
|
return self.record_browse_schema_get_public(
|
|
523
|
-
profile=
|
|
526
|
+
profile=profile,
|
|
524
527
|
app_key=app_key,
|
|
525
528
|
view_id=view_id,
|
|
526
529
|
output_profile=output_profile,
|
|
@@ -528,13 +531,14 @@ class RecordTools(ToolBase):
|
|
|
528
531
|
|
|
529
532
|
@mcp.tool()
|
|
530
533
|
def record_update_schema_get(
|
|
534
|
+
profile: str = DEFAULT_PROFILE,
|
|
531
535
|
app_key: str = "",
|
|
532
536
|
record_id: str = "",
|
|
533
537
|
view_id: str | None = None,
|
|
534
538
|
output_profile: str = "normal",
|
|
535
539
|
) -> JSONObject:
|
|
536
540
|
return self.record_update_schema_get_public(
|
|
537
|
-
profile=
|
|
541
|
+
profile=profile,
|
|
538
542
|
app_key=app_key,
|
|
539
543
|
record_id=record_id,
|
|
540
544
|
view_id=view_id,
|
|
@@ -550,13 +554,14 @@ class RecordTools(ToolBase):
|
|
|
550
554
|
)
|
|
551
555
|
)
|
|
552
556
|
def record_insert(
|
|
557
|
+
profile: str = DEFAULT_PROFILE,
|
|
553
558
|
app_key: str = "",
|
|
554
559
|
items: list[JSONObject] | None = None,
|
|
555
560
|
verify_write: bool = True,
|
|
556
561
|
output_profile: str = "normal",
|
|
557
562
|
) -> JSONObject:
|
|
558
563
|
return self.record_insert_public(
|
|
559
|
-
profile=
|
|
564
|
+
profile=profile,
|
|
560
565
|
app_key=app_key,
|
|
561
566
|
items=items,
|
|
562
567
|
verify_write=verify_write,
|
|
@@ -573,6 +578,7 @@ class RecordTools(ToolBase):
|
|
|
573
578
|
)
|
|
574
579
|
)
|
|
575
580
|
def record_update(
|
|
581
|
+
profile: str = DEFAULT_PROFILE,
|
|
576
582
|
app_key: str = "",
|
|
577
583
|
record_id: str | None = None,
|
|
578
584
|
fields: JSONObject | None = None,
|
|
@@ -583,7 +589,7 @@ class RecordTools(ToolBase):
|
|
|
583
589
|
output_profile: str = "normal",
|
|
584
590
|
) -> JSONObject:
|
|
585
591
|
return self.record_update_public(
|
|
586
|
-
profile=
|
|
592
|
+
profile=profile,
|
|
587
593
|
app_key=app_key,
|
|
588
594
|
record_id=record_id,
|
|
589
595
|
fields=fields,
|
|
@@ -601,6 +607,7 @@ class RecordTools(ToolBase):
|
|
|
601
607
|
)
|
|
602
608
|
)
|
|
603
609
|
def record_delete(
|
|
610
|
+
profile: str = DEFAULT_PROFILE,
|
|
604
611
|
app_key: str = "",
|
|
605
612
|
record_id: str | None = None,
|
|
606
613
|
record_ids: list[str] | None = None,
|
|
@@ -608,7 +615,7 @@ class RecordTools(ToolBase):
|
|
|
608
615
|
output_profile: str = "normal",
|
|
609
616
|
) -> JSONObject:
|
|
610
617
|
return self.record_delete_public(
|
|
611
|
-
profile=
|
|
618
|
+
profile=profile,
|
|
612
619
|
app_key=app_key,
|
|
613
620
|
record_id=record_id,
|
|
614
621
|
record_ids=record_ids or [],
|
|
@@ -11896,6 +11903,8 @@ class RecordTools(ToolBase):
|
|
|
11896
11903
|
schema: JSONObject = {}
|
|
11897
11904
|
if isinstance(raw.get("subQuestions"), list):
|
|
11898
11905
|
schema["formQues"] = [raw["subQuestions"]]
|
|
11906
|
+
elif isinstance(raw.get("subQues"), list):
|
|
11907
|
+
schema["formQues"] = [raw["subQues"]]
|
|
11899
11908
|
elif isinstance(raw.get("innerQuestions"), list):
|
|
11900
11909
|
schema["formQues"] = raw["innerQuestions"]
|
|
11901
11910
|
index = _build_field_index(schema)
|
|
@@ -14045,6 +14054,7 @@ class RecordTools(ToolBase):
|
|
|
14045
14054
|
or len(count_mismatches) > mismatch_before
|
|
14046
14055
|
):
|
|
14047
14056
|
continue
|
|
14057
|
+
continue
|
|
14048
14058
|
expected_value = _canonicalize_answer_value_for_compare(answer, field)
|
|
14049
14059
|
actual_value = _canonicalize_answer_value_for_compare(actual, field)
|
|
14050
14060
|
if not _canonical_value_is_empty(expected_value) and _canonical_value_is_empty(actual_value):
|
|
@@ -18819,12 +18829,14 @@ def _extract_sort_selector(item: JSONObject) -> JSONValue:
|
|
|
18819
18829
|
return None
|
|
18820
18830
|
|
|
18821
18831
|
|
|
18822
|
-
def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[int]:
|
|
18832
|
+
def _normalize_public_column_selectors(columns: list[JSONObject | int | str]) -> list[int]:
|
|
18823
18833
|
normalized: list[int] = []
|
|
18824
18834
|
for item in columns:
|
|
18825
18835
|
field_id: int | None = None
|
|
18826
18836
|
if isinstance(item, int):
|
|
18827
18837
|
field_id = item
|
|
18838
|
+
elif isinstance(item, str):
|
|
18839
|
+
field_id = _coerce_count(item)
|
|
18828
18840
|
elif isinstance(item, dict):
|
|
18829
18841
|
_ensure_allowed_record_list_keys(
|
|
18830
18842
|
item,
|
|
@@ -18836,19 +18848,21 @@ def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[
|
|
|
18836
18848
|
if field_id is None or field_id < 0:
|
|
18837
18849
|
raise_tool_error(
|
|
18838
18850
|
QingflowApiError.config_error(
|
|
18839
|
-
"columns must be a list of field_id integers or {field_id} objects"
|
|
18851
|
+
"columns must be a list of field_id integers, integer strings, or {field_id} objects"
|
|
18840
18852
|
)
|
|
18841
18853
|
)
|
|
18842
18854
|
normalized.append(field_id)
|
|
18843
18855
|
return normalized
|
|
18844
18856
|
|
|
18845
18857
|
|
|
18846
|
-
def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]) -> list[int]:
|
|
18858
|
+
def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int | str]) -> list[int]:
|
|
18847
18859
|
normalized: list[int] = []
|
|
18848
18860
|
for item in query_fields:
|
|
18849
18861
|
field_id: int | None = None
|
|
18850
18862
|
if isinstance(item, int):
|
|
18851
18863
|
field_id = item
|
|
18864
|
+
elif isinstance(item, str):
|
|
18865
|
+
field_id = _coerce_count(item)
|
|
18852
18866
|
elif isinstance(item, dict):
|
|
18853
18867
|
_ensure_allowed_record_list_keys(
|
|
18854
18868
|
item,
|
|
@@ -18860,7 +18874,7 @@ def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]
|
|
|
18860
18874
|
if field_id is None or field_id < 0:
|
|
18861
18875
|
raise_tool_error(
|
|
18862
18876
|
QingflowApiError.config_error(
|
|
18863
|
-
"query_fields must be a list of field_id integers or {field_id} objects"
|
|
18877
|
+
"query_fields must be a list of field_id integers, integer strings, or {field_id} objects"
|
|
18864
18878
|
)
|
|
18865
18879
|
)
|
|
18866
18880
|
normalized.append(field_id)
|