@josephyan/qingflow-cli 0.2.0-beta.74 → 0.2.0-beta.76
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 +121 -0
- package/src/qingflow_mcp/builder_facade/service.py +297 -66
- package/src/qingflow_mcp/public_surface.py +230 -0
- package/src/qingflow_mcp/response_trim.py +14 -248
- package/src/qingflow_mcp/server_app_user.py +4 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +35 -5
- package/src/qingflow_mcp/tools/approval_tools.py +0 -16
- package/src/qingflow_mcp/tools/record_tools.py +298 -25
- package/src/qingflow_mcp/tools/task_context_tools.py +130 -7
|
@@ -6,6 +6,7 @@ import time
|
|
|
6
6
|
|
|
7
7
|
from pydantic import ValidationError
|
|
8
8
|
|
|
9
|
+
from ..public_surface import public_builder_contract_tool_names
|
|
9
10
|
from ..config import DEFAULT_PROFILE
|
|
10
11
|
from ..errors import QingflowApiError
|
|
11
12
|
from ..json_types import JSONObject
|
|
@@ -462,11 +463,7 @@ class AiBuilderTools(ToolBase):
|
|
|
462
463
|
|
|
463
464
|
def builder_tool_contract(self, *, tool_name: str) -> JSONObject:
|
|
464
465
|
requested = str(tool_name or "").strip()
|
|
465
|
-
public_tool_names =
|
|
466
|
-
name
|
|
467
|
-
for name in _BUILDER_TOOL_CONTRACTS.keys()
|
|
468
|
-
if name not in _PRIVATE_BUILDER_TOOL_CONTRACTS and name not in _BUILDER_TOOL_CONTRACT_ALIASES
|
|
469
|
-
)
|
|
466
|
+
public_tool_names = public_builder_contract_tool_names()
|
|
470
467
|
if requested in _PRIVATE_BUILDER_TOOL_CONTRACTS:
|
|
471
468
|
lookup_name = ""
|
|
472
469
|
elif requested in _BUILDER_TOOL_CONTRACTS:
|
|
@@ -2207,6 +2204,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2207
2204
|
"allowed_values": {
|
|
2208
2205
|
"payload.trigger_action": [member.value for member in PublicButtonTriggerAction],
|
|
2209
2206
|
},
|
|
2207
|
+
"execution_notes": [
|
|
2208
|
+
"custom button writes now auto-publish the current app draft as a fixed closing step",
|
|
2209
|
+
"background_color and text_color cannot both be white",
|
|
2210
|
+
"for addData buttons, put field mappings in payload.trigger_add_data_config.que_relation",
|
|
2211
|
+
],
|
|
2210
2212
|
"minimal_example": {
|
|
2211
2213
|
"profile": "default",
|
|
2212
2214
|
"app_key": "APP_KEY",
|
|
@@ -2240,6 +2242,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2240
2242
|
"allowed_values": {
|
|
2241
2243
|
"payload.trigger_action": [member.value for member in PublicButtonTriggerAction],
|
|
2242
2244
|
},
|
|
2245
|
+
"execution_notes": [
|
|
2246
|
+
"custom button writes now auto-publish the current app draft as a fixed closing step",
|
|
2247
|
+
"background_color and text_color cannot both be white",
|
|
2248
|
+
"for addData buttons, put field mappings in payload.trigger_add_data_config.que_relation",
|
|
2249
|
+
],
|
|
2243
2250
|
"minimal_example": {
|
|
2244
2251
|
"profile": "default",
|
|
2245
2252
|
"app_key": "APP_KEY",
|
|
@@ -2281,6 +2288,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2281
2288
|
"field.allow_multiple": "field.relation_mode",
|
|
2282
2289
|
"field.optional_data_num": "field.relation_mode",
|
|
2283
2290
|
"field.optionalDataNum": "field.relation_mode",
|
|
2291
|
+
"field.departmentScope": "field.department_scope",
|
|
2284
2292
|
"field.remoteLookupConfig": "field.remote_lookup_config",
|
|
2285
2293
|
"field.qLinkerBinding": "field.q_linker_binding",
|
|
2286
2294
|
"field.codeBlockConfig": "field.code_block_config",
|
|
@@ -2338,6 +2346,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2338
2346
|
"field.allow_multiple": "field.relation_mode",
|
|
2339
2347
|
"field.optional_data_num": "field.relation_mode",
|
|
2340
2348
|
"field.optionalDataNum": "field.relation_mode",
|
|
2349
|
+
"field.departmentScope": "field.department_scope",
|
|
2341
2350
|
"field.remoteLookupConfig": "field.remote_lookup_config",
|
|
2342
2351
|
"field.qLinkerBinding": "field.q_linker_binding",
|
|
2343
2352
|
"field.codeBlockConfig": "field.code_block_config",
|
|
@@ -2349,6 +2358,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2349
2358
|
"allowed_values": {
|
|
2350
2359
|
"field.type": [member.value for member in PublicFieldType],
|
|
2351
2360
|
"field.relation_mode": [member.value for member in PublicRelationMode],
|
|
2361
|
+
"field.department_scope.mode": ["all", "custom"],
|
|
2352
2362
|
"field.code_block_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
|
|
2353
2363
|
"field.q_linker_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
|
|
2354
2364
|
"field_type_ids": sorted(FIELD_TYPE_ID_ALIASES.keys()),
|
|
@@ -2360,7 +2370,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2360
2370
|
"multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
|
|
2361
2371
|
"backend 49614 is normalized to MULTIPLE_RELATION_FIELDS_UNSUPPORTED with a workaround message",
|
|
2362
2372
|
"relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
|
|
2373
|
+
"relation fields now require both display_field and visible_fields in MCP/CLI payloads",
|
|
2363
2374
|
"if relation target metadata lookup is blocked by 40161/40002/40027, explicit display_field.name and visible_fields[].name let builder degrade verification and still continue schema write",
|
|
2375
|
+
"department fields accept department_scope with mode=all or mode=custom; custom scope requires explicit departments[].dept_id and optional include_sub_departs",
|
|
2364
2376
|
"q_linker_binding lets you declare request config, dynamic inputs, alias parsing, and target-field bindings in one step; builder writes remoteLookupConfig plus the existing backend relation-default and questionRelations structures",
|
|
2365
2377
|
"code_block_binding lets you declare inputs, code, alias parsing, and target-field bindings in one step; builder writes codeBlockConfig plus the existing backend relation-default and questionRelations structures",
|
|
2366
2378
|
"builder configures code blocks only; it does not execute or trigger code blocks",
|
|
@@ -2397,6 +2409,24 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2397
2409
|
"update_fields": [],
|
|
2398
2410
|
"remove_fields": [],
|
|
2399
2411
|
},
|
|
2412
|
+
"department_scope_example": {
|
|
2413
|
+
"profile": "default",
|
|
2414
|
+
"app_key": "APP_LEAD",
|
|
2415
|
+
"publish": True,
|
|
2416
|
+
"add_fields": [
|
|
2417
|
+
{
|
|
2418
|
+
"name": "归属部门",
|
|
2419
|
+
"type": "department",
|
|
2420
|
+
"department_scope": {
|
|
2421
|
+
"mode": "custom",
|
|
2422
|
+
"departments": [{"dept_id": 334952, "dept_name": "生产"}],
|
|
2423
|
+
"include_sub_departs": False,
|
|
2424
|
+
},
|
|
2425
|
+
}
|
|
2426
|
+
],
|
|
2427
|
+
"update_fields": [],
|
|
2428
|
+
"remove_fields": [],
|
|
2429
|
+
},
|
|
2400
2430
|
"code_block_example": {
|
|
2401
2431
|
"profile": "default",
|
|
2402
2432
|
"app_key": "APP_SCRIPT",
|
|
@@ -117,22 +117,6 @@ class ApprovalTools(ToolBase):
|
|
|
117
117
|
) -> dict[str, Any]:
|
|
118
118
|
return self.task_transfer(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {}, fields=fields or {})
|
|
119
119
|
|
|
120
|
-
@mcp.tool(description=self._high_risk_tool_description(operation="save", target="workflow task fields without advancing the workflow"))
|
|
121
|
-
def task_save_only(
|
|
122
|
-
profile: str = DEFAULT_PROFILE,
|
|
123
|
-
app_key: str = "",
|
|
124
|
-
record_id: int = 0,
|
|
125
|
-
workflow_node_id: int = 0,
|
|
126
|
-
fields: dict[str, Any] | None = None,
|
|
127
|
-
) -> dict[str, Any]:
|
|
128
|
-
return self.task_save_only(
|
|
129
|
-
profile=profile,
|
|
130
|
-
app_key=app_key,
|
|
131
|
-
record_id=record_id,
|
|
132
|
-
workflow_node_id=workflow_node_id,
|
|
133
|
-
fields=fields or {},
|
|
134
|
-
)
|
|
135
|
-
|
|
136
120
|
@mcp.tool()
|
|
137
121
|
def task_transfer_candidates(
|
|
138
122
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -138,6 +138,7 @@ class LookupResolutionState:
|
|
|
138
138
|
operation: str
|
|
139
139
|
app_key: str
|
|
140
140
|
apply_id: int | None
|
|
141
|
+
workflow_node_id: int | None
|
|
141
142
|
force_refresh_form: bool
|
|
142
143
|
context_complete: bool
|
|
143
144
|
field_index: FieldIndex
|
|
@@ -252,15 +253,19 @@ class RecordTools(ToolBase):
|
|
|
252
253
|
|
|
253
254
|
@mcp.tool(
|
|
254
255
|
description=(
|
|
255
|
-
"List current-user candidate members for a member field
|
|
256
|
-
"
|
|
257
|
-
"
|
|
256
|
+
"List current-user candidate members for a member field. "
|
|
257
|
+
"When record_id/workflow_node_id/fields are provided, this tool uses the backend runtime candidate scope that matches writes. "
|
|
258
|
+
"Without runtime context it only returns a static applicant-node preview scope. "
|
|
259
|
+
"Use record_insert_schema_get or record_update_schema_get with output_profile='verbose' first, then pass the member field_id."
|
|
258
260
|
)
|
|
259
261
|
)
|
|
260
262
|
def record_member_candidates(
|
|
261
263
|
profile: str = DEFAULT_PROFILE,
|
|
262
264
|
app_key: str = "",
|
|
263
265
|
field_id: int = 0,
|
|
266
|
+
record_id: int | None = None,
|
|
267
|
+
workflow_node_id: int | None = None,
|
|
268
|
+
fields: JSONObject | None = None,
|
|
264
269
|
keyword: str = "",
|
|
265
270
|
page_num: int = 1,
|
|
266
271
|
page_size: int = 20,
|
|
@@ -269,6 +274,9 @@ class RecordTools(ToolBase):
|
|
|
269
274
|
profile=profile,
|
|
270
275
|
app_key=app_key,
|
|
271
276
|
field_id=field_id,
|
|
277
|
+
record_id=record_id,
|
|
278
|
+
workflow_node_id=workflow_node_id,
|
|
279
|
+
fields=fields or {},
|
|
272
280
|
keyword=keyword,
|
|
273
281
|
page_num=page_num,
|
|
274
282
|
page_size=page_size,
|
|
@@ -276,16 +284,19 @@ class RecordTools(ToolBase):
|
|
|
276
284
|
|
|
277
285
|
@mcp.tool(
|
|
278
286
|
description=(
|
|
279
|
-
"List current-user candidate departments for a department field
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
287
|
+
"List current-user candidate departments for a department field. "
|
|
288
|
+
"When record_id/workflow_node_id/fields are provided, this tool uses the backend runtime candidate scope that matches writes. "
|
|
289
|
+
"Without runtime context it only returns a static applicant-node preview scope. "
|
|
290
|
+
"Use record_insert_schema_get or record_update_schema_get with output_profile='verbose' first, then pass the department field_id."
|
|
283
291
|
)
|
|
284
292
|
)
|
|
285
293
|
def record_department_candidates(
|
|
286
294
|
profile: str = DEFAULT_PROFILE,
|
|
287
295
|
app_key: str = "",
|
|
288
296
|
field_id: int = 0,
|
|
297
|
+
record_id: int | None = None,
|
|
298
|
+
workflow_node_id: int | None = None,
|
|
299
|
+
fields: JSONObject | None = None,
|
|
289
300
|
keyword: str = "",
|
|
290
301
|
page_num: int = 1,
|
|
291
302
|
page_size: int = 20,
|
|
@@ -294,6 +305,9 @@ class RecordTools(ToolBase):
|
|
|
294
305
|
profile=profile,
|
|
295
306
|
app_key=app_key,
|
|
296
307
|
field_id=field_id,
|
|
308
|
+
record_id=record_id,
|
|
309
|
+
workflow_node_id=workflow_node_id,
|
|
310
|
+
fields=fields or {},
|
|
297
311
|
keyword=keyword,
|
|
298
312
|
page_num=page_num,
|
|
299
313
|
page_size=page_size,
|
|
@@ -1007,6 +1021,14 @@ class RecordTools(ToolBase):
|
|
|
1007
1021
|
payload["options"] = list(field.options)
|
|
1008
1022
|
if kind in {"member", "department", "relation"}:
|
|
1009
1023
|
payload["accepts_natural_input"] = True
|
|
1024
|
+
if kind == "department":
|
|
1025
|
+
payload["candidate_hint"] = {
|
|
1026
|
+
"tool": "record_department_candidates",
|
|
1027
|
+
"field_id": field.que_id,
|
|
1028
|
+
"scope_source": "static_applicant_scope",
|
|
1029
|
+
"preview_only": True,
|
|
1030
|
+
"runtime_context_supported": True,
|
|
1031
|
+
}
|
|
1010
1032
|
if kind == "attachment":
|
|
1011
1033
|
payload["requires_upload"] = True
|
|
1012
1034
|
if kind == "relation" and field.target_app_key:
|
|
@@ -1214,6 +1236,9 @@ class RecordTools(ToolBase):
|
|
|
1214
1236
|
profile: str,
|
|
1215
1237
|
app_key: str,
|
|
1216
1238
|
field_id: int,
|
|
1239
|
+
record_id: int | None = None,
|
|
1240
|
+
workflow_node_id: int | None = None,
|
|
1241
|
+
fields: JSONObject | None = None,
|
|
1217
1242
|
keyword: str,
|
|
1218
1243
|
page_num: int,
|
|
1219
1244
|
page_size: int,
|
|
@@ -1243,7 +1268,33 @@ class RecordTools(ToolBase):
|
|
|
1243
1268
|
},
|
|
1244
1269
|
)
|
|
1245
1270
|
)
|
|
1246
|
-
|
|
1271
|
+
normalized_fields = fields if isinstance(fields, dict) else {}
|
|
1272
|
+
runtime_lookup = self._candidate_lookup_uses_runtime_scope(
|
|
1273
|
+
record_id=record_id,
|
|
1274
|
+
workflow_node_id=workflow_node_id,
|
|
1275
|
+
fields=normalized_fields,
|
|
1276
|
+
)
|
|
1277
|
+
warnings: list[JSONObject] = []
|
|
1278
|
+
scope_source = "static_applicant_scope"
|
|
1279
|
+
if runtime_lookup:
|
|
1280
|
+
state = self._build_candidate_lookup_state(
|
|
1281
|
+
profile,
|
|
1282
|
+
context,
|
|
1283
|
+
app_key=app_key,
|
|
1284
|
+
record_id=record_id,
|
|
1285
|
+
workflow_node_id=workflow_node_id,
|
|
1286
|
+
fields=normalized_fields,
|
|
1287
|
+
)
|
|
1288
|
+
items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1289
|
+
scope_source = "backend_runtime_scope"
|
|
1290
|
+
else:
|
|
1291
|
+
items = self._resolve_member_candidates(context, field, keyword=keyword)
|
|
1292
|
+
warnings.append(
|
|
1293
|
+
{
|
|
1294
|
+
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1295
|
+
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1296
|
+
}
|
|
1297
|
+
)
|
|
1247
1298
|
total = len(items)
|
|
1248
1299
|
start = (page_num - 1) * page_size
|
|
1249
1300
|
end = start + page_size
|
|
@@ -1254,7 +1305,7 @@ class RecordTools(ToolBase):
|
|
|
1254
1305
|
"ws_id": session_profile.selected_ws_id,
|
|
1255
1306
|
"ok": True,
|
|
1256
1307
|
"request_route": self._request_route_payload(context),
|
|
1257
|
-
"warnings":
|
|
1308
|
+
"warnings": warnings,
|
|
1258
1309
|
"output_profile": "normal",
|
|
1259
1310
|
"data": {
|
|
1260
1311
|
"items": page_items,
|
|
@@ -1269,9 +1320,13 @@ class RecordTools(ToolBase):
|
|
|
1269
1320
|
"app_key": app_key,
|
|
1270
1321
|
"field_id": field.que_id,
|
|
1271
1322
|
"field_title": field.que_title,
|
|
1323
|
+
"record_id": record_id,
|
|
1324
|
+
"workflow_node_id": workflow_node_id,
|
|
1325
|
+
"fields_present": bool(normalized_fields),
|
|
1272
1326
|
"keyword": keyword,
|
|
1273
1327
|
"permission_scope": "applicant_node",
|
|
1274
1328
|
},
|
|
1329
|
+
"scope_source": scope_source,
|
|
1275
1330
|
},
|
|
1276
1331
|
}
|
|
1277
1332
|
|
|
@@ -1283,6 +1338,9 @@ class RecordTools(ToolBase):
|
|
|
1283
1338
|
profile: str,
|
|
1284
1339
|
app_key: str,
|
|
1285
1340
|
field_id: int,
|
|
1341
|
+
record_id: int | None = None,
|
|
1342
|
+
workflow_node_id: int | None = None,
|
|
1343
|
+
fields: JSONObject | None = None,
|
|
1286
1344
|
keyword: str,
|
|
1287
1345
|
page_num: int,
|
|
1288
1346
|
page_size: int,
|
|
@@ -1312,7 +1370,50 @@ class RecordTools(ToolBase):
|
|
|
1312
1370
|
},
|
|
1313
1371
|
)
|
|
1314
1372
|
)
|
|
1315
|
-
|
|
1373
|
+
normalized_fields = fields if isinstance(fields, dict) else {}
|
|
1374
|
+
runtime_lookup = self._candidate_lookup_uses_runtime_scope(
|
|
1375
|
+
record_id=record_id,
|
|
1376
|
+
workflow_node_id=workflow_node_id,
|
|
1377
|
+
fields=normalized_fields,
|
|
1378
|
+
)
|
|
1379
|
+
warnings: list[JSONObject] = []
|
|
1380
|
+
scope_source = "static_applicant_scope"
|
|
1381
|
+
if runtime_lookup:
|
|
1382
|
+
state = self._build_candidate_lookup_state(
|
|
1383
|
+
profile,
|
|
1384
|
+
context,
|
|
1385
|
+
app_key=app_key,
|
|
1386
|
+
record_id=record_id,
|
|
1387
|
+
workflow_node_id=workflow_node_id,
|
|
1388
|
+
fields=normalized_fields,
|
|
1389
|
+
)
|
|
1390
|
+
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1391
|
+
scope_source = "backend_runtime_scope"
|
|
1392
|
+
else:
|
|
1393
|
+
items = self._resolve_department_candidates(context, field, keyword=keyword)
|
|
1394
|
+
scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
|
|
1395
|
+
if (
|
|
1396
|
+
not items
|
|
1397
|
+
and field.dept_select_scope_type == 2
|
|
1398
|
+
and not _scope_has_dynamic_or_external(scope)
|
|
1399
|
+
and not list(scope.get("depart") or [])
|
|
1400
|
+
):
|
|
1401
|
+
state = self._build_candidate_lookup_state(
|
|
1402
|
+
profile,
|
|
1403
|
+
context,
|
|
1404
|
+
app_key=app_key,
|
|
1405
|
+
record_id=None,
|
|
1406
|
+
workflow_node_id=None,
|
|
1407
|
+
fields={},
|
|
1408
|
+
)
|
|
1409
|
+
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1410
|
+
scope_source = "backend_runtime_scope"
|
|
1411
|
+
warnings.append(
|
|
1412
|
+
{
|
|
1413
|
+
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1414
|
+
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1415
|
+
}
|
|
1416
|
+
)
|
|
1316
1417
|
total = len(items)
|
|
1317
1418
|
start = (page_num - 1) * page_size
|
|
1318
1419
|
end = start + page_size
|
|
@@ -1323,7 +1424,7 @@ class RecordTools(ToolBase):
|
|
|
1323
1424
|
"ws_id": session_profile.selected_ws_id,
|
|
1324
1425
|
"ok": True,
|
|
1325
1426
|
"request_route": self._request_route_payload(context),
|
|
1326
|
-
"warnings":
|
|
1427
|
+
"warnings": warnings,
|
|
1327
1428
|
"output_profile": "normal",
|
|
1328
1429
|
"data": {
|
|
1329
1430
|
"items": page_items,
|
|
@@ -1338,9 +1439,13 @@ class RecordTools(ToolBase):
|
|
|
1338
1439
|
"app_key": app_key,
|
|
1339
1440
|
"field_id": field.que_id,
|
|
1340
1441
|
"field_title": field.que_title,
|
|
1442
|
+
"record_id": record_id,
|
|
1443
|
+
"workflow_node_id": workflow_node_id,
|
|
1444
|
+
"fields_present": bool(normalized_fields),
|
|
1341
1445
|
"keyword": keyword,
|
|
1342
1446
|
"permission_scope": "applicant_node",
|
|
1343
1447
|
},
|
|
1448
|
+
"scope_source": scope_source,
|
|
1344
1449
|
},
|
|
1345
1450
|
}
|
|
1346
1451
|
|
|
@@ -3183,6 +3288,65 @@ class RecordTools(ToolBase):
|
|
|
3183
3288
|
def _lookup_context_is_incomplete(self, state: LookupResolutionState | None) -> bool:
|
|
3184
3289
|
return bool(state is not None and not state.context_complete)
|
|
3185
3290
|
|
|
3291
|
+
def _candidate_lookup_uses_runtime_scope(
|
|
3292
|
+
self,
|
|
3293
|
+
*,
|
|
3294
|
+
record_id: int | None,
|
|
3295
|
+
workflow_node_id: int | None,
|
|
3296
|
+
fields: JSONObject | None,
|
|
3297
|
+
) -> bool:
|
|
3298
|
+
return bool(
|
|
3299
|
+
(record_id is not None and record_id > 0)
|
|
3300
|
+
or (workflow_node_id is not None and workflow_node_id > 0)
|
|
3301
|
+
or bool(fields)
|
|
3302
|
+
)
|
|
3303
|
+
|
|
3304
|
+
def _build_candidate_lookup_state(
|
|
3305
|
+
self,
|
|
3306
|
+
profile: str,
|
|
3307
|
+
context, # type: ignore[no-untyped-def]
|
|
3308
|
+
*,
|
|
3309
|
+
app_key: str,
|
|
3310
|
+
record_id: int | None,
|
|
3311
|
+
workflow_node_id: int | None,
|
|
3312
|
+
fields: JSONObject,
|
|
3313
|
+
) -> LookupResolutionState:
|
|
3314
|
+
index = self._get_field_index(profile, context, app_key, force_refresh=False)
|
|
3315
|
+
apply_id = record_id if isinstance(record_id, int) and record_id > 0 else None
|
|
3316
|
+
base_answers: list[JSONObject] = []
|
|
3317
|
+
context_complete = True
|
|
3318
|
+
if apply_id is not None:
|
|
3319
|
+
try:
|
|
3320
|
+
base_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=apply_id)
|
|
3321
|
+
except QingflowApiError:
|
|
3322
|
+
context_complete = False
|
|
3323
|
+
state = LookupResolutionState(
|
|
3324
|
+
operation="update" if apply_id is not None else "insert",
|
|
3325
|
+
app_key=app_key,
|
|
3326
|
+
apply_id=apply_id,
|
|
3327
|
+
workflow_node_id=workflow_node_id if isinstance(workflow_node_id, int) and workflow_node_id > 0 else None,
|
|
3328
|
+
force_refresh_form=False,
|
|
3329
|
+
context_complete=context_complete,
|
|
3330
|
+
field_index=index,
|
|
3331
|
+
base_answers=base_answers,
|
|
3332
|
+
normalized_answers=[],
|
|
3333
|
+
confirmation_requests=[],
|
|
3334
|
+
resolved_fields=[],
|
|
3335
|
+
unresolved_field_ids=set(),
|
|
3336
|
+
unresolved_subtable_cells=set(),
|
|
3337
|
+
)
|
|
3338
|
+
if fields:
|
|
3339
|
+
state.normalized_answers = self._resolve_answers(
|
|
3340
|
+
profile,
|
|
3341
|
+
context,
|
|
3342
|
+
app_key,
|
|
3343
|
+
answers=[],
|
|
3344
|
+
fields=fields,
|
|
3345
|
+
force_refresh_form=False,
|
|
3346
|
+
resolution_state=state,
|
|
3347
|
+
)
|
|
3348
|
+
return state
|
|
3349
|
+
|
|
3186
3350
|
def _raise_lookup_context_unavailable(
|
|
3187
3351
|
self,
|
|
3188
3352
|
*,
|
|
@@ -3474,6 +3638,28 @@ class RecordTools(ToolBase):
|
|
|
3474
3638
|
return "contains_unique_match"
|
|
3475
3639
|
return reason
|
|
3476
3640
|
|
|
3641
|
+
def _candidate_scope_fix_hint(
|
|
3642
|
+
self,
|
|
3643
|
+
*,
|
|
3644
|
+
kind: str,
|
|
3645
|
+
state: LookupResolutionState | None,
|
|
3646
|
+
explicit_guidance: str,
|
|
3647
|
+
) -> str:
|
|
3648
|
+
if state is not None and self._candidate_lookup_uses_runtime_scope(
|
|
3649
|
+
record_id=state.apply_id,
|
|
3650
|
+
workflow_node_id=state.workflow_node_id,
|
|
3651
|
+
fields={item.get("queId"): True for item in state.normalized_answers if isinstance(item, dict)},
|
|
3652
|
+
):
|
|
3653
|
+
return (
|
|
3654
|
+
f"Use record_{kind}_candidates with the same record_id/workflow_node_id/fields context to inspect the "
|
|
3655
|
+
"backend runtime candidate scope and choose one returned item exactly. "
|
|
3656
|
+
f"{explicit_guidance}"
|
|
3657
|
+
)
|
|
3658
|
+
return (
|
|
3659
|
+
f"Use record_{kind}_candidates to inspect the allowed candidate scope and choose one returned item exactly. "
|
|
3660
|
+
f"{explicit_guidance}"
|
|
3661
|
+
)
|
|
3662
|
+
|
|
3477
3663
|
def _resolve_member_candidates(self, context, field: FormField, *, keyword: str) -> list[JSONObject]: # type: ignore[no-untyped-def]
|
|
3478
3664
|
scope_type = field.member_select_scope_type
|
|
3479
3665
|
scope = field.member_select_scope if isinstance(field.member_select_scope, dict) else {}
|
|
@@ -3485,7 +3671,7 @@ class RecordTools(ToolBase):
|
|
|
3485
3671
|
filtered = [item for item in candidates if item is not None]
|
|
3486
3672
|
filtered.sort(key=lambda item: (_normalize_optional_text(item.get("value")) or "", _coerce_count(item.get("id")) or 0))
|
|
3487
3673
|
return filtered
|
|
3488
|
-
if scope_type !=
|
|
3674
|
+
if scope_type != 2:
|
|
3489
3675
|
raise_tool_error(
|
|
3490
3676
|
QingflowApiError(
|
|
3491
3677
|
category="not_supported",
|
|
@@ -3581,7 +3767,7 @@ class RecordTools(ToolBase):
|
|
|
3581
3767
|
if normalized is not None:
|
|
3582
3768
|
self._merge_department_candidate(merged, normalized)
|
|
3583
3769
|
else:
|
|
3584
|
-
if scope_type !=
|
|
3770
|
+
if scope_type != 2:
|
|
3585
3771
|
raise_tool_error(
|
|
3586
3772
|
QingflowApiError(
|
|
3587
3773
|
category="not_supported",
|
|
@@ -3599,6 +3785,14 @@ class RecordTools(ToolBase):
|
|
|
3599
3785
|
if dept_id is None:
|
|
3600
3786
|
continue
|
|
3601
3787
|
dept_name = _normalize_optional_text(item.get("deptName", item.get("value")) if isinstance(item, dict) else None)
|
|
3788
|
+
configured_candidate = _normalize_candidate_department(
|
|
3789
|
+
{"deptId": dept_id, "deptName": dept_name},
|
|
3790
|
+
source_kind="department",
|
|
3791
|
+
source_id=dept_id,
|
|
3792
|
+
source_value=dept_name,
|
|
3793
|
+
)
|
|
3794
|
+
if configured_candidate is not None:
|
|
3795
|
+
self._merge_department_candidate(merged, configured_candidate)
|
|
3602
3796
|
for dept in self._list_departments_by_scope(context, dept_id=dept_id, include_sub_departments=include_sub):
|
|
3603
3797
|
normalized = _normalize_candidate_department(
|
|
3604
3798
|
dept,
|
|
@@ -4719,14 +4913,15 @@ class RecordTools(ToolBase):
|
|
|
4719
4913
|
view_key: str | None,
|
|
4720
4914
|
view_name: str | None,
|
|
4721
4915
|
existing_answers_override: list[JSONObject] | None = None,
|
|
4916
|
+
field_index_override: FieldIndex | None = None,
|
|
4722
4917
|
) -> JSONObject:
|
|
4723
4918
|
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
4724
|
-
base_index = _build_applicant_top_level_field_index(schema)
|
|
4919
|
+
base_index = field_index_override or _build_applicant_top_level_field_index(schema)
|
|
4725
4920
|
question_relations = _collect_question_relations(schema)
|
|
4726
4921
|
runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
4727
4922
|
runtime_linked_field_ids.update(_collect_option_linked_field_ids(base_index))
|
|
4728
4923
|
index = base_index
|
|
4729
|
-
if operation == "create":
|
|
4924
|
+
if operation == "create" and field_index_override is None:
|
|
4730
4925
|
linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
|
|
4731
4926
|
schema,
|
|
4732
4927
|
linked_field_ids=runtime_linked_field_ids,
|
|
@@ -4800,6 +4995,7 @@ class RecordTools(ToolBase):
|
|
|
4800
4995
|
operation=operation,
|
|
4801
4996
|
app_key=app_key,
|
|
4802
4997
|
apply_id=apply_id,
|
|
4998
|
+
workflow_node_id=None,
|
|
4803
4999
|
force_refresh_form=force_refresh_form,
|
|
4804
5000
|
context_complete=(operation != "update" or existing_answers_loaded),
|
|
4805
5001
|
field_index=index,
|
|
@@ -4824,6 +5020,7 @@ class RecordTools(ToolBase):
|
|
|
4824
5020
|
fields=scoped_fields,
|
|
4825
5021
|
force_refresh_form=force_refresh_form,
|
|
4826
5022
|
resolution_state=lookup_resolution,
|
|
5023
|
+
field_index_override=index,
|
|
4827
5024
|
)
|
|
4828
5025
|
except RecordInputError as error:
|
|
4829
5026
|
invalid_fields.append(
|
|
@@ -6929,13 +7126,14 @@ class RecordTools(ToolBase):
|
|
|
6929
7126
|
fields: JSONObject,
|
|
6930
7127
|
force_refresh_form: bool,
|
|
6931
7128
|
resolution_state: LookupResolutionState | None = None,
|
|
7129
|
+
field_index_override: FieldIndex | None = None,
|
|
6932
7130
|
) -> list[JSONObject]:
|
|
6933
7131
|
if not app_key:
|
|
6934
7132
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
6935
7133
|
if not answers and not fields:
|
|
6936
7134
|
raise_tool_error(QingflowApiError.config_error("either answers or fields is required"))
|
|
6937
7135
|
if not answers and fields:
|
|
6938
|
-
index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
|
|
7136
|
+
index = field_index_override or self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
|
|
6939
7137
|
normalized: list[JSONObject] = []
|
|
6940
7138
|
for key, value in fields.items():
|
|
6941
7139
|
if value is None:
|
|
@@ -6948,7 +7146,7 @@ class RecordTools(ToolBase):
|
|
|
6948
7146
|
return normalized
|
|
6949
7147
|
if answers and not _answers_need_resolution(answers) and not fields:
|
|
6950
7148
|
return answers
|
|
6951
|
-
index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
|
|
7149
|
+
index = field_index_override or self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
|
|
6952
7150
|
normalized: list[JSONObject] = []
|
|
6953
7151
|
for item in answers:
|
|
6954
7152
|
answer = self._normalize_answer_item(profile, context, index, item, resolution_state=resolution_state)
|
|
@@ -8473,9 +8671,10 @@ class RecordTools(ToolBase):
|
|
|
8473
8671
|
raise RecordInputError(
|
|
8474
8672
|
message=f"member value for field '{field.que_title}' could not be resolved in the current candidate scope",
|
|
8475
8673
|
error_code="MEMBER_NOT_IN_CANDIDATE_SCOPE",
|
|
8476
|
-
fix_hint=(
|
|
8477
|
-
"
|
|
8478
|
-
|
|
8674
|
+
fix_hint=self._candidate_scope_fix_hint(
|
|
8675
|
+
kind="member",
|
|
8676
|
+
state=resolution_state,
|
|
8677
|
+
explicit_guidance="Explicit member id/userId/email values do not bypass candidate scope.",
|
|
8479
8678
|
),
|
|
8480
8679
|
details={
|
|
8481
8680
|
"field": _field_ref_payload(field),
|
|
@@ -8740,9 +8939,10 @@ class RecordTools(ToolBase):
|
|
|
8740
8939
|
raise RecordInputError(
|
|
8741
8940
|
message=f"department value for field '{field.que_title}' could not be resolved in the current candidate scope",
|
|
8742
8941
|
error_code="DEPARTMENT_NOT_IN_CANDIDATE_SCOPE",
|
|
8743
|
-
fix_hint=(
|
|
8744
|
-
"
|
|
8745
|
-
|
|
8942
|
+
fix_hint=self._candidate_scope_fix_hint(
|
|
8943
|
+
kind="department",
|
|
8944
|
+
state=resolution_state,
|
|
8945
|
+
explicit_guidance="Explicit department ids do not bypass candidate scope.",
|
|
8746
8946
|
),
|
|
8747
8947
|
details={
|
|
8748
8948
|
"field": _field_ref_payload(field),
|
|
@@ -9662,6 +9862,81 @@ def _build_applicant_hidden_linked_top_level_field_index(
|
|
|
9662
9862
|
)
|
|
9663
9863
|
|
|
9664
9864
|
|
|
9865
|
+
def _answer_to_form_field(answer: JSONObject) -> FormField | None:
|
|
9866
|
+
que_id = _coerce_count(answer.get("queId", answer.get("que_id")))
|
|
9867
|
+
title = _normalize_optional_text(answer.get("queTitle", answer.get("que_title")))
|
|
9868
|
+
que_type = _coerce_count(answer.get("queType", answer.get("que_type")))
|
|
9869
|
+
if que_id is None or que_id < 0 or not title or que_type in LAYOUT_ONLY_QUE_TYPES:
|
|
9870
|
+
return None
|
|
9871
|
+
raw: JSONObject = {
|
|
9872
|
+
"queId": que_id,
|
|
9873
|
+
"queTitle": title,
|
|
9874
|
+
}
|
|
9875
|
+
if que_type is not None:
|
|
9876
|
+
raw["queType"] = que_type
|
|
9877
|
+
field = FormField(
|
|
9878
|
+
que_id=que_id,
|
|
9879
|
+
que_title=title,
|
|
9880
|
+
que_type=que_type,
|
|
9881
|
+
required=False,
|
|
9882
|
+
readonly=False,
|
|
9883
|
+
system=False,
|
|
9884
|
+
options=[],
|
|
9885
|
+
aliases=[],
|
|
9886
|
+
target_app_key=None,
|
|
9887
|
+
target_app_name_hint=None,
|
|
9888
|
+
member_select_scope_type=None,
|
|
9889
|
+
member_select_scope=None,
|
|
9890
|
+
dept_select_scope_type=None,
|
|
9891
|
+
dept_select_scope=None,
|
|
9892
|
+
raw=raw,
|
|
9893
|
+
)
|
|
9894
|
+
field.aliases = sorted(_field_alias_candidates(field))
|
|
9895
|
+
return field
|
|
9896
|
+
|
|
9897
|
+
|
|
9898
|
+
def _build_answer_backed_field_index(
|
|
9899
|
+
answers: list[JSONObject] | JSONValue,
|
|
9900
|
+
*,
|
|
9901
|
+
field_id_filter: set[str] | None = None,
|
|
9902
|
+
) -> FieldIndex:
|
|
9903
|
+
by_id: dict[str, FormField] = {}
|
|
9904
|
+
by_title: dict[str, list[FormField]] = {}
|
|
9905
|
+
by_alias: dict[str, list[FormField]] = {}
|
|
9906
|
+
if not isinstance(answers, list):
|
|
9907
|
+
return FieldIndex(
|
|
9908
|
+
by_id=by_id,
|
|
9909
|
+
by_title=by_title,
|
|
9910
|
+
by_alias=by_alias,
|
|
9911
|
+
subtable_leaf_by_id={},
|
|
9912
|
+
subtable_leaf_by_title={},
|
|
9913
|
+
subtable_leaf_by_alias={},
|
|
9914
|
+
)
|
|
9915
|
+
for item in answers:
|
|
9916
|
+
if not isinstance(item, dict):
|
|
9917
|
+
continue
|
|
9918
|
+
field = _answer_to_form_field(item)
|
|
9919
|
+
if field is None:
|
|
9920
|
+
continue
|
|
9921
|
+
field_id = str(field.que_id)
|
|
9922
|
+
if field_id_filter is not None and field_id not in field_id_filter:
|
|
9923
|
+
continue
|
|
9924
|
+
if field_id in by_id:
|
|
9925
|
+
continue
|
|
9926
|
+
by_id[field_id] = field
|
|
9927
|
+
by_title.setdefault(_normalize_field_lookup_key(field.que_title), []).append(field)
|
|
9928
|
+
for alias in field.aliases:
|
|
9929
|
+
by_alias.setdefault(_normalize_field_lookup_key(alias), []).append(field)
|
|
9930
|
+
return FieldIndex(
|
|
9931
|
+
by_id=by_id,
|
|
9932
|
+
by_title=by_title,
|
|
9933
|
+
by_alias=by_alias,
|
|
9934
|
+
subtable_leaf_by_id={},
|
|
9935
|
+
subtable_leaf_by_title={},
|
|
9936
|
+
subtable_leaf_by_alias={},
|
|
9937
|
+
)
|
|
9938
|
+
|
|
9939
|
+
|
|
9665
9940
|
def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
|
|
9666
9941
|
by_id = dict(primary.by_id)
|
|
9667
9942
|
by_title = {key: list(value) for key, value in primary.by_title.items()}
|
|
@@ -12701,8 +12976,6 @@ def _scope_has_dynamic_or_external(scope: JSONObject) -> bool:
|
|
|
12701
12976
|
|
|
12702
12977
|
|
|
12703
12978
|
def _scope_is_default_all(scope_type: int | None, scope: JSONObject, *, keys: tuple[str, ...]) -> bool:
|
|
12704
|
-
if scope_type == 2:
|
|
12705
|
-
return True
|
|
12706
12979
|
if scope_type != 1:
|
|
12707
12980
|
return False
|
|
12708
12981
|
if _scope_has_dynamic_or_external(scope):
|