@josephyan/qingflow-cli 0.2.0-beta.73 → 0.2.0-beta.74
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/backend_client.py +102 -0
- package/src/qingflow_mcp/builder_facade/service.py +609 -5
- package/src/qingflow_mcp/cli/commands/builder.py +33 -1
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/config.py +147 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +4 -0
- package/src/qingflow_mcp/server_app_builder.py +26 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +110 -0
- package/src/qingflow_mcp/tools/repository_dev_tools.py +533 -0
|
@@ -379,6 +379,61 @@ class AiBuilderFacade:
|
|
|
379
379
|
"tag_icon": tag_icon,
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
def solution_install(
|
|
383
|
+
self,
|
|
384
|
+
*,
|
|
385
|
+
profile: str,
|
|
386
|
+
solution_key: str,
|
|
387
|
+
being_copy_data: bool = True,
|
|
388
|
+
solution_source: str = "solutionDetail",
|
|
389
|
+
) -> JSONObject:
|
|
390
|
+
requested_solution_key = str(solution_key or "").strip()
|
|
391
|
+
requested_solution_source = str(solution_source or "").strip() or "solutionDetail"
|
|
392
|
+
normalized_args = {
|
|
393
|
+
"solution_key": requested_solution_key,
|
|
394
|
+
"being_copy_data": bool(being_copy_data),
|
|
395
|
+
"solution_source": requested_solution_source,
|
|
396
|
+
}
|
|
397
|
+
if not requested_solution_key:
|
|
398
|
+
return _failed(
|
|
399
|
+
"SOLUTION_KEY_REQUIRED",
|
|
400
|
+
"solution_key is required",
|
|
401
|
+
normalized_args=normalized_args,
|
|
402
|
+
suggested_next_call=None,
|
|
403
|
+
)
|
|
404
|
+
try:
|
|
405
|
+
created = self.apps.app_create(
|
|
406
|
+
profile=profile,
|
|
407
|
+
payload={
|
|
408
|
+
"solutionKey": requested_solution_key,
|
|
409
|
+
"beingCopyData": bool(being_copy_data),
|
|
410
|
+
"solutionSource": requested_solution_source,
|
|
411
|
+
},
|
|
412
|
+
)
|
|
413
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
414
|
+
api_error = _coerce_api_error(error)
|
|
415
|
+
return _failed_from_api_error(
|
|
416
|
+
"SOLUTION_INSTALL_FAILED",
|
|
417
|
+
api_error,
|
|
418
|
+
normalized_args=normalized_args,
|
|
419
|
+
details={"solution_key": requested_solution_key},
|
|
420
|
+
suggested_next_call={"tool_name": "solution_install", "arguments": {"profile": profile, **normalized_args}},
|
|
421
|
+
)
|
|
422
|
+
return {
|
|
423
|
+
"status": "success",
|
|
424
|
+
"error_code": None,
|
|
425
|
+
"recoverable": False,
|
|
426
|
+
"message": "installed solution",
|
|
427
|
+
"normalized_args": normalized_args,
|
|
428
|
+
"missing_fields": [],
|
|
429
|
+
"allowed_values": {},
|
|
430
|
+
"details": {},
|
|
431
|
+
"request_id": created.get("request_id"),
|
|
432
|
+
"suggested_next_call": None,
|
|
433
|
+
"noop": False,
|
|
434
|
+
"verification": {},
|
|
435
|
+
}
|
|
436
|
+
|
|
382
437
|
def package_list(self, *, profile: str, trial_status: str = "all") -> JSONObject:
|
|
383
438
|
listed = self.packages.package_list(profile=profile, trial_status=trial_status, include_raw=False)
|
|
384
439
|
return {
|
|
@@ -2088,6 +2143,280 @@ class AiBuilderFacade:
|
|
|
2088
2143
|
result["message"] = "read app field config"
|
|
2089
2144
|
return result
|
|
2090
2145
|
|
|
2146
|
+
def app_repair_code_blocks(
|
|
2147
|
+
self,
|
|
2148
|
+
*,
|
|
2149
|
+
profile: str,
|
|
2150
|
+
app_key: str,
|
|
2151
|
+
field: str | None = None,
|
|
2152
|
+
apply: bool = False,
|
|
2153
|
+
) -> JSONObject:
|
|
2154
|
+
try:
|
|
2155
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
2156
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2157
|
+
api_error = _coerce_api_error(error)
|
|
2158
|
+
return _failed_from_api_error(
|
|
2159
|
+
"FIELDS_READ_FAILED",
|
|
2160
|
+
api_error,
|
|
2161
|
+
normalized_args={"app_key": app_key, "field": field, "apply": apply},
|
|
2162
|
+
details={"app_key": app_key},
|
|
2163
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2164
|
+
)
|
|
2165
|
+
parsed_fields = cast(list[dict[str, Any]], state["parsed"].get("fields") or [])
|
|
2166
|
+
field_lookup = _build_public_field_lookup(parsed_fields)
|
|
2167
|
+
selected_fields: list[dict[str, Any]]
|
|
2168
|
+
requested_selector = str(field or "").strip()
|
|
2169
|
+
if requested_selector:
|
|
2170
|
+
try:
|
|
2171
|
+
selected_field = _resolve_public_field(requested_selector, field_lookup=field_lookup)
|
|
2172
|
+
except ValueError:
|
|
2173
|
+
return _failed(
|
|
2174
|
+
"FIELD_NOT_FOUND",
|
|
2175
|
+
"field selector did not match any existing field",
|
|
2176
|
+
normalized_args={"app_key": app_key, "field": requested_selector, "apply": apply},
|
|
2177
|
+
details={"app_key": app_key, "field": requested_selector},
|
|
2178
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2179
|
+
)
|
|
2180
|
+
if str(selected_field.get("type") or "") != FieldType.code_block.value:
|
|
2181
|
+
return _failed(
|
|
2182
|
+
"CODE_BLOCK_FIELD_REQUIRED",
|
|
2183
|
+
"selected field is not a code_block field",
|
|
2184
|
+
normalized_args={"app_key": app_key, "field": requested_selector, "apply": apply},
|
|
2185
|
+
details={"app_key": app_key, "field": requested_selector, "field_type": selected_field.get("type")},
|
|
2186
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2187
|
+
)
|
|
2188
|
+
selected_fields = [selected_field]
|
|
2189
|
+
else:
|
|
2190
|
+
selected_fields = [field_item for field_item in parsed_fields if str(field_item.get("type") or "") == FieldType.code_block.value]
|
|
2191
|
+
plans: list[dict[str, Any]] = []
|
|
2192
|
+
update_fields: list[FieldUpdatePatch] = []
|
|
2193
|
+
applied_fields: list[str] = []
|
|
2194
|
+
for code_block_field in selected_fields:
|
|
2195
|
+
field_name = _code_block_field_display_name(code_block_field)
|
|
2196
|
+
normalized_binding = _normalize_code_block_binding(code_block_field.get("code_block_binding"))
|
|
2197
|
+
normalized_config = _normalize_code_block_config(code_block_field.get("code_block_config") or code_block_field.get("config") or {}) or None
|
|
2198
|
+
raw_binding = code_block_field.get("code_block_binding") if isinstance(code_block_field.get("code_block_binding"), dict) else None
|
|
2199
|
+
preserved_output_targets = [
|
|
2200
|
+
str((output.get("target_field") or {}).get("name") or "").strip()
|
|
2201
|
+
for output in cast(list[dict[str, Any]], (normalized_binding or {}).get("outputs") or [])
|
|
2202
|
+
if isinstance(output.get("target_field"), dict) and str((output.get("target_field") or {}).get("name") or "").strip()
|
|
2203
|
+
]
|
|
2204
|
+
detected_issues: list[str] = []
|
|
2205
|
+
warnings: list[dict[str, Any]] = []
|
|
2206
|
+
normalized_code_preview: str | None = None
|
|
2207
|
+
normalized_binding_code = None
|
|
2208
|
+
normalized_config_code = None
|
|
2209
|
+
if normalized_binding is not None:
|
|
2210
|
+
raw_code = str(normalized_binding.get("code") or "")
|
|
2211
|
+
normalized_binding_code = _normalize_code_block_output_assignment(raw_code)
|
|
2212
|
+
if normalized_binding_code != raw_code:
|
|
2213
|
+
detected_issues.append("CONST_OR_LET_QF_OUTPUT_ASSIGNMENT")
|
|
2214
|
+
normalized_code_preview = normalized_binding_code
|
|
2215
|
+
if normalized_config is not None:
|
|
2216
|
+
raw_code_content = str(normalized_config.get("code_content") or "")
|
|
2217
|
+
normalized_config_code = _normalize_code_block_output_assignment(raw_code_content)
|
|
2218
|
+
if normalized_code_preview is None and normalized_config_code != raw_code_content:
|
|
2219
|
+
normalized_code_preview = normalized_config_code
|
|
2220
|
+
try:
|
|
2221
|
+
_validate_code_block_alias_config(
|
|
2222
|
+
field_name=field_name,
|
|
2223
|
+
raw_binding=raw_binding,
|
|
2224
|
+
normalized_config=normalized_config,
|
|
2225
|
+
)
|
|
2226
|
+
except _CodeBlockValidationError as error:
|
|
2227
|
+
detected_issues.append(error.error_code)
|
|
2228
|
+
warnings.append(_warning(error.error_code, error.message))
|
|
2229
|
+
has_outputs = bool((normalized_binding or {}).get("outputs")) or bool((normalized_config or {}).get("result_alias_path"))
|
|
2230
|
+
effective_code = ""
|
|
2231
|
+
if normalized_binding is not None:
|
|
2232
|
+
effective_code = str(normalized_binding_code if normalized_binding_code is not None else normalized_binding.get("code") or "")
|
|
2233
|
+
elif normalized_config is not None:
|
|
2234
|
+
code_content = str(normalized_config_code if normalized_config_code is not None else normalized_config.get("code_content") or "")
|
|
2235
|
+
effective_code = _strip_code_block_generated_input_prelude(code_content)
|
|
2236
|
+
if has_outputs and not _code_block_has_effective_output_assignment(effective_code):
|
|
2237
|
+
detected_issues.append("CODE_BLOCK_OUTPUT_ASSIGNMENT_MISSING")
|
|
2238
|
+
warnings.append(
|
|
2239
|
+
_warning(
|
|
2240
|
+
"CODE_BLOCK_OUTPUT_ASSIGNMENT_MISSING",
|
|
2241
|
+
"configured outputs require qf_output assignment before runtime writeback can succeed",
|
|
2242
|
+
)
|
|
2243
|
+
)
|
|
2244
|
+
target_relation_default_verified = True
|
|
2245
|
+
binding_has_target_bindings = False
|
|
2246
|
+
for output in cast(list[dict[str, Any]], (normalized_binding or {}).get("outputs") or []):
|
|
2247
|
+
target_payload = output.get("target_field")
|
|
2248
|
+
if not isinstance(target_payload, dict):
|
|
2249
|
+
continue
|
|
2250
|
+
if any(target_payload.values()):
|
|
2251
|
+
binding_has_target_bindings = True
|
|
2252
|
+
try:
|
|
2253
|
+
target_field = _resolve_field_selector_with_uniqueness(
|
|
2254
|
+
fields=parsed_fields,
|
|
2255
|
+
selector_payload=target_payload,
|
|
2256
|
+
location="repair.target_field",
|
|
2257
|
+
)
|
|
2258
|
+
except ValueError:
|
|
2259
|
+
target_relation_default_verified = False
|
|
2260
|
+
detected_issues.append("CODE_BLOCK_TARGET_DEFAULT_INVALID")
|
|
2261
|
+
warnings.append(_warning("CODE_BLOCK_TARGET_DEFAULT_INVALID", "bound output target could not be resolved during repair scan"))
|
|
2262
|
+
break
|
|
2263
|
+
if _coerce_any_int(target_field.get("default_type")) == DEFAULT_TYPE_RELATION:
|
|
2264
|
+
continue
|
|
2265
|
+
target_relation_default_verified = False
|
|
2266
|
+
detected_issues.append("CODE_BLOCK_TARGET_DEFAULT_INVALID")
|
|
2267
|
+
warnings.append(
|
|
2268
|
+
_warning(
|
|
2269
|
+
"CODE_BLOCK_TARGET_DEFAULT_INVALID",
|
|
2270
|
+
"bound output target is not stored as relation default and should be rebuilt through repair",
|
|
2271
|
+
target_field_name=_code_block_field_display_name(target_field),
|
|
2272
|
+
)
|
|
2273
|
+
)
|
|
2274
|
+
alias_issue_detected = "CODE_BLOCK_ALIAS_REQUIRED" in detected_issues
|
|
2275
|
+
assignment_missing_detected = "CODE_BLOCK_OUTPUT_ASSIGNMENT_MISSING" in detected_issues
|
|
2276
|
+
can_auto_repair = not alias_issue_detected and not assignment_missing_detected
|
|
2277
|
+
repair_mode = "none"
|
|
2278
|
+
patch_payload: dict[str, Any] | None = None
|
|
2279
|
+
safe_auto_fix = False
|
|
2280
|
+
if can_auto_repair and normalized_binding is not None and binding_has_target_bindings and (
|
|
2281
|
+
(normalized_binding_code is not None and normalized_binding_code != str(normalized_binding.get("code") or ""))
|
|
2282
|
+
or not target_relation_default_verified
|
|
2283
|
+
):
|
|
2284
|
+
patch_payload = {
|
|
2285
|
+
"selector": _field_selector_payload_for_field(code_block_field),
|
|
2286
|
+
"set": {
|
|
2287
|
+
"code_block_binding": _public_code_block_binding_payload(
|
|
2288
|
+
{**normalized_binding, "code": normalized_binding_code if normalized_binding_code is not None else str(normalized_binding.get("code") or "")}
|
|
2289
|
+
)
|
|
2290
|
+
},
|
|
2291
|
+
}
|
|
2292
|
+
repair_mode = "binding"
|
|
2293
|
+
safe_auto_fix = True
|
|
2294
|
+
elif (
|
|
2295
|
+
can_auto_repair
|
|
2296
|
+
and normalized_config is not None
|
|
2297
|
+
and normalized_config_code is not None
|
|
2298
|
+
and normalized_config_code != str(normalized_config.get("code_content") or "")
|
|
2299
|
+
):
|
|
2300
|
+
patch_payload = {
|
|
2301
|
+
"selector": _field_selector_payload_for_field(code_block_field),
|
|
2302
|
+
"set": {"code_block_config": {**normalized_config, "code_content": normalized_config_code}},
|
|
2303
|
+
}
|
|
2304
|
+
repair_mode = "config"
|
|
2305
|
+
safe_auto_fix = True
|
|
2306
|
+
if not detected_issues:
|
|
2307
|
+
detected_issues.append("NO_REPAIR_NEEDED")
|
|
2308
|
+
plan = {
|
|
2309
|
+
"field_name": field_name,
|
|
2310
|
+
"field_id": str(code_block_field.get("field_id") or "").strip() or None,
|
|
2311
|
+
"que_id": _coerce_positive_int(code_block_field.get("que_id")),
|
|
2312
|
+
"detected_issues": sorted(set(detected_issues)),
|
|
2313
|
+
"normalized_code_preview": normalized_code_preview,
|
|
2314
|
+
"would_update": bool(patch_payload and safe_auto_fix),
|
|
2315
|
+
"applied": False,
|
|
2316
|
+
"repair_mode": repair_mode,
|
|
2317
|
+
"preserved_output_targets": preserved_output_targets,
|
|
2318
|
+
"warnings": warnings,
|
|
2319
|
+
}
|
|
2320
|
+
if patch_payload and safe_auto_fix:
|
|
2321
|
+
update_fields.append(FieldUpdatePatch.model_validate(patch_payload))
|
|
2322
|
+
plans.append(plan)
|
|
2323
|
+
if not apply:
|
|
2324
|
+
return {
|
|
2325
|
+
"status": "success",
|
|
2326
|
+
"error_code": None,
|
|
2327
|
+
"recoverable": False,
|
|
2328
|
+
"message": "planned code block repair",
|
|
2329
|
+
"normalized_args": {"app_key": app_key, "field": requested_selector or None, "apply": False},
|
|
2330
|
+
"missing_fields": [],
|
|
2331
|
+
"allowed_values": {},
|
|
2332
|
+
"details": {},
|
|
2333
|
+
"request_id": None,
|
|
2334
|
+
"suggested_next_call": None,
|
|
2335
|
+
"noop": not bool(update_fields),
|
|
2336
|
+
"warnings": [],
|
|
2337
|
+
"verification": {
|
|
2338
|
+
"app_exists": True,
|
|
2339
|
+
"code_block_fields_scanned": len(plans),
|
|
2340
|
+
"would_update": bool(update_fields),
|
|
2341
|
+
},
|
|
2342
|
+
"verified": True,
|
|
2343
|
+
"app_key": app_key,
|
|
2344
|
+
"apply": False,
|
|
2345
|
+
"repair_plan": plans,
|
|
2346
|
+
"would_update_fields": [plan["field_name"] for plan in plans if plan["would_update"]],
|
|
2347
|
+
}
|
|
2348
|
+
if not update_fields:
|
|
2349
|
+
return {
|
|
2350
|
+
"status": "success",
|
|
2351
|
+
"error_code": None,
|
|
2352
|
+
"recoverable": False,
|
|
2353
|
+
"message": "no safe code block repairs were required",
|
|
2354
|
+
"normalized_args": {"app_key": app_key, "field": requested_selector or None, "apply": True},
|
|
2355
|
+
"missing_fields": [],
|
|
2356
|
+
"allowed_values": {},
|
|
2357
|
+
"details": {},
|
|
2358
|
+
"request_id": None,
|
|
2359
|
+
"suggested_next_call": None,
|
|
2360
|
+
"noop": True,
|
|
2361
|
+
"warnings": [],
|
|
2362
|
+
"verification": {
|
|
2363
|
+
"app_exists": True,
|
|
2364
|
+
"code_block_fields_scanned": len(plans),
|
|
2365
|
+
"would_update": False,
|
|
2366
|
+
"applied": False,
|
|
2367
|
+
},
|
|
2368
|
+
"verified": True,
|
|
2369
|
+
"app_key": app_key,
|
|
2370
|
+
"apply": True,
|
|
2371
|
+
"repair_plan": plans,
|
|
2372
|
+
"applied_fields": [],
|
|
2373
|
+
}
|
|
2374
|
+
apply_result = self.app_schema_apply(
|
|
2375
|
+
profile=profile,
|
|
2376
|
+
app_key=app_key,
|
|
2377
|
+
package_tag_id=None,
|
|
2378
|
+
app_name="",
|
|
2379
|
+
publish=True,
|
|
2380
|
+
add_fields=[],
|
|
2381
|
+
update_fields=update_fields,
|
|
2382
|
+
remove_fields=[],
|
|
2383
|
+
)
|
|
2384
|
+
if apply_result.get("status") == "failed":
|
|
2385
|
+
return apply_result
|
|
2386
|
+
try:
|
|
2387
|
+
reread = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
2388
|
+
verified_fields = cast(list[dict[str, Any]], reread["parsed"].get("fields") or [])
|
|
2389
|
+
verified_lookup = _build_public_field_lookup(verified_fields)
|
|
2390
|
+
for plan in plans:
|
|
2391
|
+
selector = plan["field_id"] or plan["field_name"] or plan["que_id"]
|
|
2392
|
+
if selector is None:
|
|
2393
|
+
continue
|
|
2394
|
+
try:
|
|
2395
|
+
verified_field = _resolve_public_field(selector, field_lookup=verified_lookup)
|
|
2396
|
+
except ValueError:
|
|
2397
|
+
continue
|
|
2398
|
+
verified_binding = _normalize_code_block_binding(verified_field.get("code_block_binding"))
|
|
2399
|
+
verified_config = _normalize_code_block_config(verified_field.get("code_block_config") or verified_field.get("config") or {}) or {}
|
|
2400
|
+
verified_code = str((verified_binding or {}).get("code") or "")
|
|
2401
|
+
verified_content = str(verified_config.get("code_content") or "")
|
|
2402
|
+
if "const qf_output =" not in verified_code and "let qf_output =" not in verified_code and "const qf_output =" not in verified_content and "let qf_output =" not in verified_content:
|
|
2403
|
+
if plan["would_update"]:
|
|
2404
|
+
plan["applied"] = True
|
|
2405
|
+
applied_fields.append(plan["field_name"])
|
|
2406
|
+
except (QingflowApiError, RuntimeError):
|
|
2407
|
+
pass
|
|
2408
|
+
apply_result["message"] = "repaired code block fields"
|
|
2409
|
+
apply_result["apply"] = True
|
|
2410
|
+
apply_result["repair_plan"] = plans
|
|
2411
|
+
apply_result["applied_fields"] = applied_fields
|
|
2412
|
+
apply_result["verification"] = {
|
|
2413
|
+
**(apply_result.get("verification") if isinstance(apply_result.get("verification"), dict) else {}),
|
|
2414
|
+
"code_block_fields_scanned": len(plans),
|
|
2415
|
+
"would_update": bool(update_fields),
|
|
2416
|
+
"applied": bool(applied_fields),
|
|
2417
|
+
}
|
|
2418
|
+
return apply_result
|
|
2419
|
+
|
|
2091
2420
|
def app_get_layout(self, *, profile: str, app_key: str) -> JSONObject:
|
|
2092
2421
|
result = self.app_read_layout_summary(profile=profile, app_key=app_key)
|
|
2093
2422
|
if result.get("status") == "success":
|
|
@@ -3338,18 +3667,33 @@ class AiBuilderFacade:
|
|
|
3338
3667
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3339
3668
|
)
|
|
3340
3669
|
|
|
3670
|
+
normalized_code_block_fields: list[str] = []
|
|
3341
3671
|
try:
|
|
3672
|
+
current_fields, normalized_code_block_fields = _normalize_and_validate_code_block_fields_for_write(fields=current_fields)
|
|
3342
3673
|
current_fields, compiled_question_relations = _compile_code_block_binding_fields(
|
|
3343
3674
|
fields=current_fields,
|
|
3344
3675
|
current_schema=schema_result,
|
|
3345
3676
|
)
|
|
3677
|
+
_ensure_code_block_targets_compiled_as_relation_defaults(fields=current_fields)
|
|
3678
|
+
except _CodeBlockValidationError as error:
|
|
3679
|
+
return _failed(
|
|
3680
|
+
error.error_code,
|
|
3681
|
+
error.message,
|
|
3682
|
+
normalized_args=normalized_args,
|
|
3683
|
+
details={"app_key": target.app_key, **error.details},
|
|
3684
|
+
suggested_next_call=_code_block_repair_suggested_next_call(
|
|
3685
|
+
profile=profile,
|
|
3686
|
+
app_key=target.app_key,
|
|
3687
|
+
field_name=str(error.details.get("field_name") or "").strip() or None,
|
|
3688
|
+
),
|
|
3689
|
+
)
|
|
3346
3690
|
except ValueError as error:
|
|
3347
3691
|
return _failed(
|
|
3348
3692
|
"CODE_BLOCK_BINDING_INVALID",
|
|
3349
3693
|
str(error),
|
|
3350
3694
|
normalized_args=normalized_args,
|
|
3351
3695
|
details={"app_key": target.app_key},
|
|
3352
|
-
suggested_next_call=
|
|
3696
|
+
suggested_next_call=_code_block_repair_suggested_next_call(profile=profile, app_key=target.app_key),
|
|
3353
3697
|
)
|
|
3354
3698
|
|
|
3355
3699
|
q_linker_schema_context = deepcopy(schema_result)
|
|
@@ -3375,8 +3719,19 @@ class AiBuilderFacade:
|
|
|
3375
3719
|
if not relation_limit_verified
|
|
3376
3720
|
else []
|
|
3377
3721
|
)
|
|
3722
|
+
code_block_normalization_warnings = (
|
|
3723
|
+
[
|
|
3724
|
+
_warning(
|
|
3725
|
+
"CODE_BLOCK_OUTPUT_ASSIGNMENT_NORMALIZED",
|
|
3726
|
+
"normalized code block qf_output assignment before schema write",
|
|
3727
|
+
field_names=normalized_code_block_fields,
|
|
3728
|
+
)
|
|
3729
|
+
]
|
|
3730
|
+
if normalized_code_block_fields
|
|
3731
|
+
else []
|
|
3732
|
+
)
|
|
3378
3733
|
|
|
3379
|
-
if not added and not updated and not removed and not bool(resolved.get("created")):
|
|
3734
|
+
if not added and not updated and not removed and not normalized_code_block_fields and not bool(resolved.get("created")):
|
|
3380
3735
|
tag_ids_after = _coerce_int_list((self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}).get("tagIds"))
|
|
3381
3736
|
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
3382
3737
|
response = {
|
|
@@ -3391,7 +3746,7 @@ class AiBuilderFacade:
|
|
|
3391
3746
|
"request_id": None,
|
|
3392
3747
|
"suggested_next_call": None if package_attached is not False else {"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": package_tag_id, "app_key": target.app_key}},
|
|
3393
3748
|
"noop": not bool(visual_result.get("updated")),
|
|
3394
|
-
"warnings": relation_warnings,
|
|
3749
|
+
"warnings": relation_warnings + code_block_normalization_warnings,
|
|
3395
3750
|
"verification": {
|
|
3396
3751
|
"fields_verified": True,
|
|
3397
3752
|
"relation_field_limit_verified": relation_limit_verified,
|
|
@@ -3407,6 +3762,9 @@ class AiBuilderFacade:
|
|
|
3407
3762
|
"package_attached": package_attached,
|
|
3408
3763
|
}
|
|
3409
3764
|
response["details"]["relation_field_count"] = relation_field_count
|
|
3765
|
+
if normalized_code_block_fields:
|
|
3766
|
+
response["normalized_code_block_output_assignment"] = True
|
|
3767
|
+
response["normalized_code_block_fields"] = normalized_code_block_fields
|
|
3410
3768
|
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
3411
3769
|
return finalize(self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response))
|
|
3412
3770
|
|
|
@@ -3473,16 +3831,30 @@ class AiBuilderFacade:
|
|
|
3473
3831
|
rebound_layout = cast(dict[str, Any], rebound_parsed.get("layout") or {"root_rows": [], "sections": []})
|
|
3474
3832
|
_overlay_code_block_binding_fields(target_fields=rebound_fields, source_fields=current_fields)
|
|
3475
3833
|
_overlay_q_linker_binding_fields(target_fields=rebound_fields, source_fields=current_fields)
|
|
3834
|
+
rebound_fields, _normalized_rebound_code_blocks = _normalize_and_validate_code_block_fields_for_write(fields=rebound_fields)
|
|
3476
3835
|
rebound_fields, compiled_question_relations = _compile_code_block_binding_fields(
|
|
3477
3836
|
fields=rebound_fields,
|
|
3478
3837
|
current_schema=rebound_schema,
|
|
3479
3838
|
)
|
|
3839
|
+
_ensure_code_block_targets_compiled_as_relation_defaults(fields=rebound_fields)
|
|
3480
3840
|
rebound_q_linker_schema = deepcopy(rebound_schema)
|
|
3481
3841
|
rebound_q_linker_schema["questionRelations"] = deepcopy(compiled_question_relations)
|
|
3482
3842
|
rebound_fields, compiled_question_relations = _compile_q_linker_binding_fields(
|
|
3483
3843
|
fields=rebound_fields,
|
|
3484
3844
|
current_schema=rebound_q_linker_schema,
|
|
3485
3845
|
)
|
|
3846
|
+
except _CodeBlockValidationError as error:
|
|
3847
|
+
return _failed(
|
|
3848
|
+
error.error_code,
|
|
3849
|
+
error.message,
|
|
3850
|
+
normalized_args=normalized_args,
|
|
3851
|
+
details={"app_key": target.app_key, **error.details},
|
|
3852
|
+
suggested_next_call=_code_block_repair_suggested_next_call(
|
|
3853
|
+
profile=profile,
|
|
3854
|
+
app_key=target.app_key,
|
|
3855
|
+
field_name=str(error.details.get("field_name") or "").strip() or None,
|
|
3856
|
+
),
|
|
3857
|
+
)
|
|
3486
3858
|
except ValueError as error:
|
|
3487
3859
|
return _failed(
|
|
3488
3860
|
"Q_LINKER_BINDING_INVALID" if "q_linker" in str(error) else "CODE_BLOCK_BINDING_INVALID",
|
|
@@ -3544,7 +3916,7 @@ class AiBuilderFacade:
|
|
|
3544
3916
|
"request_id": None,
|
|
3545
3917
|
"suggested_next_call": None,
|
|
3546
3918
|
"noop": False,
|
|
3547
|
-
"warnings": relation_warnings,
|
|
3919
|
+
"warnings": relation_warnings + code_block_normalization_warnings,
|
|
3548
3920
|
"verification": {
|
|
3549
3921
|
"fields_verified": False,
|
|
3550
3922
|
"package_attached": None,
|
|
@@ -3565,6 +3937,9 @@ class AiBuilderFacade:
|
|
|
3565
3937
|
"package_attached": None,
|
|
3566
3938
|
}
|
|
3567
3939
|
response["details"]["relation_field_count"] = relation_field_count
|
|
3940
|
+
if normalized_code_block_fields:
|
|
3941
|
+
response["normalized_code_block_output_assignment"] = True
|
|
3942
|
+
response["normalized_code_block_fields"] = normalized_code_block_fields
|
|
3568
3943
|
if schema_readback_delayed:
|
|
3569
3944
|
response["verification"]["schema_readback_delayed"] = True
|
|
3570
3945
|
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
@@ -8785,6 +9160,7 @@ def _field_patch_to_internal(patch: FieldPatch) -> dict[str, Any]:
|
|
|
8785
9160
|
"q_linker_binding": q_linker_binding,
|
|
8786
9161
|
"code_block_config": code_block_config,
|
|
8787
9162
|
"code_block_binding": code_block_binding,
|
|
9163
|
+
"_explicit_code_block_binding": code_block_binding is not None,
|
|
8788
9164
|
"config": deepcopy(remote_lookup_config) if remote_lookup_config is not None else (deepcopy(code_block_config) if code_block_config is not None else {}),
|
|
8789
9165
|
"auto_trigger": patch.auto_trigger,
|
|
8790
9166
|
"custom_button_text_enabled": patch.custom_button_text_enabled,
|
|
@@ -9011,6 +9387,7 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
9011
9387
|
field["config"] = deepcopy(payload["code_block_config"])
|
|
9012
9388
|
if "code_block_binding" in payload:
|
|
9013
9389
|
field["code_block_binding"] = payload["code_block_binding"]
|
|
9390
|
+
field["_explicit_code_block_binding"] = True
|
|
9014
9391
|
if "auto_trigger" in payload:
|
|
9015
9392
|
field["auto_trigger"] = payload["auto_trigger"]
|
|
9016
9393
|
if "custom_button_text_enabled" in payload:
|
|
@@ -9087,7 +9464,225 @@ def _strip_code_block_generated_input_prelude(code_content: str) -> str:
|
|
|
9087
9464
|
|
|
9088
9465
|
|
|
9089
9466
|
def _normalize_code_block_output_assignment(code_content: str) -> str:
|
|
9090
|
-
return re.sub(r"(
|
|
9467
|
+
return re.sub(r"(?<![A-Za-z0-9_$])(?:const|let)\s+qf_output\s*=", "qf_output =", code_content)
|
|
9468
|
+
|
|
9469
|
+
|
|
9470
|
+
@dataclass
|
|
9471
|
+
class _CodeBlockValidationError(ValueError):
|
|
9472
|
+
error_code: str
|
|
9473
|
+
message: str
|
|
9474
|
+
details: JSONObject = field(default_factory=dict)
|
|
9475
|
+
|
|
9476
|
+
def __str__(self) -> str:
|
|
9477
|
+
return self.message
|
|
9478
|
+
|
|
9479
|
+
|
|
9480
|
+
def _code_block_has_effective_output_assignment(code_content: str) -> bool:
|
|
9481
|
+
return bool(re.search(r"(?<![A-Za-z0-9_$])qf_output\s*=", str(code_content or "")))
|
|
9482
|
+
|
|
9483
|
+
|
|
9484
|
+
def _field_selector_payload_for_field(field: dict[str, Any]) -> dict[str, Any]:
|
|
9485
|
+
payload: dict[str, Any] = {}
|
|
9486
|
+
field_id = str(field.get("field_id") or "").strip()
|
|
9487
|
+
que_id = _coerce_positive_int(field.get("que_id"))
|
|
9488
|
+
name = str(field.get("name") or "").strip()
|
|
9489
|
+
if field_id:
|
|
9490
|
+
payload["field_id"] = field_id
|
|
9491
|
+
elif que_id is not None:
|
|
9492
|
+
payload["que_id"] = que_id
|
|
9493
|
+
elif name:
|
|
9494
|
+
payload["name"] = name
|
|
9495
|
+
return payload
|
|
9496
|
+
|
|
9497
|
+
|
|
9498
|
+
def _field_selector_payload_for_selector(value: Any) -> dict[str, Any]:
|
|
9499
|
+
if isinstance(value, dict):
|
|
9500
|
+
payload = {
|
|
9501
|
+
"field_id": str(value.get("field_id") or "").strip() or None,
|
|
9502
|
+
"que_id": _coerce_positive_int(value.get("que_id")),
|
|
9503
|
+
"name": str(value.get("name") or "").strip() or None,
|
|
9504
|
+
}
|
|
9505
|
+
return {key: item for key, item in payload.items() if item is not None}
|
|
9506
|
+
if isinstance(value, str) and value.strip():
|
|
9507
|
+
return {"name": value.strip()}
|
|
9508
|
+
return {}
|
|
9509
|
+
|
|
9510
|
+
|
|
9511
|
+
def _code_block_field_display_name(field: dict[str, Any]) -> str:
|
|
9512
|
+
name = str(field.get("name") or "").strip()
|
|
9513
|
+
if name:
|
|
9514
|
+
return name
|
|
9515
|
+
field_id = str(field.get("field_id") or "").strip()
|
|
9516
|
+
if field_id:
|
|
9517
|
+
return field_id
|
|
9518
|
+
que_id = _coerce_positive_int(field.get("que_id"))
|
|
9519
|
+
if que_id is not None:
|
|
9520
|
+
return str(que_id)
|
|
9521
|
+
return "未命名代码块"
|
|
9522
|
+
|
|
9523
|
+
|
|
9524
|
+
def _public_code_block_binding_payload(binding: dict[str, Any]) -> dict[str, Any]:
|
|
9525
|
+
return {
|
|
9526
|
+
"inputs": [
|
|
9527
|
+
{
|
|
9528
|
+
"field": _field_selector_payload_for_selector(input_item.get("field")),
|
|
9529
|
+
"var": input_item.get("var"),
|
|
9530
|
+
}
|
|
9531
|
+
for input_item in cast(list[dict[str, Any]], binding.get("inputs") or [])
|
|
9532
|
+
if _field_selector_payload_for_selector(input_item.get("field"))
|
|
9533
|
+
],
|
|
9534
|
+
"code": str(binding.get("code") or ""),
|
|
9535
|
+
"auto_trigger": binding.get("auto_trigger"),
|
|
9536
|
+
"custom_button_text_enabled": binding.get("custom_button_text_enabled"),
|
|
9537
|
+
"custom_button_text": binding.get("custom_button_text"),
|
|
9538
|
+
"outputs": [
|
|
9539
|
+
{
|
|
9540
|
+
"alias": str(output.get("alias") or "").strip(),
|
|
9541
|
+
"path": str(output.get("path") or "").strip(),
|
|
9542
|
+
"target_field": _field_selector_payload_for_selector(output.get("target_field")),
|
|
9543
|
+
}
|
|
9544
|
+
for output in cast(list[dict[str, Any]], binding.get("outputs") or [])
|
|
9545
|
+
if str(output.get("alias") or "").strip()
|
|
9546
|
+
and str(output.get("path") or "").strip()
|
|
9547
|
+
and _field_selector_payload_for_selector(output.get("target_field"))
|
|
9548
|
+
],
|
|
9549
|
+
}
|
|
9550
|
+
|
|
9551
|
+
|
|
9552
|
+
def _raw_field_selector_present(value: Any) -> bool:
|
|
9553
|
+
if isinstance(value, str):
|
|
9554
|
+
return bool(value.strip())
|
|
9555
|
+
if not isinstance(value, dict):
|
|
9556
|
+
return False
|
|
9557
|
+
return bool(
|
|
9558
|
+
str(value.get("field_id") or "").strip()
|
|
9559
|
+
or _coerce_positive_int(value.get("que_id"))
|
|
9560
|
+
or str(value.get("name") or value.get("title") or value.get("label") or "").strip()
|
|
9561
|
+
)
|
|
9562
|
+
|
|
9563
|
+
|
|
9564
|
+
def _validate_code_block_alias_config(
|
|
9565
|
+
*,
|
|
9566
|
+
field_name: str,
|
|
9567
|
+
raw_binding: dict[str, Any] | None,
|
|
9568
|
+
normalized_config: dict[str, Any] | None,
|
|
9569
|
+
) -> None:
|
|
9570
|
+
raw_outputs = raw_binding.get("outputs") if isinstance(raw_binding, dict) and isinstance(raw_binding.get("outputs"), list) else []
|
|
9571
|
+
for index, raw_output in enumerate(cast(list[Any], raw_outputs)):
|
|
9572
|
+
if not isinstance(raw_output, dict):
|
|
9573
|
+
continue
|
|
9574
|
+
raw_target = raw_output.get("target_field", raw_output.get("targetField"))
|
|
9575
|
+
if not _raw_field_selector_present(raw_target):
|
|
9576
|
+
continue
|
|
9577
|
+
alias = str(raw_output.get("alias", raw_output.get("alias_name", raw_output.get("aliasName", ""))) or "").strip()
|
|
9578
|
+
path = str(raw_output.get("path", raw_output.get("alias_path", raw_output.get("aliasPath", ""))) or "").strip()
|
|
9579
|
+
if alias and path:
|
|
9580
|
+
continue
|
|
9581
|
+
raise _CodeBlockValidationError(
|
|
9582
|
+
"CODE_BLOCK_ALIAS_REQUIRED",
|
|
9583
|
+
f"code_block field '{field_name}' requires non-empty alias and path for output target binding",
|
|
9584
|
+
details={"field_name": field_name, "output_index": index},
|
|
9585
|
+
)
|
|
9586
|
+
normalized_aliases = cast(list[dict[str, Any]], (normalized_config or {}).get("result_alias_path") or [])
|
|
9587
|
+
for index, alias_item in enumerate(normalized_aliases):
|
|
9588
|
+
alias_name = str(alias_item.get("alias_name") or "").strip()
|
|
9589
|
+
alias_path = str(alias_item.get("alias_path") or "").strip()
|
|
9590
|
+
if alias_name and alias_path:
|
|
9591
|
+
continue
|
|
9592
|
+
raise _CodeBlockValidationError(
|
|
9593
|
+
"CODE_BLOCK_ALIAS_REQUIRED",
|
|
9594
|
+
f"code_block field '{field_name}' contains an empty output alias configuration",
|
|
9595
|
+
details={"field_name": field_name, "alias_index": index},
|
|
9596
|
+
)
|
|
9597
|
+
|
|
9598
|
+
|
|
9599
|
+
def _normalize_and_validate_code_block_fields_for_write(
|
|
9600
|
+
*,
|
|
9601
|
+
fields: list[dict[str, Any]],
|
|
9602
|
+
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
9603
|
+
next_fields = deepcopy(fields)
|
|
9604
|
+
normalized_fields: list[str] = []
|
|
9605
|
+
for field in next_fields:
|
|
9606
|
+
if not isinstance(field, dict) or field.get("type") != FieldType.code_block.value:
|
|
9607
|
+
continue
|
|
9608
|
+
field_name = _code_block_field_display_name(field)
|
|
9609
|
+
raw_binding = field.get("code_block_binding") if isinstance(field.get("code_block_binding"), dict) else None
|
|
9610
|
+
binding = _normalize_code_block_binding(raw_binding)
|
|
9611
|
+
current_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {}) or None
|
|
9612
|
+
_validate_code_block_alias_config(field_name=field_name, raw_binding=raw_binding, normalized_config=current_config)
|
|
9613
|
+
normalized = False
|
|
9614
|
+
if binding is not None:
|
|
9615
|
+
raw_code = str(binding.get("code") or "")
|
|
9616
|
+
normalized_code = _normalize_code_block_output_assignment(raw_code)
|
|
9617
|
+
if normalized_code != raw_code:
|
|
9618
|
+
normalized = True
|
|
9619
|
+
binding["code"] = normalized_code
|
|
9620
|
+
field["code_block_binding"] = binding
|
|
9621
|
+
if current_config is not None:
|
|
9622
|
+
raw_code_content = str(current_config.get("code_content") or "")
|
|
9623
|
+
normalized_code_content = _normalize_code_block_output_assignment(raw_code_content)
|
|
9624
|
+
if normalized_code_content != raw_code_content:
|
|
9625
|
+
normalized = True
|
|
9626
|
+
current_config["code_content"] = normalized_code_content
|
|
9627
|
+
field["code_block_config"] = current_config
|
|
9628
|
+
field["config"] = deepcopy(current_config)
|
|
9629
|
+
conflicting_binding_and_config = False
|
|
9630
|
+
if binding is not None and current_config is not None:
|
|
9631
|
+
current_config_code = str(current_config.get("code_content") or "")
|
|
9632
|
+
compiled_body = _strip_code_block_generated_input_prelude(current_config_code)
|
|
9633
|
+
if current_config_code.strip() and compiled_body != str(binding.get("code") or ""):
|
|
9634
|
+
conflicting_binding_and_config = True
|
|
9635
|
+
has_outputs = bool((binding or {}).get("outputs")) or bool((current_config or {}).get("result_alias_path"))
|
|
9636
|
+
effective_code = ""
|
|
9637
|
+
if binding is not None:
|
|
9638
|
+
effective_code = str(binding.get("code") or "")
|
|
9639
|
+
elif current_config is not None:
|
|
9640
|
+
effective_code = _strip_code_block_generated_input_prelude(str(current_config.get("code_content") or ""))
|
|
9641
|
+
if has_outputs and not conflicting_binding_and_config and not _code_block_has_effective_output_assignment(effective_code):
|
|
9642
|
+
raise _CodeBlockValidationError(
|
|
9643
|
+
"CODE_BLOCK_OUTPUT_ASSIGNMENT_MISSING",
|
|
9644
|
+
f"code_block field '{field_name}' must assign to qf_output when outputs are configured",
|
|
9645
|
+
details={"field_name": field_name},
|
|
9646
|
+
)
|
|
9647
|
+
if normalized:
|
|
9648
|
+
normalized_fields.append(field_name)
|
|
9649
|
+
return next_fields, sorted(set(normalized_fields))
|
|
9650
|
+
|
|
9651
|
+
|
|
9652
|
+
def _ensure_code_block_targets_compiled_as_relation_defaults(*, fields: list[dict[str, Any]]) -> None:
|
|
9653
|
+
for field in fields:
|
|
9654
|
+
if not isinstance(field, dict) or field.get("type") != FieldType.code_block.value:
|
|
9655
|
+
continue
|
|
9656
|
+
field_name = _code_block_field_display_name(field)
|
|
9657
|
+
binding = _normalize_code_block_binding(field.get("code_block_binding"))
|
|
9658
|
+
if binding is None:
|
|
9659
|
+
continue
|
|
9660
|
+
for output_index, output in enumerate(cast(list[dict[str, Any]], binding.get("outputs") or [])):
|
|
9661
|
+
target_payload = output.get("target_field")
|
|
9662
|
+
if not isinstance(target_payload, dict):
|
|
9663
|
+
continue
|
|
9664
|
+
target_field = _resolve_field_selector_with_uniqueness(
|
|
9665
|
+
fields=fields,
|
|
9666
|
+
selector_payload=target_payload,
|
|
9667
|
+
location=f"code_block_binding.outputs[{output_index}].target_field",
|
|
9668
|
+
)
|
|
9669
|
+
if _coerce_any_int(target_field.get("default_type")) == DEFAULT_TYPE_RELATION:
|
|
9670
|
+
continue
|
|
9671
|
+
raise _CodeBlockValidationError(
|
|
9672
|
+
"CODE_BLOCK_TARGET_DEFAULT_INVALID",
|
|
9673
|
+
f"code_block field '{field_name}' output target '{_code_block_field_display_name(target_field)}' is not compiled as relation default",
|
|
9674
|
+
details={
|
|
9675
|
+
"field_name": field_name,
|
|
9676
|
+
"target_field_name": _code_block_field_display_name(target_field),
|
|
9677
|
+
},
|
|
9678
|
+
)
|
|
9679
|
+
|
|
9680
|
+
|
|
9681
|
+
def _code_block_repair_suggested_next_call(*, profile: str, app_key: str, field_name: str | None = None) -> JSONObject:
|
|
9682
|
+
arguments: JSONObject = {"profile": profile, "app_key": app_key}
|
|
9683
|
+
if field_name:
|
|
9684
|
+
arguments["field"] = field_name
|
|
9685
|
+
return {"tool_name": "app_repair_code_blocks", "arguments": arguments}
|
|
9091
9686
|
|
|
9092
9687
|
|
|
9093
9688
|
def _ensure_field_temp_ids(fields: list[dict[str, Any]]) -> None:
|
|
@@ -9122,12 +9717,21 @@ def _compile_code_block_binding_fields(
|
|
|
9122
9717
|
binding = _normalize_code_block_binding(field.get("code_block_binding"))
|
|
9123
9718
|
if binding is None:
|
|
9124
9719
|
continue
|
|
9720
|
+
explicit_binding = bool(field.get("_explicit_code_block_binding"))
|
|
9721
|
+
binding_outputs = cast(list[dict[str, Any]], binding.get("outputs") or [])
|
|
9722
|
+
binding_has_target_bindings = any(
|
|
9723
|
+
isinstance(output.get("target_field"), dict) and any(cast(dict[str, Any], output.get("target_field") or {}).values())
|
|
9724
|
+
for output in binding_outputs
|
|
9725
|
+
)
|
|
9125
9726
|
current_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {}) or {
|
|
9126
9727
|
"config_mode": 1,
|
|
9127
9728
|
"code_content": "",
|
|
9128
9729
|
"being_hide_on_form": False,
|
|
9129
9730
|
"result_alias_path": [],
|
|
9130
9731
|
}
|
|
9732
|
+
if binding_outputs and not binding_has_target_bindings and not explicit_binding:
|
|
9733
|
+
field["code_block_binding"] = None
|
|
9734
|
+
continue
|
|
9131
9735
|
low_level_code = _strip_code_block_generated_input_prelude(str(current_config.get("code_content") or ""))
|
|
9132
9736
|
if str(current_config.get("code_content") or "").strip() and low_level_code != str(binding.get("code") or ""):
|
|
9133
9737
|
raise ValueError(f"code_block field '{field.get('name')}' has conflicting code_block_config.code_content and code_block_binding.code")
|