@josephyan/qingflow-cli 0.2.0-beta.985 → 0.2.0-beta.987
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/docs/local-agent-install.md +70 -11
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +376 -19
- package/src/qingflow_mcp/cli/commands/auth.py +14 -43
- package/src/qingflow_mcp/cli/commands/workspace.py +8 -5
- package/src/qingflow_mcp/cli/formatters.py +19 -22
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/public_surface.py +4 -6
- package/src/qingflow_mcp/response_trim.py +1 -8
- package/src/qingflow_mcp/server.py +1 -1
- package/src/qingflow_mcp/server_app_builder.py +4 -28
- package/src/qingflow_mcp/server_app_user.py +4 -28
- package/src/qingflow_mcp/session_store.py +31 -5
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +117 -1
- package/src/qingflow_mcp/tools/app_tools.py +51 -1
- package/src/qingflow_mcp/tools/approval_tools.py +82 -1
- package/src/qingflow_mcp/tools/auth_tools.py +306 -288
- package/src/qingflow_mcp/tools/base.py +204 -4
- package/src/qingflow_mcp/tools/code_block_tools.py +21 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +24 -1
- package/src/qingflow_mcp/tools/directory_tools.py +28 -1
- package/src/qingflow_mcp/tools/feedback_tools.py +8 -0
- package/src/qingflow_mcp/tools/file_tools.py +25 -1
- package/src/qingflow_mcp/tools/import_tools.py +40 -1
- package/src/qingflow_mcp/tools/navigation_tools.py +34 -1
- package/src/qingflow_mcp/tools/package_tools.py +37 -1
- package/src/qingflow_mcp/tools/portal_tools.py +28 -1
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +38 -1
- package/src/qingflow_mcp/tools/record_tools.py +255 -2
- package/src/qingflow_mcp/tools/repository_dev_tools.py +21 -2
- package/src/qingflow_mcp/tools/resource_read_tools.py +23 -1
- package/src/qingflow_mcp/tools/role_tools.py +19 -1
- package/src/qingflow_mcp/tools/solution_tools.py +56 -1
- package/src/qingflow_mcp/tools/task_context_tools.py +72 -1
- package/src/qingflow_mcp/tools/task_tools.py +49 -3
- package/src/qingflow_mcp/tools/view_tools.py +56 -1
- package/src/qingflow_mcp/tools/workflow_tools.py +65 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +100 -217
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-cli@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-cli@0.2.0-beta.987
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-cli@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-cli@0.2.0-beta.987 qingflow
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
|
@@ -6,6 +6,20 @@
|
|
|
6
6
|
2. 记录/待办优先的 `qingflow-app-user-mcp`
|
|
7
7
|
3. 精简 builder 的 `qingflow-app-builder-mcp`
|
|
8
8
|
|
|
9
|
+
## 本地鉴权推荐方案
|
|
10
|
+
|
|
11
|
+
本地模式现在推荐优先使用 `credential` 建立会话,而不是直接注入 `token`。
|
|
12
|
+
|
|
13
|
+
推荐链路:
|
|
14
|
+
|
|
15
|
+
1. createClaw 或其它本地宿主为当前实例保存 `credential`
|
|
16
|
+
2. 本地 MCP 调用 `auth_use_credential`
|
|
17
|
+
3. MCP 用该 `credential` 请求 apaas `/mcp/auth/context`
|
|
18
|
+
4. 解析并保存 `token / wsId / qfVersion / uid`
|
|
19
|
+
5. 业务工具直接使用这份上下文
|
|
20
|
+
|
|
21
|
+
`auth_use_credential` 是本地唯一鉴权主路径。
|
|
22
|
+
|
|
9
23
|
## npm 安装器适用场景
|
|
10
24
|
|
|
11
25
|
适合这类本地 agent / gateway:
|
|
@@ -63,17 +77,17 @@ npm run pack:npm
|
|
|
63
77
|
会生成:
|
|
64
78
|
|
|
65
79
|
```bash
|
|
66
|
-
dist/npm/
|
|
67
|
-
dist/npm/
|
|
68
|
-
dist/npm/
|
|
80
|
+
dist/npm/qingflow-tech-qingflow-cli-<version>.tgz
|
|
81
|
+
dist/npm/qingflow-tech-qingflow-app-user-mcp-<version>.tgz
|
|
82
|
+
dist/npm/qingflow-tech-qingflow-app-builder-mcp-<version>.tgz
|
|
69
83
|
```
|
|
70
84
|
|
|
71
85
|
然后在目标机器安装:
|
|
72
86
|
|
|
73
87
|
```bash
|
|
74
|
-
npm install /absolute/path/to/dist/npm/
|
|
75
|
-
npm install /absolute/path/to/dist/npm/
|
|
76
|
-
npm install /absolute/path/to/dist/npm/
|
|
88
|
+
npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-cli-<version>.tgz
|
|
89
|
+
npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-app-user-mcp-<version>.tgz
|
|
90
|
+
npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-app-builder-mcp-<version>.tgz
|
|
77
91
|
```
|
|
78
92
|
|
|
79
93
|
安装时会自动:
|
|
@@ -136,7 +150,10 @@ qingflow-app-builder-mcp
|
|
|
136
150
|
],
|
|
137
151
|
"env": {
|
|
138
152
|
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
139
|
-
"QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
|
|
153
|
+
"QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
|
|
154
|
+
"QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
|
|
155
|
+
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
|
|
156
|
+
"QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
|
|
140
157
|
}
|
|
141
158
|
}
|
|
142
159
|
}
|
|
@@ -153,7 +170,10 @@ qingflow-app-builder-mcp
|
|
|
153
170
|
"args": [],
|
|
154
171
|
"env": {
|
|
155
172
|
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
156
|
-
"QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
|
|
173
|
+
"QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
|
|
174
|
+
"QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
|
|
175
|
+
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
|
|
176
|
+
"QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
|
|
157
177
|
}
|
|
158
178
|
}
|
|
159
179
|
}
|
|
@@ -170,7 +190,10 @@ qingflow-app-builder-mcp
|
|
|
170
190
|
"args": [],
|
|
171
191
|
"env": {
|
|
172
192
|
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
173
|
-
"QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
|
|
193
|
+
"QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
|
|
194
|
+
"QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
|
|
195
|
+
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
|
|
196
|
+
"QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
|
|
174
197
|
}
|
|
175
198
|
}
|
|
176
199
|
}
|
|
@@ -191,7 +214,10 @@ qingflow-app-builder-mcp
|
|
|
191
214
|
"@josephyan/qingflow-app-user-mcp"
|
|
192
215
|
],
|
|
193
216
|
"env": {
|
|
194
|
-
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api"
|
|
217
|
+
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
218
|
+
"QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
|
|
219
|
+
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
|
|
220
|
+
"QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
|
|
195
221
|
}
|
|
196
222
|
},
|
|
197
223
|
"qingflow-builder": {
|
|
@@ -201,7 +227,10 @@ qingflow-app-builder-mcp
|
|
|
201
227
|
"@josephyan/qingflow-app-builder-mcp"
|
|
202
228
|
],
|
|
203
229
|
"env": {
|
|
204
|
-
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api"
|
|
230
|
+
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
231
|
+
"QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
|
|
232
|
+
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
|
|
233
|
+
"QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
|
|
205
234
|
}
|
|
206
235
|
}
|
|
207
236
|
}
|
|
@@ -212,6 +241,7 @@ qingflow-app-builder-mcp
|
|
|
212
241
|
- 源码目录 `npm install` 不会把命令加到全局 PATH;这种模式请用 `node ./npm/bin/qingflow.mjs`、`node ./npm/bin/qingflow-app-user-mcp.mjs` 或 `node ./npm/bin/qingflow-app-builder-mcp.mjs`
|
|
213
242
|
- `npx` 方式适合临时安装或容器化本地 agent
|
|
214
243
|
- 全局安装方式更适合长期固定使用的本机开发环境
|
|
244
|
+
- 计费接口使用当前登录会话的 `token` 与 `wsId` 请求头,可通过 `QINGFLOW_MCP_CREDIT_APAAS_BASE_URL/PATH` 覆盖调用记录接口地址
|
|
215
245
|
|
|
216
246
|
## 排障
|
|
217
247
|
|
|
@@ -242,3 +272,32 @@ npm install
|
|
|
242
272
|
4. 再启动 MCP 客户端
|
|
243
273
|
|
|
244
274
|
现在 stdio MCP 入口会拒绝在启动瞬间“边启动边重建 Python 运行时”,因为安装日志一旦写进 stdout,就会破坏 MCP 握手并表现成 `Transport closed`。如果运行时缺失或版本不一致,入口会直接报错并提示重装,而不是静默自修复。
|
|
275
|
+
|
|
276
|
+
## createClaw 本地接入示例
|
|
277
|
+
|
|
278
|
+
如果 createClaw 已经为当前本地实例保存了 `credential`,推荐在首次建链时调用:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
qingflow auth use-credential \
|
|
282
|
+
--base-url https://qingflow.com/api \
|
|
283
|
+
--credential-stdin
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
然后把 `credential` 写到 stdin。
|
|
287
|
+
|
|
288
|
+
等价 MCP 工具调用参数:
|
|
289
|
+
|
|
290
|
+
```json
|
|
291
|
+
{
|
|
292
|
+
"profile": "default",
|
|
293
|
+
"base_url": "https://qingflow.com/api",
|
|
294
|
+
"credential": "1602853_277941",
|
|
295
|
+
"persist": false
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
说明:
|
|
300
|
+
|
|
301
|
+
- 本地会把解析后的 `token` 和原始 `credential` 写入 profile 文件,用于后续 CLI 命令恢复会话
|
|
302
|
+
- `persist=true` 时,本地还会优先把解析后的 `token` 和原始 `credential` 同步写入系统 keychain
|
|
303
|
+
- 当前工作区以 `/mcp/auth/context` 返回的 `wsId` 为准,不再通过本地 MCP 显式切换
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@josephyan/qingflow-cli",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.987",
|
|
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
|
@@ -7708,7 +7708,7 @@ class AiBuilderFacade:
|
|
|
7708
7708
|
"request_route": request_route,
|
|
7709
7709
|
},
|
|
7710
7710
|
suggested_next_call=(
|
|
7711
|
-
{"tool_name": "
|
|
7711
|
+
{"tool_name": "auth_use_credential", "arguments": {"profile": profile}}
|
|
7712
7712
|
if request_route.get("ws_id")
|
|
7713
7713
|
else None
|
|
7714
7714
|
),
|
|
@@ -9811,6 +9811,15 @@ def _apply_relation_target_selection(
|
|
|
9811
9811
|
config["refer_field_types"] = [item.get("type") for item in normalized_visible]
|
|
9812
9812
|
config["auth_field_ids"] = [item.get("field_id") or item.get("name") for item in normalized_visible]
|
|
9813
9813
|
config["auth_field_que_ids"] = [_coerce_positive_int(item.get("que_id")) or 0 for item in normalized_visible]
|
|
9814
|
+
config["refer_auth_ques"] = [
|
|
9815
|
+
{
|
|
9816
|
+
"queId": _coerce_positive_int(item.get("que_id")) or 0,
|
|
9817
|
+
"queAuth": _REFERENCE_FIELD_VISIBLE_AUTH,
|
|
9818
|
+
"_field_id": item.get("field_id") or item.get("name"),
|
|
9819
|
+
}
|
|
9820
|
+
for item in normalized_visible
|
|
9821
|
+
if (_coerce_positive_int(item.get("que_id")) or 0) > 0
|
|
9822
|
+
]
|
|
9814
9823
|
config["field_name_show"] = bool(field.get("field_name_show", True))
|
|
9815
9824
|
field["target_field_id"] = display_field.get("field_id") or display_field.get("name")
|
|
9816
9825
|
field["target_field_que_id"] = _coerce_positive_int(display_field.get("que_id")) or 0
|
|
@@ -10071,17 +10080,32 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
10071
10080
|
field["target_app_key"] = reference.get("referAppKey")
|
|
10072
10081
|
field["relation_mode"] = _relation_mode_from_optional_data_num(reference.get("optionalDataNum"))
|
|
10073
10082
|
refer_questions = reference.get("referQuestions") if isinstance(reference.get("referQuestions"), list) else []
|
|
10083
|
+
refer_auth_questions = reference.get("referAuthQues") if isinstance(reference.get("referAuthQues"), list) else []
|
|
10084
|
+
refer_auth_by_que_id: dict[int, int] = {}
|
|
10085
|
+
for raw_item in refer_auth_questions:
|
|
10086
|
+
if not isinstance(raw_item, dict):
|
|
10087
|
+
continue
|
|
10088
|
+
que_id = _coerce_nonnegative_int(raw_item.get("queId"))
|
|
10089
|
+
que_auth = _coerce_nonnegative_int(raw_item.get("queAuth"))
|
|
10090
|
+
if que_id is None or que_auth is None or que_id in refer_auth_by_que_id:
|
|
10091
|
+
continue
|
|
10092
|
+
refer_auth_by_que_id[que_id] = que_auth
|
|
10074
10093
|
visible_fields: list[dict[str, Any]] = []
|
|
10075
10094
|
display_field_que_id = _coerce_nonnegative_int(reference.get("referQueId"))
|
|
10076
10095
|
display_field_name: str | None = None
|
|
10077
10096
|
for item in refer_questions:
|
|
10078
10097
|
if not isinstance(item, dict):
|
|
10079
10098
|
continue
|
|
10099
|
+
que_id = _coerce_nonnegative_int(item.get("queId"))
|
|
10100
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
10101
|
+
if que_auth is None and que_id is not None:
|
|
10102
|
+
que_auth = refer_auth_by_que_id.get(que_id)
|
|
10080
10103
|
selector = {
|
|
10081
|
-
"que_id":
|
|
10104
|
+
"que_id": que_id,
|
|
10082
10105
|
"name": str(item.get("queTitle") or "").strip() or None,
|
|
10083
10106
|
}
|
|
10084
|
-
|
|
10107
|
+
if que_auth != _REFERENCE_FIELD_HIDDEN_AUTH:
|
|
10108
|
+
visible_fields.append(selector)
|
|
10085
10109
|
if display_field_que_id is not None and selector["que_id"] == display_field_que_id:
|
|
10086
10110
|
display_field_name = selector["name"]
|
|
10087
10111
|
if display_field_name is None and visible_fields:
|
|
@@ -13275,6 +13299,333 @@ def _normalize_reference_auth_question_for_save(value: Any) -> dict[str, Any] |
|
|
|
13275
13299
|
return payload
|
|
13276
13300
|
|
|
13277
13301
|
|
|
13302
|
+
def _dedupe_reference_auth_questions(auth_questions: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
13303
|
+
deduped: list[dict[str, Any]] = []
|
|
13304
|
+
seen_que_ids: set[int] = set()
|
|
13305
|
+
for item in auth_questions:
|
|
13306
|
+
normalized_item = _normalize_reference_auth_question_for_save(item)
|
|
13307
|
+
if normalized_item is None:
|
|
13308
|
+
continue
|
|
13309
|
+
que_id = _coerce_any_int(normalized_item.get("queId"))
|
|
13310
|
+
if que_id is None or que_id in seen_que_ids:
|
|
13311
|
+
continue
|
|
13312
|
+
seen_que_ids.add(que_id)
|
|
13313
|
+
deduped.append(normalized_item)
|
|
13314
|
+
return deduped
|
|
13315
|
+
|
|
13316
|
+
|
|
13317
|
+
_REFERENCE_FIELD_HIDDEN_AUTH = 2
|
|
13318
|
+
_REFERENCE_FIELD_VISIBLE_AUTH = 3
|
|
13319
|
+
|
|
13320
|
+
|
|
13321
|
+
def _synthesize_reference_auth_questions_for_save(
|
|
13322
|
+
*,
|
|
13323
|
+
source: dict[str, Any],
|
|
13324
|
+
field: dict[str, Any],
|
|
13325
|
+
) -> list[dict[str, Any]]:
|
|
13326
|
+
config = field.get("config") if isinstance(field.get("config"), dict) else {}
|
|
13327
|
+
synthesized: list[dict[str, Any]] = []
|
|
13328
|
+
|
|
13329
|
+
if isinstance(config.get("refer_auth_ques"), list):
|
|
13330
|
+
synthesized.extend(cast(list[dict[str, Any]], config.get("refer_auth_ques") or []))
|
|
13331
|
+
if synthesized:
|
|
13332
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13333
|
+
|
|
13334
|
+
refer_question_ids_by_name: dict[str, int] = {}
|
|
13335
|
+
for raw_item in cast(list[Any], source.get("referQuestions") or []):
|
|
13336
|
+
if not isinstance(raw_item, dict):
|
|
13337
|
+
continue
|
|
13338
|
+
que_id = _coerce_any_int(raw_item.get("queId"))
|
|
13339
|
+
name = str(raw_item.get("queTitle") or "").strip()
|
|
13340
|
+
if que_id is None or not name or name in refer_question_ids_by_name:
|
|
13341
|
+
continue
|
|
13342
|
+
refer_question_ids_by_name[name] = que_id
|
|
13343
|
+
|
|
13344
|
+
visible_fields = cast(list[dict[str, Any]], field.get("visible_fields") or [])
|
|
13345
|
+
for item in visible_fields:
|
|
13346
|
+
if not isinstance(item, dict):
|
|
13347
|
+
continue
|
|
13348
|
+
que_id = _coerce_any_int(item.get("que_id"))
|
|
13349
|
+
if que_id is None:
|
|
13350
|
+
name = str(item.get("name") or "").strip()
|
|
13351
|
+
que_id = refer_question_ids_by_name.get(name)
|
|
13352
|
+
if que_id is None:
|
|
13353
|
+
continue
|
|
13354
|
+
synthesized.append({"queId": que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
|
|
13355
|
+
if synthesized:
|
|
13356
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13357
|
+
|
|
13358
|
+
auth_field_que_ids = cast(list[Any], config.get("auth_field_que_ids") or [])
|
|
13359
|
+
for raw_que_id in auth_field_que_ids:
|
|
13360
|
+
que_id = _coerce_any_int(raw_que_id)
|
|
13361
|
+
if que_id is None:
|
|
13362
|
+
continue
|
|
13363
|
+
synthesized.append({"queId": que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
|
|
13364
|
+
if synthesized:
|
|
13365
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13366
|
+
|
|
13367
|
+
for raw_item in cast(list[Any], source.get("referQuestions") or []):
|
|
13368
|
+
if not isinstance(raw_item, dict):
|
|
13369
|
+
continue
|
|
13370
|
+
que_id = _coerce_any_int(raw_item.get("queId"))
|
|
13371
|
+
if que_id is None:
|
|
13372
|
+
continue
|
|
13373
|
+
synthesized.append(
|
|
13374
|
+
{
|
|
13375
|
+
"queId": que_id,
|
|
13376
|
+
"queAuth": _REFERENCE_FIELD_VISIBLE_AUTH,
|
|
13377
|
+
}
|
|
13378
|
+
)
|
|
13379
|
+
if synthesized:
|
|
13380
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13381
|
+
|
|
13382
|
+
fallback_que_id = _coerce_any_int(field.get("target_field_que_id"))
|
|
13383
|
+
if fallback_que_id is not None:
|
|
13384
|
+
synthesized.append({"queId": fallback_que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
|
|
13385
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13386
|
+
|
|
13387
|
+
|
|
13388
|
+
def _reference_question_auth_overrides_for_save(
|
|
13389
|
+
*,
|
|
13390
|
+
source: dict[str, Any],
|
|
13391
|
+
field: dict[str, Any],
|
|
13392
|
+
) -> dict[int, int]:
|
|
13393
|
+
overrides: dict[int, int] = {}
|
|
13394
|
+
visible_que_ids: set[int] = set()
|
|
13395
|
+
|
|
13396
|
+
for item in cast(list[Any], field.get("visible_fields") or []):
|
|
13397
|
+
if not isinstance(item, dict):
|
|
13398
|
+
continue
|
|
13399
|
+
que_id = _coerce_any_int(item.get("que_id"))
|
|
13400
|
+
if que_id is not None:
|
|
13401
|
+
visible_que_ids.add(que_id)
|
|
13402
|
+
|
|
13403
|
+
if not visible_que_ids:
|
|
13404
|
+
refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
|
|
13405
|
+
for item in refer_auth_ques:
|
|
13406
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13407
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13408
|
+
if que_id is None or que_auth is None:
|
|
13409
|
+
continue
|
|
13410
|
+
overrides[que_id] = que_auth
|
|
13411
|
+
if overrides:
|
|
13412
|
+
return overrides
|
|
13413
|
+
|
|
13414
|
+
for raw_item in cast(list[Any], source.get("referQuestions") or []):
|
|
13415
|
+
if not isinstance(raw_item, dict):
|
|
13416
|
+
continue
|
|
13417
|
+
que_id = _coerce_any_int(raw_item.get("queId"))
|
|
13418
|
+
if que_id is None:
|
|
13419
|
+
continue
|
|
13420
|
+
overrides[que_id] = (
|
|
13421
|
+
_REFERENCE_FIELD_VISIBLE_AUTH if que_id in visible_que_ids else _REFERENCE_FIELD_HIDDEN_AUTH
|
|
13422
|
+
)
|
|
13423
|
+
return overrides
|
|
13424
|
+
|
|
13425
|
+
|
|
13426
|
+
def _reference_question_matches_visible_selector(question: dict[str, Any], selector: dict[str, Any]) -> bool:
|
|
13427
|
+
question_que_id = _coerce_any_int(question.get("queId"))
|
|
13428
|
+
selector_que_id = _coerce_any_int(selector.get("que_id"))
|
|
13429
|
+
if question_que_id is not None and selector_que_id is not None and question_que_id == selector_que_id:
|
|
13430
|
+
return True
|
|
13431
|
+
question_name = str(question.get("queTitle") or "").strip()
|
|
13432
|
+
selector_name = str(selector.get("name") or "").strip()
|
|
13433
|
+
return bool(question_name and selector_name and question_name == selector_name)
|
|
13434
|
+
|
|
13435
|
+
|
|
13436
|
+
def _build_reference_question_from_visible_selector(
|
|
13437
|
+
selector: dict[str, Any],
|
|
13438
|
+
*,
|
|
13439
|
+
ordinal: int,
|
|
13440
|
+
) -> dict[str, Any] | None:
|
|
13441
|
+
return _normalize_reference_question_for_save(
|
|
13442
|
+
{
|
|
13443
|
+
"queId": _coerce_any_int(selector.get("que_id")),
|
|
13444
|
+
"queTitle": str(selector.get("name") or "").strip() or None,
|
|
13445
|
+
"queType": str(selector.get("type") or "2"),
|
|
13446
|
+
"ordinal": ordinal,
|
|
13447
|
+
},
|
|
13448
|
+
ordinal=ordinal,
|
|
13449
|
+
)
|
|
13450
|
+
|
|
13451
|
+
|
|
13452
|
+
def _canonicalize_reference_questions_for_save(
|
|
13453
|
+
*,
|
|
13454
|
+
source: dict[str, Any],
|
|
13455
|
+
field: dict[str, Any],
|
|
13456
|
+
) -> list[dict[str, Any]]:
|
|
13457
|
+
normalized_source_questions = [
|
|
13458
|
+
item
|
|
13459
|
+
for item in (
|
|
13460
|
+
_normalize_reference_question_for_save(raw_item, ordinal=index)
|
|
13461
|
+
for index, raw_item in enumerate(cast(list[Any], source.get("referQuestions") or []), start=1)
|
|
13462
|
+
)
|
|
13463
|
+
if item is not None
|
|
13464
|
+
]
|
|
13465
|
+
|
|
13466
|
+
display_field = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
|
|
13467
|
+
visible_fields = [item for item in cast(list[Any], field.get("visible_fields") or []) if isinstance(item, dict)]
|
|
13468
|
+
ordered_visible_selectors: list[dict[str, Any]] = []
|
|
13469
|
+
if display_field is not None:
|
|
13470
|
+
ordered_visible_selectors.append(display_field)
|
|
13471
|
+
for item in visible_fields:
|
|
13472
|
+
if any(_relation_target_field_matches(existing, item) for existing in ordered_visible_selectors):
|
|
13473
|
+
continue
|
|
13474
|
+
ordered_visible_selectors.append(item)
|
|
13475
|
+
|
|
13476
|
+
if not ordered_visible_selectors:
|
|
13477
|
+
return normalized_source_questions
|
|
13478
|
+
|
|
13479
|
+
canonical_questions: list[dict[str, Any]] = []
|
|
13480
|
+
used_source_indexes: set[int] = set()
|
|
13481
|
+
|
|
13482
|
+
for ordinal, selector in enumerate(ordered_visible_selectors, start=1):
|
|
13483
|
+
matched_index: int | None = None
|
|
13484
|
+
matched_item: dict[str, Any] | None = None
|
|
13485
|
+
for index, item in enumerate(normalized_source_questions):
|
|
13486
|
+
if index in used_source_indexes:
|
|
13487
|
+
continue
|
|
13488
|
+
if _reference_question_matches_visible_selector(item, selector):
|
|
13489
|
+
matched_index = index
|
|
13490
|
+
matched_item = deepcopy(item)
|
|
13491
|
+
break
|
|
13492
|
+
if matched_item is None:
|
|
13493
|
+
matched_item = _build_reference_question_from_visible_selector(selector, ordinal=ordinal)
|
|
13494
|
+
if matched_item is None:
|
|
13495
|
+
continue
|
|
13496
|
+
matched_item["ordinal"] = ordinal
|
|
13497
|
+
canonical_questions.append(matched_item)
|
|
13498
|
+
if matched_index is not None:
|
|
13499
|
+
used_source_indexes.add(matched_index)
|
|
13500
|
+
|
|
13501
|
+
next_ordinal = len(canonical_questions) + 1
|
|
13502
|
+
for index, item in enumerate(normalized_source_questions):
|
|
13503
|
+
if index in used_source_indexes:
|
|
13504
|
+
continue
|
|
13505
|
+
remaining_item = deepcopy(item)
|
|
13506
|
+
remaining_item["ordinal"] = next_ordinal
|
|
13507
|
+
next_ordinal += 1
|
|
13508
|
+
canonical_questions.append(remaining_item)
|
|
13509
|
+
|
|
13510
|
+
return canonical_questions
|
|
13511
|
+
|
|
13512
|
+
|
|
13513
|
+
def _canonicalize_reference_auth_questions_for_save(
|
|
13514
|
+
*,
|
|
13515
|
+
source: dict[str, Any],
|
|
13516
|
+
refer_questions: list[dict[str, Any]],
|
|
13517
|
+
) -> list[dict[str, Any]]:
|
|
13518
|
+
source_auth_questions = [
|
|
13519
|
+
item
|
|
13520
|
+
for item in (
|
|
13521
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13522
|
+
for raw_item in cast(list[Any], source.get("referAuthQues") or [])
|
|
13523
|
+
)
|
|
13524
|
+
if item is not None
|
|
13525
|
+
]
|
|
13526
|
+
source_auth_by_que_id: dict[int, dict[str, Any]] = {}
|
|
13527
|
+
for item in source_auth_questions:
|
|
13528
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13529
|
+
if que_id is None or que_id in source_auth_by_que_id:
|
|
13530
|
+
continue
|
|
13531
|
+
source_auth_by_que_id[que_id] = item
|
|
13532
|
+
|
|
13533
|
+
auth_questions: list[dict[str, Any]] = []
|
|
13534
|
+
for item in refer_questions:
|
|
13535
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13536
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13537
|
+
if que_id is None or que_auth is None:
|
|
13538
|
+
continue
|
|
13539
|
+
payload = deepcopy(source_auth_by_que_id.get(que_id) or {"queId": que_id})
|
|
13540
|
+
payload["queId"] = que_id
|
|
13541
|
+
payload["queAuth"] = que_auth
|
|
13542
|
+
auth_questions.append(payload)
|
|
13543
|
+
return _dedupe_reference_auth_questions(auth_questions)
|
|
13544
|
+
|
|
13545
|
+
|
|
13546
|
+
def _enforce_reference_config_consistency_for_save(
|
|
13547
|
+
payload: dict[str, Any],
|
|
13548
|
+
*,
|
|
13549
|
+
field: dict[str, Any],
|
|
13550
|
+
) -> dict[str, Any]:
|
|
13551
|
+
refer_questions = [
|
|
13552
|
+
item
|
|
13553
|
+
for item in (
|
|
13554
|
+
_normalize_reference_question_for_save(raw_item, ordinal=index)
|
|
13555
|
+
for index, raw_item in enumerate(cast(list[Any], payload.get("referQuestions") or []), start=1)
|
|
13556
|
+
)
|
|
13557
|
+
if item is not None
|
|
13558
|
+
]
|
|
13559
|
+
if not refer_questions:
|
|
13560
|
+
return payload
|
|
13561
|
+
|
|
13562
|
+
refer_auth_ques = _dedupe_reference_auth_questions(
|
|
13563
|
+
[
|
|
13564
|
+
item
|
|
13565
|
+
for item in (
|
|
13566
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13567
|
+
for raw_item in cast(list[Any], payload.get("referAuthQues") or [])
|
|
13568
|
+
)
|
|
13569
|
+
if item is not None
|
|
13570
|
+
]
|
|
13571
|
+
)
|
|
13572
|
+
refer_auth_by_que_id: dict[int, int] = {}
|
|
13573
|
+
for item in refer_auth_ques:
|
|
13574
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13575
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13576
|
+
if que_id is None or que_auth is None or que_id in refer_auth_by_que_id:
|
|
13577
|
+
continue
|
|
13578
|
+
refer_auth_by_que_id[que_id] = que_auth
|
|
13579
|
+
|
|
13580
|
+
display_field_que_id = _coerce_any_int(payload.get("referQueId"))
|
|
13581
|
+
if display_field_que_id is None:
|
|
13582
|
+
display_field_que_id = _coerce_any_int(field.get("target_field_que_id"))
|
|
13583
|
+
if display_field_que_id is not None:
|
|
13584
|
+
payload["referQueId"] = display_field_que_id
|
|
13585
|
+
|
|
13586
|
+
if display_field_que_id is not None and not any(
|
|
13587
|
+
_coerce_any_int(item.get("queId")) == display_field_que_id for item in refer_questions
|
|
13588
|
+
):
|
|
13589
|
+
display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
|
|
13590
|
+
display_question = (
|
|
13591
|
+
_build_reference_question_from_visible_selector(display_selector, ordinal=1)
|
|
13592
|
+
if display_selector is not None
|
|
13593
|
+
else None
|
|
13594
|
+
)
|
|
13595
|
+
if display_question is not None:
|
|
13596
|
+
display_question["queId"] = display_field_que_id
|
|
13597
|
+
display_question["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
|
|
13598
|
+
refer_questions = [display_question, *refer_questions]
|
|
13599
|
+
|
|
13600
|
+
if display_field_que_id is not None:
|
|
13601
|
+
display_questions = [
|
|
13602
|
+
item for item in refer_questions if _coerce_any_int(item.get("queId")) == display_field_que_id
|
|
13603
|
+
]
|
|
13604
|
+
trailing_questions = [
|
|
13605
|
+
item for item in refer_questions if _coerce_any_int(item.get("queId")) != display_field_que_id
|
|
13606
|
+
]
|
|
13607
|
+
refer_questions = [*display_questions, *trailing_questions]
|
|
13608
|
+
|
|
13609
|
+
for ordinal, item in enumerate(refer_questions, start=1):
|
|
13610
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13611
|
+
if que_id is None:
|
|
13612
|
+
continue
|
|
13613
|
+
item["ordinal"] = ordinal
|
|
13614
|
+
item["queAuth"] = refer_auth_by_que_id.get(
|
|
13615
|
+
que_id,
|
|
13616
|
+
_coerce_nonnegative_int(item.get("queAuth")) or _REFERENCE_FIELD_VISIBLE_AUTH,
|
|
13617
|
+
)
|
|
13618
|
+
if display_field_que_id is not None and que_id == display_field_que_id:
|
|
13619
|
+
item["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
|
|
13620
|
+
|
|
13621
|
+
payload["referQuestions"] = refer_questions
|
|
13622
|
+
payload["referAuthQues"] = _canonicalize_reference_auth_questions_for_save(
|
|
13623
|
+
source={"referAuthQues": refer_auth_ques},
|
|
13624
|
+
refer_questions=refer_questions,
|
|
13625
|
+
)
|
|
13626
|
+
return payload
|
|
13627
|
+
|
|
13628
|
+
|
|
13278
13629
|
def _normalize_reference_config_for_save(
|
|
13279
13630
|
reference: Any,
|
|
13280
13631
|
*,
|
|
@@ -13289,11 +13640,13 @@ def _normalize_reference_config_for_save(
|
|
|
13289
13640
|
if field.get("field_name_show") is not None:
|
|
13290
13641
|
payload["fieldNameShow"] = bool(field.get("field_name_show"))
|
|
13291
13642
|
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13643
|
+
refer_question_auth_overrides = _reference_question_auth_overrides_for_save(source=source, field=field)
|
|
13644
|
+
refer_questions = _canonicalize_reference_questions_for_save(source=source, field=field)
|
|
13645
|
+
for index, normalized_item in enumerate(refer_questions, start=1):
|
|
13646
|
+
que_id = _coerce_any_int(normalized_item.get("queId"))
|
|
13647
|
+
if que_id is not None and que_id in refer_question_auth_overrides:
|
|
13648
|
+
normalized_item["queAuth"] = refer_question_auth_overrides[que_id]
|
|
13649
|
+
normalized_item["ordinal"] = index
|
|
13297
13650
|
if refer_questions or "referQuestions" in source:
|
|
13298
13651
|
payload["referQuestions"] = refer_questions
|
|
13299
13652
|
|
|
@@ -13308,18 +13661,13 @@ def _normalize_reference_config_for_save(
|
|
|
13308
13661
|
if refer_fill_rules or "referFillRules" in source:
|
|
13309
13662
|
payload["referFillRules"] = refer_fill_rules
|
|
13310
13663
|
|
|
13311
|
-
refer_auth_ques =
|
|
13312
|
-
|
|
13313
|
-
|
|
13314
|
-
_normalize_reference_auth_question_for_save(raw_item)
|
|
13315
|
-
for raw_item in cast(list[Any], source.get("referAuthQues") or [])
|
|
13316
|
-
)
|
|
13317
|
-
if item is not None
|
|
13318
|
-
]
|
|
13664
|
+
refer_auth_ques = _canonicalize_reference_auth_questions_for_save(source=source, refer_questions=refer_questions)
|
|
13665
|
+
if not refer_auth_ques:
|
|
13666
|
+
refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
|
|
13319
13667
|
if refer_auth_ques or "referAuthQues" in source:
|
|
13320
13668
|
payload["referAuthQues"] = refer_auth_ques
|
|
13321
13669
|
|
|
13322
|
-
return payload
|
|
13670
|
+
return _enforce_reference_config_consistency_for_save(payload, field=field)
|
|
13323
13671
|
|
|
13324
13672
|
|
|
13325
13673
|
def _normalize_relation_question_for_save(question: dict[str, Any], *, field: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -13550,9 +13898,16 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
13550
13898
|
preserved_reference["referAppKey"] = field.get("target_app_key")
|
|
13551
13899
|
question["referenceConfig"] = preserved_reference
|
|
13552
13900
|
else:
|
|
13901
|
+
existing_reference = (
|
|
13902
|
+
deepcopy(relation_question_template.get("referenceConfig"))
|
|
13903
|
+
if relation_question_template is not None and isinstance(relation_question_template.get("referenceConfig"), dict)
|
|
13904
|
+
else deepcopy(question.get("referenceConfig"))
|
|
13905
|
+
if isinstance(question.get("referenceConfig"), dict)
|
|
13906
|
+
else {}
|
|
13907
|
+
)
|
|
13553
13908
|
reference = (
|
|
13554
|
-
|
|
13555
|
-
if relation_config_explicit
|
|
13909
|
+
existing_reference
|
|
13910
|
+
if relation_config_explicit
|
|
13556
13911
|
else deepcopy(question.get("referenceConfig"))
|
|
13557
13912
|
if isinstance(question.get("referenceConfig"), dict)
|
|
13558
13913
|
else {}
|
|
@@ -13573,6 +13928,8 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
13573
13928
|
"fieldNameShow",
|
|
13574
13929
|
"_targetFieldId",
|
|
13575
13930
|
):
|
|
13931
|
+
if relation_config_explicit and key in {"referQuestions", "referAuthQues"}:
|
|
13932
|
+
continue
|
|
13576
13933
|
if key in built_reference:
|
|
13577
13934
|
reference[key] = deepcopy(built_reference[key])
|
|
13578
13935
|
reference["referAppKey"] = field.get("target_app_key")
|