@qingflow-tech/qingflow-cli 1.1.44-beta.2 → 1.1.45-beta.1
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 +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/app_form_validator.py +81 -0
- package/src/qingflow_mcp/cli/commands/builder.py +204 -1
- package/src/qingflow_mcp/public_surface.py +2 -0
- package/src/qingflow_mcp/response_trim.py +6 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +58 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Qingflow CLI 是轻流面向开发者和 AI Agent 的命令行工具。它把轻
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install @qingflow-tech/qingflow-cli@1.1.
|
|
10
|
+
npm install @qingflow-tech/qingflow-cli@1.1.45-beta.1
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
推荐从官网复制这一条一键安装 Qingflow CLI:
|
|
@@ -21,7 +21,7 @@ npx qingflow-cli@latest install
|
|
|
21
21
|
## 运行
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npx -y -p @qingflow-tech/qingflow-cli@1.1.
|
|
24
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.45-beta.1 qingflow
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
全局安装后可以直接运行:
|
|
@@ -62,7 +62,7 @@ qingflow-skills install --agent codex --scope user
|
|
|
62
62
|
如果你通过一次性 `npx -p` 执行 skills 安装,建议加 `--copy`,避免 symlink 指向 npm 临时执行缓存:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
npx -y -p @qingflow-tech/qingflow-cli@1.1.
|
|
65
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.45-beta.1 qingflow-skills install --agent codex --scope user --copy
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
`qingflow-skills install` 默认使用 symlink,且遇到同名 skill 不会覆盖;只有显式传入 `--force` 才会替换。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qingflow-tech/qingflow-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.45-beta.1",
|
|
4
4
|
"description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/pyproject.toml
CHANGED
|
@@ -24,6 +24,8 @@ _INTEGRATION_OUTPUT_TARGET_FIELD_TYPES = {
|
|
|
24
24
|
}
|
|
25
25
|
_Q_LINKER_TEMPLATE_RE = re.compile(r"\{\{(?P<key>[^{}\s]+)\}\}")
|
|
26
26
|
_FORMULA_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
27
|
+
#: 公式字段引用正则(#queId_<id>$,支持表格嵌套 <id>.<id>)
|
|
28
|
+
_FORMULA_FIELD_REF_RE = re.compile(r"#queId_([^$]+)\$")
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class AppFormValidationError(ValueError):
|
|
@@ -460,3 +462,82 @@ def validate_declaration(
|
|
|
460
462
|
issues = [{"pointer": pointer, "message": message} for pointer, message in sorted(found)]
|
|
461
463
|
raise AppFormValidationError(issues, schema_version=pinned, api_version=api_version)
|
|
462
464
|
return {"valid": True, "schemaVersion": pinned, "apiVersion": api_version, "fieldTypes": list(types)}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def validate_formula_expression(
|
|
468
|
+
formula: str,
|
|
469
|
+
fields: list[dict[str, Any]] | None = None,
|
|
470
|
+
) -> list[dict[str, str]]:
|
|
471
|
+
"""校验单条公式表达式:函数名合法 + 字段引用存在性。不依赖整份 AppForm,不调后端。
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
formula: 公式文本,如 ``CONCAT(#queId_101$, ":", #queId_102$)``。
|
|
475
|
+
fields: 字段清单 ``[{queId, name}]``(兼容 ``que_id``/``id``);提供时校验字段引用存在性,
|
|
476
|
+
不提供时跳过字段引用校验。
|
|
477
|
+
Returns:
|
|
478
|
+
错误列表 ``[{pointer, message}]``,空列表表示本地语法通过。
|
|
479
|
+
"""
|
|
480
|
+
errors: list[dict[str, str]] = []
|
|
481
|
+
if not isinstance(formula, str) or not formula.strip():
|
|
482
|
+
errors.append({"pointer": "formula", "message": "formula is empty"})
|
|
483
|
+
return errors
|
|
484
|
+
|
|
485
|
+
# 函数名合法性(复用 AppForm formula Schema 的函数目录)
|
|
486
|
+
supported = _supported_formula_functions("v1alpha1")
|
|
487
|
+
unknown = sorted(
|
|
488
|
+
name for name in _formula_function_calls(formula) if name.upper() not in supported
|
|
489
|
+
)
|
|
490
|
+
for name in unknown:
|
|
491
|
+
errors.append({"pointer": "formula", "message": f"unsupported formula function '{name}'"})
|
|
492
|
+
|
|
493
|
+
# 字段引用存在性(#queId_<id>$,支持表格嵌套 <id>.<id>,按叶子 id 与原始 id 双重匹配)
|
|
494
|
+
if fields:
|
|
495
|
+
known_ids = {
|
|
496
|
+
str(identifier)
|
|
497
|
+
for field in fields
|
|
498
|
+
if isinstance(field, dict)
|
|
499
|
+
for identifier in (
|
|
500
|
+
field.get("queId"),
|
|
501
|
+
field.get("que_id"),
|
|
502
|
+
field.get("id"),
|
|
503
|
+
)
|
|
504
|
+
if identifier is not None and str(identifier) != ""
|
|
505
|
+
}
|
|
506
|
+
for match in _FORMULA_FIELD_REF_RE.finditer(formula):
|
|
507
|
+
raw_id = match.group(1)
|
|
508
|
+
leaf_id = raw_id.split(".")[-1]
|
|
509
|
+
if leaf_id not in known_ids and raw_id not in known_ids:
|
|
510
|
+
errors.append(
|
|
511
|
+
{
|
|
512
|
+
"pointer": "formula",
|
|
513
|
+
"message": f"field reference '{match.group(0)}' refers to unknown queId {raw_id}",
|
|
514
|
+
}
|
|
515
|
+
)
|
|
516
|
+
return errors
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def validate_code_block_static(code: str) -> list[dict[str, str]]:
|
|
520
|
+
"""校验代码块静态结构:qf_output 赋值 + 基本语法。不调后端。
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
code: 代码文本。
|
|
524
|
+
Returns:
|
|
525
|
+
错误列表 ``[{pointer, message}]``,空列表表示本地静态校验通过。
|
|
526
|
+
"""
|
|
527
|
+
errors: list[dict[str, str]] = []
|
|
528
|
+
if not isinstance(code, str) or not code.strip():
|
|
529
|
+
errors.append({"pointer": "code", "message": "code is empty"})
|
|
530
|
+
return errors
|
|
531
|
+
|
|
532
|
+
# qf_output 必须直接赋值(不能用 const/let 声明);仅当代码涉及 outputs 绑定时才提示
|
|
533
|
+
has_qf_output = bool(re.search(r"(^|[^A-Za-z0-9_$])qf_output\s*=(?!=)", code))
|
|
534
|
+
has_outputs_binding = "outputs" in code
|
|
535
|
+
if not has_qf_output and has_outputs_binding:
|
|
536
|
+
errors.append(
|
|
537
|
+
{
|
|
538
|
+
"pointer": "code",
|
|
539
|
+
"message": "code declares outputs but does not assign qf_output; "
|
|
540
|
+
"use 'qf_output = {...}' (not const/let)",
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
return errors
|
|
@@ -2,9 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
from copy import deepcopy
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from ..context import CliContext
|
|
7
|
-
from ...app_form_validator import
|
|
8
|
+
from ...app_form_validator import (
|
|
9
|
+
AppFormValidationError,
|
|
10
|
+
validate_code_block_static,
|
|
11
|
+
validate_declaration,
|
|
12
|
+
validate_formula_expression,
|
|
13
|
+
)
|
|
8
14
|
from ..json_io import load_json_value
|
|
9
15
|
from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
|
|
10
16
|
|
|
@@ -123,6 +129,41 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
123
129
|
app_form_validate.add_argument("--schema-version", help="固定使用的 AppForm Schema 版本")
|
|
124
130
|
app_form_validate.set_defaults(handler=_handle_app_form_validate, format_hint="builder_summary", force_json_output=True)
|
|
125
131
|
|
|
132
|
+
app_form_validate_formula = app_form_subparsers.add_parser(
|
|
133
|
+
"validate-formula", help="校验公式表达式(本地语法 + 后端求值)"
|
|
134
|
+
)
|
|
135
|
+
app_form_validate_formula.add_argument("--app-key", required=True, help="目标应用 key")
|
|
136
|
+
app_form_validate_formula.add_argument("--formula", required=True, help="公式文本")
|
|
137
|
+
app_form_validate_formula.add_argument(
|
|
138
|
+
"--test-case",
|
|
139
|
+
required=True,
|
|
140
|
+
help="测试用例 JSON 文件路径,- 表示 stdin;格式 {queId: value}",
|
|
141
|
+
)
|
|
142
|
+
app_form_validate_formula.add_argument(
|
|
143
|
+
"--fields",
|
|
144
|
+
required=False,
|
|
145
|
+
help="字段清单 JSON 文件路径(用于本地字段引用校验);缺省时从后端拉取",
|
|
146
|
+
)
|
|
147
|
+
app_form_validate_formula.set_defaults(
|
|
148
|
+
handler=_handle_app_form_validate_formula, format_hint="builder_summary", force_json_output=True
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
app_form_validate_code_block = app_form_subparsers.add_parser(
|
|
152
|
+
"validate-code-block", help="校验代码块(本地静态 + 后端真跑)"
|
|
153
|
+
)
|
|
154
|
+
app_form_validate_code_block.add_argument("--app-key", required=True, help="目标应用 key")
|
|
155
|
+
app_form_validate_code_block.add_argument(
|
|
156
|
+
"--code", required=True, help="代码文本(或 @文件路径读取)"
|
|
157
|
+
)
|
|
158
|
+
app_form_validate_code_block.add_argument(
|
|
159
|
+
"--test-case",
|
|
160
|
+
required=True,
|
|
161
|
+
help="测试用例 JSON 文件路径,- 表示 stdin;格式 [{keyQueId, ordinal?, values}]",
|
|
162
|
+
)
|
|
163
|
+
app_form_validate_code_block.set_defaults(
|
|
164
|
+
handler=_handle_app_form_validate_code_block, format_hint="builder_summary", force_json_output=True
|
|
165
|
+
)
|
|
166
|
+
|
|
126
167
|
app = builder_subparsers.add_parser("app", help="应用")
|
|
127
168
|
app_subparsers = app.add_subparsers(dest="builder_app_command", required=True)
|
|
128
169
|
|
|
@@ -390,6 +431,168 @@ def _handle_app_form_validate(args: argparse.Namespace, _context: CliContext) ->
|
|
|
390
431
|
return {"category": "validation", "status": "success", "ok": True, **result, "message": "AppForm declaration is valid"}
|
|
391
432
|
|
|
392
433
|
|
|
434
|
+
def _handle_app_form_validate_formula(args: argparse.Namespace, context: CliContext) -> dict:
|
|
435
|
+
formula = args.formula
|
|
436
|
+
test_case = load_json_value(
|
|
437
|
+
args.test_case, option_name="--test-case", max_bytes=1 * 1024 * 1024, max_depth=32
|
|
438
|
+
)
|
|
439
|
+
if not isinstance(test_case, dict):
|
|
440
|
+
raise_config_error(
|
|
441
|
+
"--test-case must be a JSON object {queId: value}", error_code="INVALID_TEST_CASE"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# ① 本地语法校验(函数名 + 字段引用存在性)
|
|
445
|
+
fields = _load_fields_for_validation(args, context)
|
|
446
|
+
syntax_errors = validate_formula_expression(formula, fields)
|
|
447
|
+
|
|
448
|
+
# ② 后端求值校验(语法有错时不浪费后端调用)
|
|
449
|
+
evaluation = None
|
|
450
|
+
evaluation_error = None
|
|
451
|
+
if not syntax_errors:
|
|
452
|
+
result = context.builder.app_form_evaluate_formula(
|
|
453
|
+
profile=args.profile, app_key=args.app_key, formula=formula, test_case=test_case
|
|
454
|
+
)
|
|
455
|
+
evaluation = result.get("evaluation")
|
|
456
|
+
evaluation_error = result.get("evaluationError")
|
|
457
|
+
|
|
458
|
+
ok = bool(
|
|
459
|
+
not syntax_errors
|
|
460
|
+
and isinstance(evaluation, dict)
|
|
461
|
+
and evaluation.get("valid") is True
|
|
462
|
+
)
|
|
463
|
+
return {
|
|
464
|
+
"category": "validation",
|
|
465
|
+
"status": "success" if ok else "failed",
|
|
466
|
+
"ok": ok,
|
|
467
|
+
"syntaxErrors": syntax_errors,
|
|
468
|
+
"evaluation": evaluation,
|
|
469
|
+
"evaluationError": evaluation_error,
|
|
470
|
+
"message": "formula is valid" if ok else "formula validation failed",
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _handle_app_form_validate_code_block(args: argparse.Namespace, context: CliContext) -> dict:
|
|
475
|
+
code = _read_code_arg(args.code)
|
|
476
|
+
test_case = load_json_value(
|
|
477
|
+
args.test_case, option_name="--test-case", max_bytes=1 * 1024 * 1024, max_depth=32
|
|
478
|
+
)
|
|
479
|
+
if not isinstance(test_case, list):
|
|
480
|
+
raise_config_error(
|
|
481
|
+
"--test-case must be a JSON array [{keyQueId, ordinal?, values}]",
|
|
482
|
+
error_code="INVALID_TEST_CASE",
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# ① 本地静态校验(qf_output 赋值 + 基本语法)
|
|
486
|
+
static_errors = validate_code_block_static(code)
|
|
487
|
+
|
|
488
|
+
# ② 后端真跑校验(静态有错时不浪费后端调用)
|
|
489
|
+
key_que_values = _normalize_code_block_test_case(test_case)
|
|
490
|
+
run_result = None
|
|
491
|
+
run_error = None
|
|
492
|
+
if not static_errors:
|
|
493
|
+
result = context.builder.app_form_run_code_block(
|
|
494
|
+
profile=args.profile, app_key=args.app_key, code=code, key_que_values=key_que_values
|
|
495
|
+
)
|
|
496
|
+
run_result = result.get("runResult")
|
|
497
|
+
run_error = result.get("runError")
|
|
498
|
+
|
|
499
|
+
ok = not static_errors and run_result is not None
|
|
500
|
+
return {
|
|
501
|
+
"category": "validation",
|
|
502
|
+
"status": "success" if ok else "failed",
|
|
503
|
+
"ok": ok,
|
|
504
|
+
"staticErrors": static_errors,
|
|
505
|
+
"runResult": run_result,
|
|
506
|
+
"runError": run_error,
|
|
507
|
+
"message": "code block is valid" if ok else "code block validation failed",
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _load_fields_for_validation(args: argparse.Namespace, context: CliContext) -> list[dict] | None:
|
|
512
|
+
"""加载字段清单:优先 --fields 参数,缺省时从后端拉取应用字段。
|
|
513
|
+
|
|
514
|
+
后端拉取失败时返回 None(跳过字段引用校验,不阻断流程)。
|
|
515
|
+
"""
|
|
516
|
+
fields_path = getattr(args, "fields", None)
|
|
517
|
+
if fields_path:
|
|
518
|
+
fields = load_json_value(
|
|
519
|
+
fields_path, option_name="--fields", max_bytes=2 * 1024 * 1024, max_depth=64
|
|
520
|
+
)
|
|
521
|
+
if not isinstance(fields, list):
|
|
522
|
+
raise_config_error(
|
|
523
|
+
"--fields must be a JSON array of field objects", error_code="INVALID_FIELDS"
|
|
524
|
+
)
|
|
525
|
+
return fields
|
|
526
|
+
fetched = context.builder.app_get_fields(profile=args.profile, app_key=args.app_key)
|
|
527
|
+
if not isinstance(fetched, dict) or fetched.get("status") != "success":
|
|
528
|
+
return None
|
|
529
|
+
raw_fields = fetched.get("fields")
|
|
530
|
+
if not isinstance(raw_fields, list):
|
|
531
|
+
return None
|
|
532
|
+
return [
|
|
533
|
+
{"queId": field.get("que_id"), "name": field.get("name")}
|
|
534
|
+
for field in raw_fields
|
|
535
|
+
if isinstance(field, dict)
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _read_code_arg(code_arg: str) -> str:
|
|
540
|
+
"""读取 --code 参数:@ 前缀读文件内容,否则当文本。"""
|
|
541
|
+
if not isinstance(code_arg, str) or not code_arg:
|
|
542
|
+
raise_config_error("--code is required", error_code="INVALID_CODE")
|
|
543
|
+
if code_arg.startswith("@"):
|
|
544
|
+
path = code_arg[1:]
|
|
545
|
+
try:
|
|
546
|
+
return Path(path).expanduser().read_text(encoding="utf-8")
|
|
547
|
+
except OSError as exc:
|
|
548
|
+
raise_config_error(
|
|
549
|
+
f"failed to read --code from '{path}': {exc}", error_code="INVALID_CODE"
|
|
550
|
+
)
|
|
551
|
+
return code_arg
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _normalize_code_block_test_case(test_case: list) -> list[dict]:
|
|
555
|
+
"""将 --test-case 规范化为后端 OpenMatchQueValue 结构 [{keyQueId, ordinal?, values}]。"""
|
|
556
|
+
normalized: list[dict] = []
|
|
557
|
+
for item in test_case:
|
|
558
|
+
if not isinstance(item, dict):
|
|
559
|
+
raise_config_error(
|
|
560
|
+
"--test-case items must be JSON objects {keyQueId, ordinal?, values}",
|
|
561
|
+
error_code="INVALID_TEST_CASE",
|
|
562
|
+
)
|
|
563
|
+
raw_que_id = item.get("keyQueId", item.get("key_que_id"))
|
|
564
|
+
if raw_que_id is None:
|
|
565
|
+
raise_config_error(
|
|
566
|
+
"--test-case items require keyQueId", error_code="INVALID_TEST_CASE"
|
|
567
|
+
)
|
|
568
|
+
try:
|
|
569
|
+
key_que_id = int(raw_que_id)
|
|
570
|
+
except (TypeError, ValueError):
|
|
571
|
+
raise_config_error(
|
|
572
|
+
f"--test-case keyQueId must be an integer, got {raw_que_id!r}",
|
|
573
|
+
error_code="INVALID_TEST_CASE",
|
|
574
|
+
)
|
|
575
|
+
entry: dict = {"keyQueId": key_que_id}
|
|
576
|
+
if item.get("ordinal") is not None:
|
|
577
|
+
try:
|
|
578
|
+
entry["ordinal"] = int(item.get("ordinal"))
|
|
579
|
+
except (TypeError, ValueError):
|
|
580
|
+
raise_config_error(
|
|
581
|
+
f"--test-case ordinal must be an integer, got {item.get('ordinal')!r}",
|
|
582
|
+
error_code="INVALID_TEST_CASE",
|
|
583
|
+
)
|
|
584
|
+
values = item.get("values", [])
|
|
585
|
+
if not isinstance(values, list):
|
|
586
|
+
raise_config_error(
|
|
587
|
+
"--test-case item values must be a JSON array", error_code="INVALID_TEST_CASE"
|
|
588
|
+
)
|
|
589
|
+
entry["values"] = [str(value) for value in values]
|
|
590
|
+
if item.get("tableValues") is not None:
|
|
591
|
+
entry["tableValues"] = item.get("tableValues")
|
|
592
|
+
normalized.append(entry)
|
|
593
|
+
return normalized
|
|
594
|
+
|
|
595
|
+
|
|
393
596
|
def _handle_app_delete(args: argparse.Namespace, context: CliContext) -> dict:
|
|
394
597
|
return context.builder.app_delete(profile=args.profile, app_key=args.app_key)
|
|
395
598
|
|
|
@@ -144,6 +144,8 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
144
144
|
PublicToolSpec(BUILDER_DOMAIN, "app_resolve", ("app_resolve",), ("builder", "app", "resolve"), has_contract=True, cli_show_effective_context=True),
|
|
145
145
|
PublicToolSpec(BUILDER_DOMAIN, "app_form_schema", ("app_form_schema",), ("builder", "app-form", "schema")),
|
|
146
146
|
PublicToolSpec(BUILDER_DOMAIN, "app_form_validate", (), ("builder", "app-form", "validate"), mcp_public=False),
|
|
147
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_form_validate_formula", (), ("builder", "app-form", "validate-formula"), mcp_public=False),
|
|
148
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_form_validate_code_block", (), ("builder", "app-form", "validate-code-block"), mcp_public=False),
|
|
147
149
|
PublicToolSpec(BUILDER_DOMAIN, "app_form_get", ("app_form_get",), ("builder", "app-form", "get"), cli_show_effective_context=True),
|
|
148
150
|
PublicToolSpec(BUILDER_DOMAIN, "app_form_apply", ("app_form_apply",), ("builder", "app-form", "apply"), cli_show_effective_context=True, cli_context_write=True),
|
|
149
151
|
PublicToolSpec(BUILDER_DOMAIN, "app_delete", ("app_delete",), ("builder", "app", "delete"), cli_show_effective_context=True, cli_context_write=True),
|
|
@@ -51,6 +51,10 @@ COMMON_ERROR_DROP_TOP = {
|
|
|
51
51
|
|
|
52
52
|
SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
|
|
53
53
|
|
|
54
|
+
#: 这两个校验命令的产出契约明确包含 ok 字段(供阶段 2 SKILL.md 引用),
|
|
55
|
+
#: 需在 success 裁剪时保留 ok(默认 COMMON_SUCCESS_DROP_TOP 会丢弃 ok)。
|
|
56
|
+
_KEEP_OK_TOOLS = {"builder:app_form_validate_formula", "builder:app_form_validate_code_block"}
|
|
57
|
+
|
|
54
58
|
|
|
55
59
|
def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict[str, Any]:
|
|
56
60
|
if not isinstance(payload, dict):
|
|
@@ -85,6 +89,8 @@ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dic
|
|
|
85
89
|
drop_keys = COMMON_SUCCESS_DROP_TOP - {"output_profile"}
|
|
86
90
|
if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and payload.get("ok") is False:
|
|
87
91
|
drop_keys = drop_keys - {"ok"}
|
|
92
|
+
if tool_name in _KEEP_OK_TOOLS:
|
|
93
|
+
drop_keys = drop_keys - {"ok"}
|
|
88
94
|
if tool_name == "builder:app_form_apply" and payload.get("mustReadCurrentStateBeforeRepair") is True:
|
|
89
95
|
drop_keys = drop_keys - {"suggested_next_call"}
|
|
90
96
|
_drop_top_keys(trimmed, drop_keys)
|
|
@@ -852,6 +852,64 @@ class AiBuilderTools(ToolBase):
|
|
|
852
852
|
"""Reconcile one complete declarative AppForm target."""
|
|
853
853
|
return self._app_form.apply(profile=profile, spec=spec)
|
|
854
854
|
|
|
855
|
+
def app_form_evaluate_formula(
|
|
856
|
+
self,
|
|
857
|
+
*,
|
|
858
|
+
profile: str,
|
|
859
|
+
app_key: str,
|
|
860
|
+
formula: str,
|
|
861
|
+
test_case: dict[int, Any],
|
|
862
|
+
) -> JSONObject:
|
|
863
|
+
"""调用后端 POST /ai/coding/formula/validate 对公式求值。
|
|
864
|
+
|
|
865
|
+
本地语法校验由 CLI handler 负责;本方法只做后端求值,捕获后端错误为 evaluationError。
|
|
866
|
+
鉴权/会话错误(未登录/未选工作区)按既有约定向上抛出 QingflowApiError。
|
|
867
|
+
"""
|
|
868
|
+
_session_profile, _backend_session, context = self._require_context(profile, require_workspace=True)
|
|
869
|
+
evaluation_result: Any = None
|
|
870
|
+
evaluation_error: str | None = None
|
|
871
|
+
try:
|
|
872
|
+
evaluation_result = self.backend.request(
|
|
873
|
+
"POST",
|
|
874
|
+
context,
|
|
875
|
+
"/ai/coding/formula/validate",
|
|
876
|
+
json_body={"appKey": app_key, "formula": formula, "testCase": test_case},
|
|
877
|
+
)
|
|
878
|
+
except QingflowApiError as exc:
|
|
879
|
+
evaluation_error = exc.message
|
|
880
|
+
return {"evaluation": evaluation_result, "evaluationError": evaluation_error}
|
|
881
|
+
|
|
882
|
+
def app_form_run_code_block(
|
|
883
|
+
self,
|
|
884
|
+
*,
|
|
885
|
+
profile: str,
|
|
886
|
+
app_key: str,
|
|
887
|
+
code: str,
|
|
888
|
+
key_que_values: list[JSONObject],
|
|
889
|
+
) -> JSONObject:
|
|
890
|
+
"""调用后端 POST /data/{appKey}/codeBlock/detection 真跑代码块。
|
|
891
|
+
|
|
892
|
+
本地静态校验由 CLI handler 负责;本方法只做后端真跑,捕获后端错误为 runError。
|
|
893
|
+
``key_que_values`` 需符合后端 OpenMatchQueValue 结构 ``{keyQueId, ordinal?, values}``。
|
|
894
|
+
|
|
895
|
+
传入的 ``code`` 是原始 JS(未经前端字段公式加密),必须声明 ``unEncrypted=True``;
|
|
896
|
+
否则后端 ``DataSvcImpl.codeBlockTest`` 会走 ``FormulaDecoder.extractFormula``
|
|
897
|
+
对原始 JS 解密并抛 ``QFRuntimeException``,导致后端真跑对所有真实代码必然失败。
|
|
898
|
+
"""
|
|
899
|
+
_session_profile, _backend_session, context = self._require_context(profile, require_workspace=True)
|
|
900
|
+
run_result: Any = None
|
|
901
|
+
run_error: str | None = None
|
|
902
|
+
try:
|
|
903
|
+
run_result = self.backend.request(
|
|
904
|
+
"POST",
|
|
905
|
+
context,
|
|
906
|
+
f"/data/{app_key}/codeBlock/detection",
|
|
907
|
+
json_body={"code": code, "keyQueValues": key_que_values, "unEncrypted": True},
|
|
908
|
+
)
|
|
909
|
+
except QingflowApiError as exc:
|
|
910
|
+
run_error = exc.message
|
|
911
|
+
return {"runResult": run_result, "runError": run_error}
|
|
912
|
+
|
|
855
913
|
def app_delete(self, *, profile: str, app_key: str) -> JSONObject:
|
|
856
914
|
"""Delete an application explicitly."""
|
|
857
915
|
return self._app_form.delete(profile=profile, app_key=app_key)
|