@josephyan/qingflow-app-user-mcp 0.2.0-beta.35 → 0.2.0-beta.37
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 +8 -5
- package/skills/qingflow-record-crud/SKILL.md +12 -6
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/config.py +2 -1
- package/src/qingflow_mcp/list_type_labels.py +24 -0
- package/src/qingflow_mcp/server.py +11 -4
- package/src/qingflow_mcp/server_app_user.py +15 -4
- package/src/qingflow_mcp/tools/app_tools.py +133 -1
- package/src/qingflow_mcp/tools/record_tools.py +1308 -159
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.37
|
|
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.37 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -31,6 +31,7 @@ Route to exactly one of these specialized paths:
|
|
|
31
31
|
## Routing Rules
|
|
32
32
|
|
|
33
33
|
- If the user does not know the target `app_key`, discover apps first with `app_list` or `app_search`, then route to the specialized skill
|
|
34
|
+
- If the app is known but the available data range is unclear, call `app_get` first and inspect `accessible_views`
|
|
34
35
|
- If the task is about browsing, reading, creating, updating, deleting, attachments, relations, subtable writes, or member/department-field candidate lookup, switch to `$qingflow-record-crud`
|
|
35
36
|
- If the task is about todo discovery, task context, approval actions, rollback or transfer, associated report review, or workflow log review, switch to `$qingflow-task-ops`
|
|
36
37
|
- If the task is about grouped distributions, ratios, rankings, trends, insights, or any final statistical conclusion, switch to `$qingflow-record-analysis`
|
|
@@ -9,13 +9,15 @@ metadata:
|
|
|
9
9
|
|
|
10
10
|
This skill is for final statistical conclusions only.
|
|
11
11
|
Assumes MCP is connected, authenticated, and on the correct workspace.
|
|
12
|
-
Analysis tasks must start with `record_schema_get`. Read top-level `fields` and `suggested_*`, then build field_id-based DSLs only.
|
|
12
|
+
Analysis tasks must start with `app_get`, then `record_schema_get(schema_mode="browse", view_id=...)`. Read top-level `fields` and `suggested_*`, then build field_id-based DSLs only.
|
|
13
|
+
Analysis tasks must start with `record_schema_get`.
|
|
14
|
+
If `app_get.accessible_views` marks a view with `analysis_supported=false`, do not use that view for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
|
|
13
15
|
|
|
14
|
-
## Step 1: `
|
|
16
|
+
## Step 1: `app_get` → Step 2: `record_schema_get(schema_mode="browse", view_id=...)` → Step 3: build DSL → Step 4: `record_analyze`
|
|
15
17
|
|
|
16
|
-
This is the ONLY execution order. Never skip
|
|
18
|
+
This is the ONLY execution order. Never skip `app_get` when the browse range is unclear. Never call `record_analyze` without a browse schema.
|
|
17
19
|
|
|
18
|
-
Core tools: `record_schema_get`, `record_analyze`. Use `record_list`/`record_get` only for post-analysis samples; task/comment work stays in [$qingflow-task-ops](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-task-ops/SKILL.md).
|
|
20
|
+
Core tools: `app_get`, `record_schema_get`, `record_analyze`. Use `record_list`/`record_get` only for post-analysis samples; task/comment work stays in [$qingflow-task-ops](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-task-ops/SKILL.md).
|
|
19
21
|
|
|
20
22
|
---
|
|
21
23
|
|
|
@@ -105,7 +107,8 @@ Top-level arguments:
|
|
|
105
107
|
- `dimensions`: `[]` = whole-table summary; `[{...}]` = grouped.
|
|
106
108
|
- `strict_full`: `true` for final conclusions. `false` allows partial results.
|
|
107
109
|
- `limit`: limits returned rows only, not scan scope.
|
|
108
|
-
- `
|
|
110
|
+
- `view_id`: the canonical browse selector. Prefer choosing it from `app_get.accessible_views`.
|
|
111
|
+
- Prefer `view_id` entries where `analysis_supported=true`. If a view is `boardView` or `ganttView`, switch to a system or table-style custom view before calling `record_analyze`.
|
|
109
112
|
- `bucket` in dimensions: only for `suggested_time_fields`. Values: `day`/`week`/`month`/`quarter`/`year`/`null`.
|
|
110
113
|
|
|
111
114
|
---
|
|
@@ -16,9 +16,9 @@ Assumes MCP is connected, authenticated, and on the correct workspace.
|
|
|
16
16
|
|
|
17
17
|
Use exactly one of these default paths:
|
|
18
18
|
|
|
19
|
-
1. Browse records: `record_schema_get -> record_list`
|
|
20
|
-
2. Read one record: `record_schema_get -> record_get`
|
|
21
|
-
3. Write records: `record_schema_get -> record_write`
|
|
19
|
+
1. Browse records: `app_get -> record_schema_get(schema_mode="browse", view_id=...) -> record_list`
|
|
20
|
+
2. Read one record: `app_get -> record_schema_get(schema_mode="browse", view_id=...) -> record_get`
|
|
21
|
+
3. Write records: `record_schema_get(schema_mode="applicant") -> record_write`
|
|
22
22
|
|
|
23
23
|
## Core Tools
|
|
24
24
|
|
|
@@ -27,12 +27,15 @@ Use exactly one of these default paths:
|
|
|
27
27
|
- `record_get`
|
|
28
28
|
- `record_write`
|
|
29
29
|
|
|
30
|
-
`record_schema_get`
|
|
30
|
+
`record_schema_get(schema_mode="applicant")` exposes the current user's applicant-node visible write/create fields.
|
|
31
|
+
`record_schema_get(schema_mode="browse", view_id=...)` exposes browse-schema fields for the selected accessible view.
|
|
32
|
+
Read top-level `fields` and `suggested_*`; if a field is missing, treat it as unavailable in the current permission scope.
|
|
31
33
|
|
|
32
34
|
## Supporting Tools
|
|
33
35
|
|
|
34
36
|
- `app_list`
|
|
35
37
|
- `app_search`
|
|
38
|
+
- `app_get`
|
|
36
39
|
- `record_member_candidates`
|
|
37
40
|
- `record_department_candidates`
|
|
38
41
|
- `directory_search`
|
|
@@ -50,7 +53,9 @@ Use `record_member_candidates` / `record_department_candidates` as the default l
|
|
|
50
53
|
2. Ensure workspace is selected
|
|
51
54
|
3. Confirm target app and whether the task is browse / detail / write / analysis
|
|
52
55
|
4. If `app_key` is unknown, use `app_list` or `app_search` first
|
|
53
|
-
5.
|
|
56
|
+
5. If browse/read range is unclear, run `app_get` and choose from `accessible_views`
|
|
57
|
+
6. Run `record_schema_get(schema_mode="browse", view_id=...)` before browse/read
|
|
58
|
+
7. Run `record_schema_get(schema_mode="applicant")` before write
|
|
54
59
|
6. If the request is analysis-like, switch to [$qingflow-record-analysis](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-analysis/SKILL.md)
|
|
55
60
|
7. If the request is write-like, decide `insert / update / delete` before building any payload
|
|
56
61
|
8. If fields are still ambiguous after `record_schema_get`, ask the user to confirm from a short candidate list instead of guessing
|
|
@@ -60,6 +65,7 @@ Use `record_member_candidates` / `record_department_candidates` as the default l
|
|
|
60
65
|
## Record Read Rules
|
|
61
66
|
|
|
62
67
|
- Use `record_list` for browse/export/sample inspection only
|
|
68
|
+
- Prefer choosing a `view_id` from `app_get.accessible_views`
|
|
63
69
|
- For `columns`, use `[{ "field_id": 12 }]`
|
|
64
70
|
- For `where`, use `{ "field_id": 12, "op": "eq", "value": "进行中" }`
|
|
65
71
|
- For `order_by`, use `{ "field_id": 18, "direction": "desc" }`
|
|
@@ -73,7 +79,7 @@ Use `record_write` as the only default write tool.
|
|
|
73
79
|
|
|
74
80
|
### Write workflow
|
|
75
81
|
|
|
76
|
-
1. Run `record_schema_get`
|
|
82
|
+
1. Run `record_schema_get(schema_mode="applicant")`
|
|
77
83
|
2. Decide whether the task is `insert`, `update`, or `delete`
|
|
78
84
|
3. For relation fields, read `target_app_key / target_app_name` from schema first
|
|
79
85
|
4. For member fields with unknown ids, run `record_member_candidates`
|
|
@@ -13,6 +13,7 @@ DEFAULT_RECORD_LIST_TYPE = 8
|
|
|
13
13
|
ATTACHMENT_QUESTION_TYPE = 13
|
|
14
14
|
DEFAULT_BASE_URL = "https://qingflow.com/api"
|
|
15
15
|
DEFAULT_FEEDBACK_APP_KEY = "e0d017kju002"
|
|
16
|
+
DEFAULT_FEEDBACK_QSOURCE_TOKEN = "mcp-feedback-7755d14748fc"
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def get_mcp_home() -> Path:
|
|
@@ -142,7 +143,7 @@ def get_feedback_qsource_token() -> str | None:
|
|
|
142
143
|
value = get_config_value(
|
|
143
144
|
"feedback.qsource_token",
|
|
144
145
|
env_var="QINGFLOW_MCP_FEEDBACK_QSOURCE_TOKEN",
|
|
145
|
-
default=
|
|
146
|
+
default=DEFAULT_FEEDBACK_QSOURCE_TOKEN,
|
|
146
147
|
)
|
|
147
148
|
if value is None:
|
|
148
149
|
return None
|
|
@@ -20,6 +20,18 @@ RECORD_LIST_TYPE_LABELS: dict[int, str] = {
|
|
|
20
20
|
16: "我发起的-已结束",
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
SYSTEM_VIEW_DEFINITIONS: tuple[tuple[str, int, str], ...] = (
|
|
24
|
+
("system:all", 8, "全部数据"),
|
|
25
|
+
("system:initiated", 14, "我发起的"),
|
|
26
|
+
("system:todo", 1, "待办"),
|
|
27
|
+
("system:done", 2, "已办"),
|
|
28
|
+
("system:cc", 12, "抄送我的"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
SYSTEM_VIEW_ID_TO_LIST_TYPE: dict[str, int] = {view_id: list_type for view_id, list_type, _ in SYSTEM_VIEW_DEFINITIONS}
|
|
32
|
+
SYSTEM_VIEW_ID_TO_NAME: dict[str, str] = {view_id: name for view_id, _, name in SYSTEM_VIEW_DEFINITIONS}
|
|
33
|
+
SYSTEM_LIST_TYPE_TO_VIEW_ID: dict[int, str] = {list_type: view_id for view_id, list_type, _ in SYSTEM_VIEW_DEFINITIONS}
|
|
34
|
+
|
|
23
35
|
TASK_TYPE_LABELS: dict[int, str] = {
|
|
24
36
|
1: "待办",
|
|
25
37
|
2: "我发起的",
|
|
@@ -40,6 +52,18 @@ def get_record_list_type_label(list_type: int | None) -> str | None:
|
|
|
40
52
|
return RECORD_LIST_TYPE_LABELS.get(list_type)
|
|
41
53
|
|
|
42
54
|
|
|
55
|
+
def get_system_view_id(list_type: int | None) -> str | None:
|
|
56
|
+
if list_type is None:
|
|
57
|
+
return None
|
|
58
|
+
return SYSTEM_LIST_TYPE_TO_VIEW_ID.get(list_type)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_system_view_name(view_id: str | None) -> str | None:
|
|
62
|
+
if view_id is None:
|
|
63
|
+
return None
|
|
64
|
+
return SYSTEM_VIEW_ID_TO_NAME.get(view_id)
|
|
65
|
+
|
|
66
|
+
|
|
43
67
|
def get_task_type_label(type_value: int | None) -> str | None:
|
|
44
68
|
if type_value is None:
|
|
45
69
|
return None
|
|
@@ -47,17 +47,21 @@ All resource tools operate with the logged-in user's Qingflow permissions.
|
|
|
47
47
|
## App Discovery
|
|
48
48
|
|
|
49
49
|
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
50
|
+
If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
|
|
51
|
+
If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
|
|
50
52
|
|
|
51
53
|
## Schema-First Rule
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
Call `record_schema_get(schema_mode="applicant")` before `record_write`.
|
|
56
|
+
Call `app_get` first when the data range is unclear, then use `record_schema_get(schema_mode="browse", view_id=...)` before `record_list`, `record_get`, or `record_analyze`.
|
|
54
57
|
|
|
55
58
|
- All `field_id` values must come from the schema response.
|
|
56
59
|
- Never guess field names or ids.
|
|
57
60
|
|
|
58
61
|
## Schema Scope
|
|
59
62
|
|
|
60
|
-
`record_schema_get` returns the current user's applicant-node visible fields
|
|
63
|
+
`record_schema_get(schema_mode="applicant")` returns the current user's applicant-node visible fields for write/create.
|
|
64
|
+
`record_schema_get(schema_mode="browse", view_id=...)` returns browse-schema fields for the selected accessible view.
|
|
61
65
|
|
|
62
66
|
- Hidden fields are omitted.
|
|
63
67
|
- Missing fields mean the field is not visible in the current permission scope.
|
|
@@ -65,7 +69,9 @@ Always call `record_schema_get` before `record_list`, `record_get`, `record_writ
|
|
|
65
69
|
|
|
66
70
|
## Analytics Path
|
|
67
71
|
|
|
68
|
-
`record_schema_get -> record_analyze`
|
|
72
|
+
`app_get -> record_schema_get(schema_mode="browse", view_id=...) -> record_analyze`
|
|
73
|
+
|
|
74
|
+
Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
|
|
69
75
|
|
|
70
76
|
Use this DSL shape:
|
|
71
77
|
|
|
@@ -86,7 +92,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
86
92
|
|
|
87
93
|
## Record CRUD Path
|
|
88
94
|
|
|
89
|
-
`record_schema_get -> record_list / record_get
|
|
95
|
+
`app_get -> record_schema_get(schema_mode="browse", view_id=...) -> record_list / record_get`
|
|
96
|
+
`record_schema_get(schema_mode="applicant") -> record_write`
|
|
90
97
|
|
|
91
98
|
- Use `columns` as `[{{field_id}}]`
|
|
92
99
|
- Use `where` items as `{{field_id, op, value}}`
|
|
@@ -27,6 +27,8 @@ def build_user_server() -> FastMCP:
|
|
|
27
27
|
## App Discovery
|
|
28
28
|
|
|
29
29
|
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
30
|
+
If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
|
|
31
|
+
If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
|
|
30
32
|
|
|
31
33
|
## Shared Helper
|
|
32
34
|
|
|
@@ -38,14 +40,16 @@ If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
|
38
40
|
|
|
39
41
|
## Schema-First Rule
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
Call `record_schema_get(schema_mode="applicant")` before `record_write`.
|
|
44
|
+
Call `app_get` first when the data range is unclear, then use `record_schema_get(schema_mode="browse", view_id=...)` before `record_list`, `record_get`, or `record_analyze`.
|
|
42
45
|
|
|
43
46
|
- All `field_id` values must come from the schema response.
|
|
44
47
|
- Never guess field names or ids.
|
|
45
48
|
|
|
46
49
|
## Schema Scope
|
|
47
50
|
|
|
48
|
-
`record_schema_get` returns the current user's applicant-node visible fields
|
|
51
|
+
`record_schema_get(schema_mode="applicant")` returns the current user's applicant-node visible fields for write/create.
|
|
52
|
+
`record_schema_get(schema_mode="browse", view_id=...)` returns browse-schema fields for the selected accessible view.
|
|
49
53
|
|
|
50
54
|
- Hidden fields are omitted.
|
|
51
55
|
- Missing fields mean the field is not visible in the current permission scope.
|
|
@@ -53,7 +57,9 @@ Always call `record_schema_get` before `record_list`, `record_get`, `record_writ
|
|
|
53
57
|
|
|
54
58
|
## Analytics Path
|
|
55
59
|
|
|
56
|
-
`record_schema_get -> record_analyze`
|
|
60
|
+
`app_get -> record_schema_get(schema_mode="browse", view_id=...) -> record_analyze`
|
|
61
|
+
|
|
62
|
+
Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
|
|
57
63
|
|
|
58
64
|
Use this DSL shape:
|
|
59
65
|
|
|
@@ -74,7 +80,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
74
80
|
|
|
75
81
|
## Record CRUD Path
|
|
76
82
|
|
|
77
|
-
`record_schema_get -> record_list / record_get
|
|
83
|
+
`app_get -> record_schema_get(schema_mode="browse", view_id=...) -> record_list / record_get`
|
|
84
|
+
`record_schema_get(schema_mode="applicant") -> record_write`
|
|
78
85
|
|
|
79
86
|
- Use `columns` as `[{{field_id}}]`
|
|
80
87
|
- Use `where` items as `{{field_id, op, value}}`
|
|
@@ -201,6 +208,10 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
201
208
|
def app_search(profile: str = DEFAULT_PROFILE, keyword: str = "", page_num: int = 1, page_size: int = 50) -> dict:
|
|
202
209
|
return apps.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
|
|
203
210
|
|
|
211
|
+
@server.tool()
|
|
212
|
+
def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
|
|
213
|
+
return apps.app_get(profile=profile, app_key=app_key)
|
|
214
|
+
|
|
204
215
|
@server.tool()
|
|
205
216
|
def file_get_upload_info(
|
|
206
217
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -9,7 +9,7 @@ from ..backend_client import BackendRequestContext
|
|
|
9
9
|
from ..config import DEFAULT_PROFILE
|
|
10
10
|
from ..errors import QingflowApiError, raise_tool_error
|
|
11
11
|
from ..json_types import JSONObject
|
|
12
|
-
from ..list_type_labels import get_app_publish_status_label
|
|
12
|
+
from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS, get_app_publish_status_label
|
|
13
13
|
from .base import ToolBase
|
|
14
14
|
|
|
15
15
|
|
|
@@ -23,6 +23,10 @@ class AppTools(ToolBase):
|
|
|
23
23
|
def app_search(profile: str = DEFAULT_PROFILE, keyword: str = "", page_num: int = 1, page_size: int = 50) -> JSONObject:
|
|
24
24
|
return self.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
|
|
25
25
|
|
|
26
|
+
@mcp.tool()
|
|
27
|
+
def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
28
|
+
return self.app_get(profile=profile, app_key=app_key)
|
|
29
|
+
|
|
26
30
|
@mcp.tool()
|
|
27
31
|
def app_get_base(profile: str = DEFAULT_PROFILE, app_key: str = "", include_raw: bool = False) -> JSONObject:
|
|
28
32
|
return self.app_get_base(profile=profile, app_key=app_key, include_raw=include_raw)
|
|
@@ -132,6 +136,50 @@ class AppTools(ToolBase):
|
|
|
132
136
|
|
|
133
137
|
return self._run(profile, runner)
|
|
134
138
|
|
|
139
|
+
def app_get(self, *, profile: str, app_key: str) -> JSONObject:
|
|
140
|
+
self._require_app_key(app_key)
|
|
141
|
+
|
|
142
|
+
def runner(session_profile, context):
|
|
143
|
+
warnings: list[JSONObject] = []
|
|
144
|
+
app_name = app_key
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
148
|
+
if isinstance(base_info, dict):
|
|
149
|
+
app_name = (
|
|
150
|
+
str(base_info.get("formTitle") or base_info.get("title") or base_info.get("appName") or app_key).strip()
|
|
151
|
+
or app_key
|
|
152
|
+
)
|
|
153
|
+
except QingflowApiError as exc:
|
|
154
|
+
if exc.backend_code not in {40002, 40027}:
|
|
155
|
+
raise
|
|
156
|
+
warnings.append(
|
|
157
|
+
{
|
|
158
|
+
"code": "APP_BASE_INFO_UNAVAILABLE",
|
|
159
|
+
"message": f"app_get could not load base info for {app_key}; using app_key as app_name.",
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
can_create = self._probe_create_access(context, app_key)
|
|
164
|
+
accessible_views = self._resolve_accessible_system_views(context, app_key)
|
|
165
|
+
accessible_views.extend(self._resolve_accessible_custom_views(context, app_key))
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"profile": profile,
|
|
169
|
+
"ws_id": session_profile.selected_ws_id,
|
|
170
|
+
"ok": True,
|
|
171
|
+
"request_route": {"base_url": context.base_url, "qf_version": context.qf_version},
|
|
172
|
+
"warnings": warnings,
|
|
173
|
+
"data": {
|
|
174
|
+
"app_key": app_key,
|
|
175
|
+
"app_name": app_name,
|
|
176
|
+
"can_create": can_create,
|
|
177
|
+
"accessible_views": accessible_views,
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return self._run(profile, runner)
|
|
182
|
+
|
|
135
183
|
def app_get_base(self, *, profile: str, app_key: str, include_raw: bool = False) -> JSONObject:
|
|
136
184
|
self._require_app_key(app_key)
|
|
137
185
|
|
|
@@ -313,6 +361,67 @@ class AppTools(ToolBase):
|
|
|
313
361
|
if not app_key:
|
|
314
362
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
315
363
|
|
|
364
|
+
def _probe_create_access(self, context: BackendRequestContext, app_key: str) -> bool:
|
|
365
|
+
try:
|
|
366
|
+
self.backend.request(
|
|
367
|
+
"GET",
|
|
368
|
+
context,
|
|
369
|
+
f"/app/{app_key}/form",
|
|
370
|
+
params={"type": 2, "beingApply": True},
|
|
371
|
+
)
|
|
372
|
+
return True
|
|
373
|
+
except QingflowApiError as exc:
|
|
374
|
+
if exc.backend_code in {40002, 40027}:
|
|
375
|
+
return False
|
|
376
|
+
raise
|
|
377
|
+
|
|
378
|
+
def _probe_list_type_access(self, context: BackendRequestContext, app_key: str, list_type: int) -> bool:
|
|
379
|
+
try:
|
|
380
|
+
self.backend.request(
|
|
381
|
+
"POST",
|
|
382
|
+
context,
|
|
383
|
+
f"/app/{app_key}/apply/filter",
|
|
384
|
+
json_body={"type": list_type, "pageNum": 1, "pageSize": 1},
|
|
385
|
+
)
|
|
386
|
+
return True
|
|
387
|
+
except QingflowApiError as exc:
|
|
388
|
+
if exc.backend_code in {40002, 40027}:
|
|
389
|
+
return False
|
|
390
|
+
raise
|
|
391
|
+
|
|
392
|
+
def _resolve_accessible_system_views(self, context: BackendRequestContext, app_key: str) -> list[JSONObject]:
|
|
393
|
+
items: list[JSONObject] = []
|
|
394
|
+
for view_id, list_type, name in SYSTEM_VIEW_DEFINITIONS:
|
|
395
|
+
if not self._probe_list_type_access(context, app_key, list_type):
|
|
396
|
+
continue
|
|
397
|
+
items.append({"view_id": view_id, "name": name, "kind": "system", "analysis_supported": True})
|
|
398
|
+
return items
|
|
399
|
+
|
|
400
|
+
def _resolve_accessible_custom_views(self, context: BackendRequestContext, app_key: str) -> list[JSONObject]:
|
|
401
|
+
try:
|
|
402
|
+
payload = self.backend.request("GET", context, f"/app/{app_key}/view/viewList")
|
|
403
|
+
except QingflowApiError as exc:
|
|
404
|
+
if exc.backend_code in {40002, 40027}:
|
|
405
|
+
return []
|
|
406
|
+
raise
|
|
407
|
+
|
|
408
|
+
items: list[JSONObject] = []
|
|
409
|
+
for item in _normalize_view_list(payload):
|
|
410
|
+
view_key = str(item.get("viewKey") or "").strip()
|
|
411
|
+
if not view_key:
|
|
412
|
+
continue
|
|
413
|
+
normalized: JSONObject = {
|
|
414
|
+
"view_id": f"custom:{view_key}",
|
|
415
|
+
"name": str(item.get("viewName") or view_key).strip() or view_key,
|
|
416
|
+
"kind": "custom",
|
|
417
|
+
}
|
|
418
|
+
view_type = str(item.get("viewType") or item.get("viewgraphType") or "").strip()
|
|
419
|
+
if view_type:
|
|
420
|
+
normalized["view_type"] = view_type
|
|
421
|
+
normalized["analysis_supported"] = _analysis_supported_for_view_type(view_type or None)
|
|
422
|
+
items.append(normalized)
|
|
423
|
+
return items
|
|
424
|
+
|
|
316
425
|
def _compact_base_info(self, result: dict[str, Any]) -> JSONObject:
|
|
317
426
|
publish_status = result.get("appPublishStatus")
|
|
318
427
|
auth = result.get("auth") if isinstance(result.get("auth"), dict) else {}
|
|
@@ -562,6 +671,29 @@ def _normalize_form_type(value: int | str) -> int:
|
|
|
562
671
|
raise_tool_error(QingflowApiError.config_error("form_type must be a positive integer or one of: default, form, schema, new, draft, edit"))
|
|
563
672
|
|
|
564
673
|
|
|
674
|
+
def _normalize_view_list(payload: Any) -> list[JSONObject]:
|
|
675
|
+
if not isinstance(payload, list):
|
|
676
|
+
return []
|
|
677
|
+
flattened: list[JSONObject] = []
|
|
678
|
+
for group in payload:
|
|
679
|
+
if not isinstance(group, dict):
|
|
680
|
+
continue
|
|
681
|
+
view_list = group.get("viewList")
|
|
682
|
+
if not isinstance(view_list, list):
|
|
683
|
+
continue
|
|
684
|
+
for item in view_list:
|
|
685
|
+
if isinstance(item, dict) and item.get("viewKey"):
|
|
686
|
+
flattened.append(item)
|
|
687
|
+
return flattened
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _analysis_supported_for_view_type(view_type: str | None) -> bool:
|
|
691
|
+
normalized = str(view_type or "").strip().lower()
|
|
692
|
+
if not normalized:
|
|
693
|
+
return True
|
|
694
|
+
return normalized not in {"boardview", "ganttview"}
|
|
695
|
+
|
|
696
|
+
|
|
565
697
|
def _coerce_positive_int(value: Any) -> int | None:
|
|
566
698
|
try:
|
|
567
699
|
number = int(value)
|