@josephyan/qingflow-app-user-mcp 0.2.0-beta.32 → 0.2.0-beta.34
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/skills/qingflow-app-user/SKILL.md +1 -0
- package/skills/qingflow-record-analysis/SKILL.md +6 -0
- package/skills/qingflow-record-crud/SKILL.md +6 -0
- package/skills/qingflow-task-ops/SKILL.md +6 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/config.py +38 -0
- package/src/qingflow_mcp/server.py +11 -1
- package/src/qingflow_mcp/server_app_builder.py +6 -1
- package/src/qingflow_mcp/server_app_user.py +13 -2
- package/src/qingflow_mcp/tools/feedback_tools.py +224 -0
- package/src/qingflow_mcp/tools/record_tools.py +4 -1
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.34
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.34 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -41,6 +41,7 @@ Route to exactly one of these specialized paths:
|
|
|
41
41
|
- prefer canonical app ids, record ids, task ids, and workflow node ids over guessed names
|
|
42
42
|
- if a field or target is still ambiguous after schema/task lookup, ask the user to confirm from a short candidate list instead of guessing
|
|
43
43
|
- if the task can stay read-only, do not write or act
|
|
44
|
+
- if the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, summarize the gap, ask whether to submit feedback, and call `feedback_submit` only after explicit user confirmation
|
|
44
45
|
|
|
45
46
|
## Resources
|
|
46
47
|
|
|
@@ -140,6 +140,12 @@ Top-level arguments:
|
|
|
140
140
|
- Otherwise → `初步观察`
|
|
141
141
|
- `rows_truncated=true` → 用 `前 N 个分组`, 不用 `全部`/`所有`
|
|
142
142
|
|
|
143
|
+
## Feedback Escalation
|
|
144
|
+
|
|
145
|
+
- If the desired analysis still cannot be completed because of missing capability, unsupported analysis shape, or an obviously awkward workflow after reasonable attempts, summarize the exact gap.
|
|
146
|
+
- Ask whether the user wants you to submit product feedback.
|
|
147
|
+
- Only after explicit user confirmation, call `feedback_submit`.
|
|
148
|
+
|
|
143
149
|
## Resources
|
|
144
150
|
|
|
145
151
|
- DSL templates: [references/dsl-templates.md](references/dsl-templates.md)
|
|
@@ -126,6 +126,12 @@ The DSL is clause-shaped like SQL, but it is **not raw SQL text**.
|
|
|
126
126
|
- Prefer canonical schema titles and aliases in your final wording
|
|
127
127
|
- If only part of the requested work is completed, explicitly disclose which parts are done and which are not
|
|
128
128
|
|
|
129
|
+
## Feedback Escalation
|
|
130
|
+
|
|
131
|
+
- If record CRUD still cannot satisfy the request because of missing capability, unsupported field behavior, or an obviously awkward workflow after reasonable attempts, summarize the exact blocker.
|
|
132
|
+
- Ask whether the user wants you to submit product feedback.
|
|
133
|
+
- Only after explicit user confirmation, call `feedback_submit`.
|
|
134
|
+
|
|
129
135
|
## Resources
|
|
130
136
|
|
|
131
137
|
- Record operation patterns: [references/record-patterns.md](references/record-patterns.md)
|
|
@@ -98,6 +98,12 @@ Use exactly one of these default paths:
|
|
|
98
98
|
- Avoid actions on ambiguous tasks or records
|
|
99
99
|
- Summarize the final action and the exact `app_key / record_id / workflow_node_id`
|
|
100
100
|
|
|
101
|
+
## Feedback Escalation
|
|
102
|
+
|
|
103
|
+
- If task capabilities, associated report detail, workflow log visibility, or action support still cannot satisfy the user's goal after reasonable use of this skill, summarize the exact gap in plain language.
|
|
104
|
+
- Ask whether the user wants you to submit product feedback.
|
|
105
|
+
- Only after explicit user confirmation, call `feedback_submit`.
|
|
106
|
+
|
|
101
107
|
## Response Interpretation
|
|
102
108
|
|
|
103
109
|
- `task_list` returns normalized todo rows and is the only default discovery path
|
|
@@ -12,6 +12,7 @@ DEFAULT_USER_AGENT = "qingflow-mcp/1.0"
|
|
|
12
12
|
DEFAULT_RECORD_LIST_TYPE = 8
|
|
13
13
|
ATTACHMENT_QUESTION_TYPE = 13
|
|
14
14
|
DEFAULT_BASE_URL = "https://qingflow.com/api"
|
|
15
|
+
DEFAULT_FEEDBACK_APP_KEY = "e0d017kju002"
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def get_mcp_home() -> Path:
|
|
@@ -136,6 +137,43 @@ def get_default_qf_version() -> str | None:
|
|
|
136
137
|
return normalized or None
|
|
137
138
|
|
|
138
139
|
|
|
140
|
+
def get_feedback_qsource_token() -> str | None:
|
|
141
|
+
"""获取反馈 q-source 被动入口 token"""
|
|
142
|
+
value = get_config_value(
|
|
143
|
+
"feedback.qsource_token",
|
|
144
|
+
env_var="QINGFLOW_MCP_FEEDBACK_QSOURCE_TOKEN",
|
|
145
|
+
default=None,
|
|
146
|
+
)
|
|
147
|
+
if value is None:
|
|
148
|
+
return None
|
|
149
|
+
normalized = str(value).strip()
|
|
150
|
+
return normalized or None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_feedback_base_url() -> str | None:
|
|
154
|
+
"""获取反馈 q-source 使用的 base URL"""
|
|
155
|
+
value = get_config_value(
|
|
156
|
+
"feedback.base_url",
|
|
157
|
+
env_var="QINGFLOW_MCP_FEEDBACK_BASE_URL",
|
|
158
|
+
default=None,
|
|
159
|
+
)
|
|
160
|
+
if value is None:
|
|
161
|
+
return get_default_base_url()
|
|
162
|
+
normalized = normalize_base_url(value)
|
|
163
|
+
return normalized or get_default_base_url()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_feedback_app_key() -> str:
|
|
167
|
+
"""获取内部反馈表 app_key"""
|
|
168
|
+
value = get_config_value(
|
|
169
|
+
"feedback.app_key",
|
|
170
|
+
env_var="QINGFLOW_MCP_FEEDBACK_APP_KEY",
|
|
171
|
+
default=DEFAULT_FEEDBACK_APP_KEY,
|
|
172
|
+
)
|
|
173
|
+
normalized = str(value or "").strip()
|
|
174
|
+
return normalized or DEFAULT_FEEDBACK_APP_KEY
|
|
175
|
+
|
|
176
|
+
|
|
139
177
|
def get_timeout_seconds() -> float:
|
|
140
178
|
"""获取 HTTP 超时秒数"""
|
|
141
179
|
value = get_config_value(
|
|
@@ -8,6 +8,7 @@ from .backend_client import BackendClient
|
|
|
8
8
|
from .session_store import SessionStore
|
|
9
9
|
from .tools.app_tools import AppTools
|
|
10
10
|
from .tools.auth_tools import AuthTools
|
|
11
|
+
from .tools.feedback_tools import FeedbackTools
|
|
11
12
|
from .tools.file_tools import FileTools
|
|
12
13
|
from .tools.package_tools import PackageTools
|
|
13
14
|
from .tools.navigation_tools import NavigationTools
|
|
@@ -116,11 +117,20 @@ Default to `prod` unless the user explicitly specifies `test`.
|
|
|
116
117
|
|
|
117
118
|
## Constraints
|
|
118
119
|
|
|
119
|
-
Avoid builder-side app or schema changes here.
|
|
120
|
+
Avoid builder-side app or schema changes here.
|
|
121
|
+
|
|
122
|
+
## Feedback Path
|
|
123
|
+
|
|
124
|
+
If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, offer to submit product feedback.
|
|
125
|
+
|
|
126
|
+
- First summarize what is still not working
|
|
127
|
+
- Ask the user whether to submit feedback
|
|
128
|
+
- Call `feedback_submit` only after explicit user confirmation""",
|
|
120
129
|
)
|
|
121
130
|
sessions = SessionStore()
|
|
122
131
|
backend = BackendClient()
|
|
123
132
|
AuthTools(sessions, backend).register(server)
|
|
133
|
+
FeedbackTools(backend, mcp_side="通用").register(server)
|
|
124
134
|
WorkspaceTools(sessions, backend).register(server)
|
|
125
135
|
FileTools(sessions, backend).register(server)
|
|
126
136
|
RecordTools(sessions, backend).register(server)
|
|
@@ -7,6 +7,7 @@ from .config import DEFAULT_PROFILE
|
|
|
7
7
|
from .session_store import SessionStore
|
|
8
8
|
from .tools.ai_builder_tools import AiBuilderTools
|
|
9
9
|
from .tools.auth_tools import AuthTools
|
|
10
|
+
from .tools.feedback_tools import FeedbackTools
|
|
10
11
|
from .tools.file_tools import FileTools
|
|
11
12
|
from .tools.workspace_tools import WorkspaceTools
|
|
12
13
|
|
|
@@ -26,7 +27,8 @@ def build_builder_server() -> FastMCP:
|
|
|
26
27
|
"Use package_attach_app to attach apps to packages, and app_publish_verify for explicit final publish verification. "
|
|
27
28
|
"For workflow edits, prefer preset plus explicit patching over generating a full custom graph from scratch, and declare node assignees and editable fields explicitly. "
|
|
28
29
|
"If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
|
|
29
|
-
"Do not handcraft internal solution payloads or rely on build_id/stage/repair."
|
|
30
|
+
"Do not handcraft internal solution payloads or rely on build_id/stage/repair. "
|
|
31
|
+
"If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, first summarize the gap, ask whether to submit feedback, and call feedback_submit only after explicit user confirmation."
|
|
30
32
|
),
|
|
31
33
|
)
|
|
32
34
|
sessions = SessionStore()
|
|
@@ -35,6 +37,7 @@ def build_builder_server() -> FastMCP:
|
|
|
35
37
|
workspace = WorkspaceTools(sessions, backend)
|
|
36
38
|
files = FileTools(sessions, backend)
|
|
37
39
|
ai_builder = AiBuilderTools(sessions, backend)
|
|
40
|
+
feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
|
|
38
41
|
|
|
39
42
|
@server.tool()
|
|
40
43
|
def auth_login(
|
|
@@ -120,6 +123,8 @@ def build_builder_server() -> FastMCP:
|
|
|
120
123
|
file_related_url=file_related_url,
|
|
121
124
|
)
|
|
122
125
|
|
|
126
|
+
feedback.register(server)
|
|
127
|
+
|
|
123
128
|
@server.tool()
|
|
124
129
|
def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all") -> dict:
|
|
125
130
|
return ai_builder.package_list(profile=profile, trial_status=trial_status)
|
|
@@ -10,6 +10,7 @@ from .session_store import SessionStore
|
|
|
10
10
|
from .tools.app_tools import AppTools
|
|
11
11
|
from .tools.auth_tools import AuthTools
|
|
12
12
|
from .tools.directory_tools import DirectoryTools
|
|
13
|
+
from .tools.feedback_tools import FeedbackTools
|
|
13
14
|
from .tools.file_tools import FileTools
|
|
14
15
|
from .tools.record_tools import RecordTools
|
|
15
16
|
from .tools.task_context_tools import TaskContextTools
|
|
@@ -104,7 +105,15 @@ Default to `prod` unless the user explicitly specifies `test`.
|
|
|
104
105
|
|
|
105
106
|
## Constraints
|
|
106
107
|
|
|
107
|
-
Avoid builder-side app or schema changes here.
|
|
108
|
+
Avoid builder-side app or schema changes here.
|
|
109
|
+
|
|
110
|
+
## Feedback Path
|
|
111
|
+
|
|
112
|
+
If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, offer to submit product feedback.
|
|
113
|
+
|
|
114
|
+
- First summarize what is still not working
|
|
115
|
+
- Ask the user whether to submit feedback
|
|
116
|
+
- Call `feedback_submit` only after explicit user confirmation""",
|
|
108
117
|
)
|
|
109
118
|
sessions = SessionStore()
|
|
110
119
|
backend = BackendClient()
|
|
@@ -112,6 +121,7 @@ Avoid builder-side app or schema changes here.""",
|
|
|
112
121
|
apps = AppTools(sessions, backend)
|
|
113
122
|
workspace = WorkspaceTools(sessions, backend)
|
|
114
123
|
files = FileTools(sessions, backend)
|
|
124
|
+
feedback = FeedbackTools(backend, mcp_side="App User MCP")
|
|
115
125
|
|
|
116
126
|
@server.tool()
|
|
117
127
|
def auth_login(
|
|
@@ -227,8 +237,9 @@ Avoid builder-side app or schema changes here.""",
|
|
|
227
237
|
bucket_type=bucket_type,
|
|
228
238
|
path_id=path_id,
|
|
229
239
|
file_related_url=file_related_url,
|
|
230
|
-
|
|
240
|
+
)
|
|
231
241
|
|
|
242
|
+
feedback.register(server)
|
|
232
243
|
RecordTools(sessions, backend).register(server)
|
|
233
244
|
TaskContextTools(sessions, backend).register(server)
|
|
234
245
|
DirectoryTools(sessions, backend).register(server)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
|
|
7
|
+
from ..backend_client import BackendClient
|
|
8
|
+
from ..config import get_feedback_app_key, get_feedback_base_url, get_feedback_qsource_token, normalize_base_url
|
|
9
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
10
|
+
from ..json_types import JSONObject
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CATEGORY_MAP = {
|
|
14
|
+
"feature_request": "功能需求",
|
|
15
|
+
"bug_report": "问题反馈",
|
|
16
|
+
"ux_feedback": "体验建议",
|
|
17
|
+
"unsupported_scenario": "不支持场景",
|
|
18
|
+
"other": "其他",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
IMPACT_SCOPE_MAP = {
|
|
22
|
+
"personal": "仅个人",
|
|
23
|
+
"small_team": "小范围团队",
|
|
24
|
+
"cross_team": "跨团队",
|
|
25
|
+
"global": "全局",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class FeedbackTools:
|
|
31
|
+
backend: BackendClient
|
|
32
|
+
mcp_side: str
|
|
33
|
+
|
|
34
|
+
def register(self, mcp: FastMCP) -> None:
|
|
35
|
+
@mcp.tool()
|
|
36
|
+
def feedback_submit(
|
|
37
|
+
category: str = "",
|
|
38
|
+
title: str = "",
|
|
39
|
+
description: str = "",
|
|
40
|
+
expected_behavior: str | None = None,
|
|
41
|
+
actual_behavior: str | None = None,
|
|
42
|
+
impact_scope: str | None = None,
|
|
43
|
+
tool_name: str | None = None,
|
|
44
|
+
app_key: str | None = None,
|
|
45
|
+
record_id: str | int | None = None,
|
|
46
|
+
workflow_node_id: str | int | None = None,
|
|
47
|
+
note: str | None = None,
|
|
48
|
+
) -> JSONObject:
|
|
49
|
+
return self.feedback_submit(
|
|
50
|
+
category=category,
|
|
51
|
+
title=title,
|
|
52
|
+
description=description,
|
|
53
|
+
expected_behavior=expected_behavior,
|
|
54
|
+
actual_behavior=actual_behavior,
|
|
55
|
+
impact_scope=impact_scope,
|
|
56
|
+
tool_name=tool_name,
|
|
57
|
+
app_key=app_key,
|
|
58
|
+
record_id=record_id,
|
|
59
|
+
workflow_node_id=workflow_node_id,
|
|
60
|
+
note=note,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def feedback_submit(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
category: str,
|
|
67
|
+
title: str,
|
|
68
|
+
description: str,
|
|
69
|
+
expected_behavior: str | None,
|
|
70
|
+
actual_behavior: str | None,
|
|
71
|
+
impact_scope: str | None,
|
|
72
|
+
tool_name: str | None,
|
|
73
|
+
app_key: str | None,
|
|
74
|
+
record_id: str | int | None,
|
|
75
|
+
workflow_node_id: str | int | None,
|
|
76
|
+
note: str | None,
|
|
77
|
+
) -> JSONObject:
|
|
78
|
+
qsource_token = get_feedback_qsource_token()
|
|
79
|
+
if not qsource_token:
|
|
80
|
+
raise_tool_error(
|
|
81
|
+
QingflowApiError(
|
|
82
|
+
category="config",
|
|
83
|
+
message=(
|
|
84
|
+
"feedback_submit is not configured. Set "
|
|
85
|
+
"feedback.qsource_token or QINGFLOW_MCP_FEEDBACK_QSOURCE_TOKEN first."
|
|
86
|
+
),
|
|
87
|
+
details={"error_code": "FEEDBACK_NOT_CONFIGURED"},
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
base_url = get_feedback_base_url()
|
|
92
|
+
if not base_url:
|
|
93
|
+
raise_tool_error(
|
|
94
|
+
QingflowApiError.config_error(
|
|
95
|
+
"feedback_submit requires a base_url. Configure feedback.base_url or default_base_url."
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
normalized_payload = self._build_payload(
|
|
100
|
+
category=category,
|
|
101
|
+
title=title,
|
|
102
|
+
description=description,
|
|
103
|
+
expected_behavior=expected_behavior,
|
|
104
|
+
actual_behavior=actual_behavior,
|
|
105
|
+
impact_scope=impact_scope,
|
|
106
|
+
tool_name=tool_name,
|
|
107
|
+
app_key=app_key,
|
|
108
|
+
record_id=record_id,
|
|
109
|
+
workflow_node_id=workflow_node_id,
|
|
110
|
+
note=note,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
response = self.backend.public_request_with_meta(
|
|
115
|
+
"POST",
|
|
116
|
+
base_url,
|
|
117
|
+
f"/qsource/{qsource_token}",
|
|
118
|
+
json_body=normalized_payload,
|
|
119
|
+
unwrap=True,
|
|
120
|
+
qf_version=None,
|
|
121
|
+
)
|
|
122
|
+
except QingflowApiError as exc:
|
|
123
|
+
raise_tool_error(exc)
|
|
124
|
+
result = response.data if isinstance(response.data, dict) else {}
|
|
125
|
+
feedback_request_id = result.get("requestId") if isinstance(result, dict) else None
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"ok": True,
|
|
129
|
+
"request_route": {
|
|
130
|
+
"base_url": normalize_base_url(base_url) or base_url,
|
|
131
|
+
"qf_version": None,
|
|
132
|
+
"qf_version_source": "not_applicable",
|
|
133
|
+
},
|
|
134
|
+
"submission_mode": "qsource_passive",
|
|
135
|
+
"feedback_target": {
|
|
136
|
+
"app_key": get_feedback_app_key(),
|
|
137
|
+
"mcp_side": self.mcp_side,
|
|
138
|
+
},
|
|
139
|
+
"normalized_payload": normalized_payload,
|
|
140
|
+
"feedback_request_id": feedback_request_id,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def _build_payload(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
category: str,
|
|
147
|
+
title: str,
|
|
148
|
+
description: str,
|
|
149
|
+
expected_behavior: str | None,
|
|
150
|
+
actual_behavior: str | None,
|
|
151
|
+
impact_scope: str | None,
|
|
152
|
+
tool_name: str | None,
|
|
153
|
+
app_key: str | None,
|
|
154
|
+
record_id: str | int | None,
|
|
155
|
+
workflow_node_id: str | int | None,
|
|
156
|
+
note: str | None,
|
|
157
|
+
) -> JSONObject:
|
|
158
|
+
payload: JSONObject = {
|
|
159
|
+
"title": self._require_text("title", title),
|
|
160
|
+
"category": self._normalize_label("category", category, CATEGORY_MAP, required=True),
|
|
161
|
+
"description": self._require_text("description", description),
|
|
162
|
+
"submit_method": "AI代提",
|
|
163
|
+
"status": "待处理",
|
|
164
|
+
"mcp_side": self.mcp_side,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
optional_text = {
|
|
168
|
+
"expected_result": expected_behavior,
|
|
169
|
+
"actual_behavior": actual_behavior,
|
|
170
|
+
"tool_name": tool_name,
|
|
171
|
+
"app_key": app_key,
|
|
172
|
+
"note": note,
|
|
173
|
+
}
|
|
174
|
+
for key, value in optional_text.items():
|
|
175
|
+
normalized = self._normalize_optional_text(value)
|
|
176
|
+
if normalized is not None:
|
|
177
|
+
payload[key] = normalized
|
|
178
|
+
|
|
179
|
+
if impact_scope is not None and str(impact_scope).strip():
|
|
180
|
+
payload["impact_scope"] = self._normalize_label("impact_scope", impact_scope, IMPACT_SCOPE_MAP, required=False)
|
|
181
|
+
|
|
182
|
+
if record_id is not None and str(record_id).strip():
|
|
183
|
+
payload["record_id"] = str(record_id).strip()
|
|
184
|
+
if workflow_node_id is not None and str(workflow_node_id).strip():
|
|
185
|
+
payload["workflow_node_id"] = str(workflow_node_id).strip()
|
|
186
|
+
|
|
187
|
+
return payload
|
|
188
|
+
|
|
189
|
+
def _normalize_label(self, field: str, value: str, mapping: dict[str, str], *, required: bool) -> str:
|
|
190
|
+
text = str(value or "").strip()
|
|
191
|
+
if not text:
|
|
192
|
+
if required:
|
|
193
|
+
raise_tool_error(QingflowApiError.config_error(f"{field} is required"))
|
|
194
|
+
return ""
|
|
195
|
+
|
|
196
|
+
canonical = text.lower()
|
|
197
|
+
if canonical in mapping:
|
|
198
|
+
return mapping[canonical]
|
|
199
|
+
if text in mapping.values():
|
|
200
|
+
return text
|
|
201
|
+
supported_values = list(mapping.keys()) + list(mapping.values())
|
|
202
|
+
raise_tool_error(
|
|
203
|
+
QingflowApiError(
|
|
204
|
+
category="config",
|
|
205
|
+
message=f"{field} must be one of the supported canonical values or labels",
|
|
206
|
+
details={
|
|
207
|
+
"error_code": "FEEDBACK_INVALID_INPUT",
|
|
208
|
+
"field": field,
|
|
209
|
+
"supported_values": supported_values,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def _require_text(self, field: str, value: str) -> str:
|
|
215
|
+
normalized = str(value or "").strip()
|
|
216
|
+
if not normalized:
|
|
217
|
+
raise_tool_error(QingflowApiError.config_error(f"{field} is required"))
|
|
218
|
+
return normalized
|
|
219
|
+
|
|
220
|
+
def _normalize_optional_text(self, value: str | None) -> str | None:
|
|
221
|
+
if value is None:
|
|
222
|
+
return None
|
|
223
|
+
normalized = str(value).strip()
|
|
224
|
+
return normalized or None
|
|
@@ -3367,7 +3367,10 @@ class RecordTools(ToolBase):
|
|
|
3367
3367
|
for item in match_rules:
|
|
3368
3368
|
if not isinstance(item, dict):
|
|
3369
3369
|
continue
|
|
3370
|
-
|
|
3370
|
+
item_queries = _normalize_list_query_rules(item)
|
|
3371
|
+
if item_queries:
|
|
3372
|
+
queries.extend(item_queries)
|
|
3373
|
+
continue
|
|
3371
3374
|
normalized_match_rules.extend(_normalize_list_match_rules(item))
|
|
3372
3375
|
if queries:
|
|
3373
3376
|
body["queries"] = queries
|