@qingflow-tech/qingflow-app-user-mcp 1.0.3 → 1.0.4
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/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +109 -0
- package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
- package/src/qingflow_mcp/builder_facade/models.py +44 -5
- package/src/qingflow_mcp/builder_facade/service.py +21 -8
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/app.py +47 -1
- package/src/qingflow_mcp/cli/commands/builder.py +7 -0
- package/src/qingflow_mcp/cli/commands/exports.py +111 -0
- package/src/qingflow_mcp/cli/commands/task.py +644 -22
- package/src/qingflow_mcp/cli/commands/workspace.py +64 -2
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +139 -4
- package/src/qingflow_mcp/cli/interaction.py +72 -0
- package/src/qingflow_mcp/cli/main.py +5 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/export_store.py +14 -0
- package/src/qingflow_mcp/public_surface.py +5 -0
- package/src/qingflow_mcp/response_trim.py +15 -1
- package/src/qingflow_mcp/server.py +22 -0
- package/src/qingflow_mcp/server_app_builder.py +4 -0
- package/src/qingflow_mcp/server_app_user.py +89 -0
- package/src/qingflow_mcp/session_store.py +57 -6
- package/src/qingflow_mcp/tools/ai_builder_tools.py +59 -16
- package/src/qingflow_mcp/tools/auth_tools.py +26 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
- package/src/qingflow_mcp/tools/export_tools.py +1565 -0
- package/src/qingflow_mcp/tools/import_tools.py +42 -2
- package/src/qingflow_mcp/tools/record_tools.py +145 -13
- package/src/qingflow_mcp/tools/resource_read_tools.py +40 -1
- package/src/qingflow_mcp/tools/task_context_tools.py +26 -8
|
@@ -37,6 +37,13 @@ SAFE_REPAIRS = {
|
|
|
37
37
|
"normalize_url_cells",
|
|
38
38
|
}
|
|
39
39
|
EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
40
|
+
IMPORT_STATUS_BY_PROCESS_STATUS = {
|
|
41
|
+
1: "queued",
|
|
42
|
+
2: "running",
|
|
43
|
+
3: "succeeded",
|
|
44
|
+
4: "failed",
|
|
45
|
+
5: "partially_failed",
|
|
46
|
+
}
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
class ImportTools(ToolBase):
|
|
@@ -866,13 +873,26 @@ class ImportTools(ToolBase):
|
|
|
866
873
|
"process_id_str": normalized_process,
|
|
867
874
|
},
|
|
868
875
|
)
|
|
876
|
+
raw_process_status = matched_record.get("processStatus")
|
|
869
877
|
total_rows = _coerce_int(matched_record.get("totalNumber") or matched_record.get("total_rows"))
|
|
870
878
|
success_rows = _coerce_int(matched_record.get("successNum") or matched_record.get("success_rows"))
|
|
871
879
|
failed_rows = _coerce_int(matched_record.get("errorNum") or matched_record.get("failed_rows"))
|
|
872
880
|
progress = _coerce_int(matched_record.get("importPercentage") or matched_record.get("progress"))
|
|
881
|
+
normalized_status = _normalize_import_status(raw_process_status)
|
|
882
|
+
warnings: list[dict[str, str]] = []
|
|
883
|
+
if normalized_status in {"succeeded", "failed", "partially_failed"} and all(
|
|
884
|
+
value is None for value in (total_rows, success_rows, failed_rows)
|
|
885
|
+
):
|
|
886
|
+
warnings.append(
|
|
887
|
+
{
|
|
888
|
+
"code": "IMPORT_STATUS_COUNTERS_MISSING",
|
|
889
|
+
"message": "backend import history returned a terminal process status without row counters",
|
|
890
|
+
}
|
|
891
|
+
)
|
|
873
892
|
return {
|
|
874
893
|
"ok": True,
|
|
875
|
-
"status":
|
|
894
|
+
"status": normalized_status,
|
|
895
|
+
"process_status": _coerce_int(raw_process_status),
|
|
876
896
|
"app_key": resolved_app_key,
|
|
877
897
|
"import_id": normalized_import_id or (local_job.get("import_id") if isinstance(local_job, dict) else None),
|
|
878
898
|
"process_id_str": normalized_process,
|
|
@@ -885,7 +905,7 @@ class ImportTools(ToolBase):
|
|
|
885
905
|
"error_file_urls": _normalize_error_file_urls(matched_record.get("errorFileUrls")),
|
|
886
906
|
"operate_time": matched_record.get("operateTime"),
|
|
887
907
|
"operate_user": matched_record.get("operateUser"),
|
|
888
|
-
"warnings":
|
|
908
|
+
"warnings": warnings,
|
|
889
909
|
"verification": {
|
|
890
910
|
"status_lookup_completed": True,
|
|
891
911
|
"matched_by": matched_by,
|
|
@@ -2217,6 +2237,26 @@ def _coerce_int(value: Any) -> int | None:
|
|
|
2217
2237
|
return None
|
|
2218
2238
|
|
|
2219
2239
|
|
|
2240
|
+
def _normalize_import_status(value: Any) -> str:
|
|
2241
|
+
status_code = _coerce_int(value)
|
|
2242
|
+
if status_code is not None:
|
|
2243
|
+
return IMPORT_STATUS_BY_PROCESS_STATUS.get(status_code, "unknown")
|
|
2244
|
+
text = str(value or "").strip().lower()
|
|
2245
|
+
if text in {"queued", "running", "succeeded", "failed", "partially_failed", "unknown"}:
|
|
2246
|
+
return text
|
|
2247
|
+
if text in {"line_up", "lineup"}:
|
|
2248
|
+
return "queued"
|
|
2249
|
+
if text in {"execute", "executing", "processing"}:
|
|
2250
|
+
return "running"
|
|
2251
|
+
if text in {"success", "completed"}:
|
|
2252
|
+
return "succeeded"
|
|
2253
|
+
if text in {"partly_fail", "partial_fail", "partially_fail", "partial_failed"}:
|
|
2254
|
+
return "partially_failed"
|
|
2255
|
+
if text in {"fail", "error"}:
|
|
2256
|
+
return "failed"
|
|
2257
|
+
return "unknown"
|
|
2258
|
+
|
|
2259
|
+
|
|
2220
2260
|
def _normalize_error_file_urls(value: Any) -> list[str]:
|
|
2221
2261
|
if isinstance(value, list):
|
|
2222
2262
|
return [str(item).strip() for item in value if str(item).strip()]
|
|
@@ -2920,12 +2920,14 @@ class RecordTools(ToolBase):
|
|
|
2920
2920
|
payload["category"] = error.category
|
|
2921
2921
|
if error.backend_code is not None:
|
|
2922
2922
|
payload["backend_code"] = error.backend_code
|
|
2923
|
+
if error.request_id is not None:
|
|
2924
|
+
payload["request_id"] = error.request_id
|
|
2923
2925
|
if error.http_status is not None:
|
|
2924
2926
|
payload["http_status"] = error.http_status
|
|
2925
2927
|
return payload
|
|
2926
2928
|
|
|
2927
2929
|
def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
|
|
2928
|
-
if error.backend_code in {40002, 40027, 40038, 404}:
|
|
2930
|
+
if error.backend_code in {40002, 40023, 40027, 40038, 404}:
|
|
2929
2931
|
return True
|
|
2930
2932
|
if error.http_status == 404:
|
|
2931
2933
|
return True
|
|
@@ -3059,6 +3061,107 @@ class RecordTools(ToolBase):
|
|
|
3059
3061
|
f"({get_record_list_type_label(used_list_type)})."
|
|
3060
3062
|
]
|
|
3061
3063
|
|
|
3064
|
+
def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
|
|
3065
|
+
if exc.backend_code == 500:
|
|
3066
|
+
return True
|
|
3067
|
+
if exc.http_status is not None and exc.http_status >= 500:
|
|
3068
|
+
return True
|
|
3069
|
+
normalized_message = exc.message.strip().lower()
|
|
3070
|
+
return normalized_message in {"unknown error", "internal server error"}
|
|
3071
|
+
|
|
3072
|
+
def _remap_record_update_target_context_error(
|
|
3073
|
+
self,
|
|
3074
|
+
profile: str,
|
|
3075
|
+
context, # type: ignore[no-untyped-def]
|
|
3076
|
+
*,
|
|
3077
|
+
app_key: str,
|
|
3078
|
+
apply_id: int,
|
|
3079
|
+
exc: QingflowApiError,
|
|
3080
|
+
) -> None:
|
|
3081
|
+
if not self._looks_like_generic_record_update_backend_failure(exc):
|
|
3082
|
+
return
|
|
3083
|
+
try:
|
|
3084
|
+
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
3085
|
+
probes = self._probe_candidate_record_contexts(
|
|
3086
|
+
context,
|
|
3087
|
+
app_key=app_key,
|
|
3088
|
+
apply_id=apply_id,
|
|
3089
|
+
candidate_routes=candidate_routes,
|
|
3090
|
+
)
|
|
3091
|
+
except (QingflowApiError, RuntimeError):
|
|
3092
|
+
return
|
|
3093
|
+
if not probes or any(probe.readable for probe in probes):
|
|
3094
|
+
return
|
|
3095
|
+
|
|
3096
|
+
blocker = (
|
|
3097
|
+
"CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3098
|
+
if all(probe.transport_error for probe in probes)
|
|
3099
|
+
else "NO_MATCHING_ACCESSIBLE_VIEW_FOR_RECORD"
|
|
3100
|
+
)
|
|
3101
|
+
recommended_next_actions = (
|
|
3102
|
+
[
|
|
3103
|
+
"Retry after the record becomes readable in the current workspace/profile context.",
|
|
3104
|
+
"If the issue persists, verify that the current profile still has read access to this record.",
|
|
3105
|
+
]
|
|
3106
|
+
if blocker == "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3107
|
+
else [
|
|
3108
|
+
"Use record_get or record_list to confirm the record still exists in the current workspace.",
|
|
3109
|
+
"Call record_update_schema_get to inspect whether any accessible view still matches this record.",
|
|
3110
|
+
]
|
|
3111
|
+
)
|
|
3112
|
+
first_error_payload = next(
|
|
3113
|
+
(
|
|
3114
|
+
cast(JSONObject, probe.error_payload)
|
|
3115
|
+
for probe in probes
|
|
3116
|
+
if isinstance(probe.error_payload, dict)
|
|
3117
|
+
),
|
|
3118
|
+
None,
|
|
3119
|
+
)
|
|
3120
|
+
backend_code = (
|
|
3121
|
+
cast(int, first_error_payload.get("backend_code"))
|
|
3122
|
+
if isinstance(first_error_payload, dict) and isinstance(first_error_payload.get("backend_code"), int)
|
|
3123
|
+
else exc.backend_code
|
|
3124
|
+
)
|
|
3125
|
+
request_id = (
|
|
3126
|
+
_normalize_optional_text(first_error_payload.get("request_id"))
|
|
3127
|
+
if isinstance(first_error_payload, dict)
|
|
3128
|
+
else None
|
|
3129
|
+
) or exc.request_id
|
|
3130
|
+
http_status = (
|
|
3131
|
+
cast(int, first_error_payload.get("http_status"))
|
|
3132
|
+
if isinstance(first_error_payload, dict) and isinstance(first_error_payload.get("http_status"), int)
|
|
3133
|
+
else exc.http_status
|
|
3134
|
+
)
|
|
3135
|
+
raise_tool_error(
|
|
3136
|
+
QingflowApiError(
|
|
3137
|
+
category="backend",
|
|
3138
|
+
message=(
|
|
3139
|
+
"Direct record edit was blocked because the current record context could not be loaded from any candidate route."
|
|
3140
|
+
if blocker == "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3141
|
+
else "Direct record edit was blocked because the target record is no longer accessible in any readable view for the current profile."
|
|
3142
|
+
),
|
|
3143
|
+
backend_code=backend_code,
|
|
3144
|
+
request_id=request_id,
|
|
3145
|
+
http_status=http_status,
|
|
3146
|
+
details={
|
|
3147
|
+
"error_code": blocker,
|
|
3148
|
+
"operation": "update",
|
|
3149
|
+
"app_key": app_key,
|
|
3150
|
+
"record_id": apply_id,
|
|
3151
|
+
"blockers": [blocker],
|
|
3152
|
+
"request_route": self._request_route_payload(context),
|
|
3153
|
+
"view_probe_summary": [
|
|
3154
|
+
self._record_context_probe_summary_payload(probe)
|
|
3155
|
+
for probe in probes
|
|
3156
|
+
],
|
|
3157
|
+
"recommended_next_actions": recommended_next_actions,
|
|
3158
|
+
"fix_hint": (
|
|
3159
|
+
"Confirm the target record still exists and remains visible in at least one accessible view before retrying the update."
|
|
3160
|
+
),
|
|
3161
|
+
},
|
|
3162
|
+
)
|
|
3163
|
+
)
|
|
3164
|
+
|
|
3062
3165
|
def _record_matches_accessible_view(
|
|
3063
3166
|
self,
|
|
3064
3167
|
context, # type: ignore[no-untyped-def]
|
|
@@ -6355,12 +6458,22 @@ class RecordTools(ToolBase):
|
|
|
6355
6458
|
force_refresh_form=force_refresh_form,
|
|
6356
6459
|
)
|
|
6357
6460
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6461
|
+
try:
|
|
6462
|
+
result = self.backend.request(
|
|
6463
|
+
"POST",
|
|
6464
|
+
context,
|
|
6465
|
+
f"/app/{app_key}/apply/{normalized_apply_id}",
|
|
6466
|
+
json_body={"role": role, "answers": normalized_answers},
|
|
6467
|
+
)
|
|
6468
|
+
except QingflowApiError as exc:
|
|
6469
|
+
self._remap_record_update_target_context_error(
|
|
6470
|
+
profile,
|
|
6471
|
+
context,
|
|
6472
|
+
app_key=app_key,
|
|
6473
|
+
apply_id=normalized_apply_id,
|
|
6474
|
+
exc=exc,
|
|
6475
|
+
)
|
|
6476
|
+
raise
|
|
6364
6477
|
verification = self._verify_record_write_result(
|
|
6365
6478
|
context,
|
|
6366
6479
|
app_key=app_key,
|
|
@@ -6890,6 +7003,8 @@ class RecordTools(ToolBase):
|
|
|
6890
7003
|
isinstance(payload.get("viewgraphLimit"), list)
|
|
6891
7004
|
or isinstance(payload.get("viewConfig"), dict)
|
|
6892
7005
|
or isinstance(payload.get("viewgraphConfig"), dict)
|
|
7006
|
+
or isinstance(payload.get("viewgraphQuestions"), list)
|
|
7007
|
+
or isinstance(payload.get("viewgraphQueIds"), list)
|
|
6893
7008
|
):
|
|
6894
7009
|
config = payload
|
|
6895
7010
|
else:
|
|
@@ -8794,7 +8909,13 @@ class RecordTools(ToolBase):
|
|
|
8794
8909
|
except json.JSONDecodeError:
|
|
8795
8910
|
parsed = None
|
|
8796
8911
|
if isinstance(parsed, dict):
|
|
8797
|
-
|
|
8912
|
+
parsed_details = parsed.get("details")
|
|
8913
|
+
details_payload = cast(JSONObject, parsed_details) if isinstance(parsed_details, dict) else None
|
|
8914
|
+
error_payload["error_code"] = (
|
|
8915
|
+
parsed.get("error_code")
|
|
8916
|
+
or (details_payload.get("error_code") if details_payload is not None else None)
|
|
8917
|
+
or error_payload["error_code"]
|
|
8918
|
+
)
|
|
8798
8919
|
error_payload["message"] = parsed.get("message") or error_payload["message"]
|
|
8799
8920
|
if parsed.get("backend_code") is not None:
|
|
8800
8921
|
error_payload["backend_code"] = parsed.get("backend_code")
|
|
@@ -8802,6 +8923,8 @@ class RecordTools(ToolBase):
|
|
|
8802
8923
|
error_payload["request_id"] = parsed.get("request_id")
|
|
8803
8924
|
if isinstance(parsed.get("request_route"), dict):
|
|
8804
8925
|
request_route = cast(JSONObject, parsed.get("request_route"))
|
|
8926
|
+
elif details_payload is not None and isinstance(details_payload.get("request_route"), dict):
|
|
8927
|
+
request_route = cast(JSONObject, details_payload.get("request_route"))
|
|
8805
8928
|
response: JSONObject = {
|
|
8806
8929
|
"profile": profile,
|
|
8807
8930
|
"ws_id": None,
|
|
@@ -10089,11 +10212,20 @@ class RecordTools(ToolBase):
|
|
|
10089
10212
|
"""执行内部辅助逻辑。"""
|
|
10090
10213
|
if not app_key:
|
|
10091
10214
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10215
|
+
return self._normalize_internal_backend_id(apply_id, field_name="apply_id")
|
|
10216
|
+
|
|
10217
|
+
def _normalize_internal_backend_id(self, value: Any, *, field_name: str) -> int:
|
|
10218
|
+
"""Normalize backend/apply ids after the public boundary has already preserved long string ids."""
|
|
10219
|
+
if value in (None, "") or isinstance(value, bool):
|
|
10220
|
+
raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
|
|
10221
|
+
if isinstance(value, int):
|
|
10222
|
+
if value <= 0:
|
|
10223
|
+
raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
|
|
10224
|
+
return value
|
|
10225
|
+
text = stringify_backend_id(value)
|
|
10226
|
+
if text is None or not text.isdecimal() or int(text) <= 0:
|
|
10227
|
+
raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
|
|
10228
|
+
return int(text)
|
|
10097
10229
|
|
|
10098
10230
|
def _validate_record_write(self, app_key: str, answers: list[JSONObject], apply_id: int | None = None) -> None:
|
|
10099
10231
|
"""执行内部辅助逻辑。"""
|
|
@@ -96,6 +96,9 @@ class ResourceReadTools(ToolBase):
|
|
|
96
96
|
)
|
|
97
97
|
)
|
|
98
98
|
if system_view is not None:
|
|
99
|
+
export_capability = _view_export_capability_payload(
|
|
100
|
+
supported=_export_supported_for_view_type(system_view["view_type"])
|
|
101
|
+
)
|
|
99
102
|
return self._run(
|
|
100
103
|
profile,
|
|
101
104
|
lambda session_profile, _context: {
|
|
@@ -106,11 +109,17 @@ class ResourceReadTools(ToolBase):
|
|
|
106
109
|
{
|
|
107
110
|
"code": "VIEW_APP_KEY_UNRESOLVED",
|
|
108
111
|
"message": f"view_get could not resolve app_key for system view `{view_id}`; keep using the app_key from the parent app context.",
|
|
109
|
-
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"code": "VIEW_EXPORT_APP_CONTEXT_REQUIRED",
|
|
115
|
+
"message": f"view_get supports exporting `{view_id}`, but the export call still needs the parent app context `app_key`.",
|
|
116
|
+
},
|
|
110
117
|
],
|
|
111
118
|
"verification": {
|
|
112
119
|
"view_exists": True,
|
|
113
120
|
"descriptor_only": True,
|
|
121
|
+
"export_route_supported": export_capability["supported"],
|
|
122
|
+
"export_permission_verified": export_capability["permission_verified"],
|
|
114
123
|
},
|
|
115
124
|
"data": {
|
|
116
125
|
"app_key": None,
|
|
@@ -120,11 +129,13 @@ class ResourceReadTools(ToolBase):
|
|
|
120
129
|
"view_type": system_view["view_type"],
|
|
121
130
|
"visible_columns": [],
|
|
122
131
|
"analysis_supported": system_view["analysis_supported"],
|
|
132
|
+
"export_capability": export_capability,
|
|
123
133
|
},
|
|
124
134
|
},
|
|
125
135
|
)
|
|
126
136
|
|
|
127
137
|
def runner(session_profile, context):
|
|
138
|
+
raw_view_type = None
|
|
128
139
|
warnings: list[JSONObject] = []
|
|
129
140
|
verification = {
|
|
130
141
|
"view_exists": True,
|
|
@@ -150,6 +161,11 @@ class ResourceReadTools(ToolBase):
|
|
|
150
161
|
str(base_info.get("viewgraphType") or "").strip()
|
|
151
162
|
or str(config.get("viewgraphType") or config.get("viewType") or "").strip()
|
|
152
163
|
)
|
|
164
|
+
export_capability = _view_export_capability_payload(
|
|
165
|
+
supported=_export_supported_for_view_type(raw_view_type or None)
|
|
166
|
+
)
|
|
167
|
+
verification["export_route_supported"] = export_capability["supported"]
|
|
168
|
+
verification["export_permission_verified"] = export_capability["permission_verified"]
|
|
153
169
|
resolved_app_key = str(base_info.get("appKey") or config.get("appKey") or "").strip() or None
|
|
154
170
|
if not resolved_app_key:
|
|
155
171
|
resolved_app_key = self._resolve_app_key_from_view_form(context=context, view_key=view_key)
|
|
@@ -165,6 +181,13 @@ class ResourceReadTools(ToolBase):
|
|
|
165
181
|
"message": f"view_get could not resolve app_key for `{view_id}` from view metadata; keep using the app_key from the parent app or portal context.",
|
|
166
182
|
}
|
|
167
183
|
)
|
|
184
|
+
if export_capability["supported"]:
|
|
185
|
+
warnings.append(
|
|
186
|
+
{
|
|
187
|
+
"code": "VIEW_EXPORT_APP_CONTEXT_REQUIRED",
|
|
188
|
+
"message": f"view_get supports exporting `{view_id}`, but the export call still needs an explicit `app_key` from the parent app context.",
|
|
189
|
+
}
|
|
190
|
+
)
|
|
168
191
|
return {
|
|
169
192
|
"profile": profile,
|
|
170
193
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -186,6 +209,7 @@ class ResourceReadTools(ToolBase):
|
|
|
186
209
|
if str(item.get("queTitle") or item.get("title") or "").strip()
|
|
187
210
|
],
|
|
188
211
|
"analysis_supported": _analysis_supported_for_view_type(raw_view_type or None),
|
|
212
|
+
"export_capability": export_capability,
|
|
189
213
|
},
|
|
190
214
|
}
|
|
191
215
|
|
|
@@ -464,6 +488,21 @@ def _lookup_system_view_descriptor(view_id: str) -> dict[str, Any] | None:
|
|
|
464
488
|
return None
|
|
465
489
|
|
|
466
490
|
|
|
491
|
+
def _view_export_capability_payload(*, supported: bool) -> JSONObject:
|
|
492
|
+
return {
|
|
493
|
+
"supported": supported,
|
|
494
|
+
"tool": "record_export_start",
|
|
495
|
+
"format": "xlsx",
|
|
496
|
+
"async": True,
|
|
497
|
+
"requires_app_key": True,
|
|
498
|
+
"permission_verified": False,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _export_supported_for_view_type(view_type: str | None) -> bool:
|
|
503
|
+
return _analysis_supported_for_view_type(view_type)
|
|
504
|
+
|
|
505
|
+
|
|
467
506
|
def _normalize_view_type(view_type: Any) -> str | None:
|
|
468
507
|
value = str(view_type or "").strip()
|
|
469
508
|
if not value:
|
|
@@ -1762,6 +1762,7 @@ class TaskContextTools(ToolBase):
|
|
|
1762
1762
|
"apply_time": record.get("apply_time"),
|
|
1763
1763
|
"last_update_time": record.get("last_update_time"),
|
|
1764
1764
|
"core_fields": self._task_record_core_fields(record.get("answers") or []),
|
|
1765
|
+
"all_fields": self._task_record_all_fields(record.get("answers") or []),
|
|
1765
1766
|
},
|
|
1766
1767
|
"available_actions": available_actions,
|
|
1767
1768
|
"editable_fields": [
|
|
@@ -1921,9 +1922,21 @@ class TaskContextTools(ToolBase):
|
|
|
1921
1922
|
return None
|
|
1922
1923
|
|
|
1923
1924
|
def _task_record_core_fields(self, answers: Any, *, limit: int = 12) -> dict[str, Any]:
|
|
1925
|
+
return self._task_record_field_map(answers, limit=limit, truncate_text=160)
|
|
1926
|
+
|
|
1927
|
+
def _task_record_all_fields(self, answers: Any) -> dict[str, Any]:
|
|
1928
|
+
return self._task_record_field_map(answers, limit=None, truncate_text=None)
|
|
1929
|
+
|
|
1930
|
+
def _task_record_field_map(
|
|
1931
|
+
self,
|
|
1932
|
+
answers: Any,
|
|
1933
|
+
*,
|
|
1934
|
+
limit: int | None,
|
|
1935
|
+
truncate_text: int | None,
|
|
1936
|
+
) -> dict[str, Any]:
|
|
1924
1937
|
if not isinstance(answers, list):
|
|
1925
1938
|
return {}
|
|
1926
|
-
|
|
1939
|
+
field_map: dict[str, Any] = {}
|
|
1927
1940
|
for answer in answers:
|
|
1928
1941
|
if not isinstance(answer, dict):
|
|
1929
1942
|
continue
|
|
@@ -1943,19 +1956,24 @@ class TaskContextTools(ToolBase):
|
|
|
1943
1956
|
value = values[0] if len(values) == 1 else values
|
|
1944
1957
|
if value in (None, "", []):
|
|
1945
1958
|
continue
|
|
1946
|
-
|
|
1947
|
-
if len(
|
|
1959
|
+
field_map[str(title)] = self._compact_task_value(value, truncate_text=truncate_text)
|
|
1960
|
+
if limit is not None and len(field_map) >= limit:
|
|
1948
1961
|
break
|
|
1949
|
-
return
|
|
1962
|
+
return field_map
|
|
1950
1963
|
|
|
1951
|
-
def _compact_task_value(self, value: Any) -> Any:
|
|
1964
|
+
def _compact_task_value(self, value: Any, *, truncate_text: int | None = 160) -> Any:
|
|
1952
1965
|
if isinstance(value, list):
|
|
1953
|
-
|
|
1966
|
+
items = [self._compact_task_value(item, truncate_text=truncate_text) for item in value]
|
|
1967
|
+
if truncate_text is not None:
|
|
1968
|
+
return items[:8]
|
|
1969
|
+
return items
|
|
1954
1970
|
text = re.sub(r"<[^>]+>", " ", str(value))
|
|
1955
1971
|
text = re.sub(r"\s+", " ", text).strip()
|
|
1956
|
-
if len(text) <=
|
|
1972
|
+
if truncate_text is None or len(text) <= truncate_text:
|
|
1957
1973
|
return text
|
|
1958
|
-
|
|
1974
|
+
if truncate_text <= 3:
|
|
1975
|
+
return text[:truncate_text]
|
|
1976
|
+
return text[: truncate_text - 3].rstrip() + "..."
|
|
1959
1977
|
|
|
1960
1978
|
def _compact_task_editable_field(self, field: dict[str, Any], update_schema: dict[str, Any]) -> dict[str, Any]:
|
|
1961
1979
|
payload_template = update_schema.get("payload_template") if isinstance(update_schema.get("payload_template"), dict) else {}
|