@qingflow-tech/qingflow-cli 1.1.39 → 1.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +8 -8
- package/src/qingflow_mcp/server_cloud.py +89 -3
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1 -59
- package/src/qingflow_mcp/tools/app_tools.py +15 -29
- package/src/qingflow_mcp/tools/approval_tools.py +16 -40
- package/src/qingflow_mcp/tools/auth_tools.py +1 -4
- package/src/qingflow_mcp/tools/base.py +16 -34
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -5
- package/src/qingflow_mcp/tools/custom_button_tools.py +6 -11
- package/src/qingflow_mcp/tools/directory_tools.py +9 -16
- package/src/qingflow_mcp/tools/export_tools.py +5 -9
- package/src/qingflow_mcp/tools/feedback_tools.py +0 -2
- package/src/qingflow_mcp/tools/file_tools.py +3 -5
- package/src/qingflow_mcp/tools/import_tools.py +7 -13
- package/src/qingflow_mcp/tools/navigation_tools.py +12 -23
- package/src/qingflow_mcp/tools/package_tools.py +11 -21
- package/src/qingflow_mcp/tools/portal_tools.py +9 -17
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +18 -23
- package/src/qingflow_mcp/tools/record_tools.py +97 -53
- package/src/qingflow_mcp/tools/repository_dev_tools.py +4 -7
- package/src/qingflow_mcp/tools/resource_read_tools.py +6 -9
- package/src/qingflow_mcp/tools/role_tools.py +5 -9
- package/src/qingflow_mcp/tools/solution_tools.py +4 -16
- package/src/qingflow_mcp/tools/task_context_tools.py +6 -12
- package/src/qingflow_mcp/tools/task_tools.py +12 -29
- package/src/qingflow_mcp/tools/view_tools.py +23 -32
- package/src/qingflow_mcp/tools/workflow_tools.py +30 -47
- package/src/qingflow_mcp/tools/workspace_tools.py +5 -9
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Qingflow CLI 是轻流面向开发者和 AI Agent 的命令行工具。它把轻
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install @qingflow-tech/qingflow-cli@1.1.
|
|
10
|
+
npm install @qingflow-tech/qingflow-cli@1.1.40
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
推荐从官网复制这一条一键安装 Qingflow CLI:
|
|
@@ -21,7 +21,7 @@ npx qingflow-cli@latest install
|
|
|
21
21
|
## 运行
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npx -y -p @qingflow-tech/qingflow-cli@1.1.
|
|
24
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.40 qingflow
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
全局安装后可以直接运行:
|
|
@@ -55,7 +55,7 @@ qingflow-skills install --agent codex --scope user
|
|
|
55
55
|
如果你通过一次性 `npx -p` 执行 skills 安装,建议加 `--copy`,避免 symlink 指向 npm 临时执行缓存:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
npx -y -p @qingflow-tech/qingflow-cli@1.1.
|
|
58
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.40 qingflow-skills install --agent codex --scope user --copy
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
`qingflow-skills install` 默认使用 symlink,且遇到同名 skill 不会覆盖;只有显式传入 `--force` 才会替换。
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -294,8 +294,8 @@ class AiBuilderFacade:
|
|
|
294
294
|
self.directory = directory
|
|
295
295
|
self.solutions = solutions
|
|
296
296
|
|
|
297
|
-
def _with_backend_context(self, profile: str, handler):
|
|
298
|
-
return self.apps._run(profile, lambda _session_profile, context: handler(context))
|
|
297
|
+
def _with_backend_context(self, profile: str, handler, *, tool_name: str = "搭建后台上下文"):
|
|
298
|
+
return self.apps._run(profile, lambda _session_profile, context: handler(context), tool_name=tool_name)
|
|
299
299
|
|
|
300
300
|
def _read_app_base_raw_direct(self, *, profile: str, app_key: str) -> JSONObject:
|
|
301
301
|
return self._with_backend_context(
|
|
@@ -7392,7 +7392,7 @@ class AiBuilderFacade:
|
|
|
7392
7392
|
)
|
|
7393
7393
|
return _normalize_associated_resources_payload(payload, include_raw=include_raw)
|
|
7394
7394
|
|
|
7395
|
-
return self.apps._run(profile, runner)
|
|
7395
|
+
return self.apps._run(profile, runner, tool_name="关联资源读取")
|
|
7396
7396
|
|
|
7397
7397
|
def _match_field_index_for_app(self, *, profile: str, app_key: str) -> dict[int, dict[str, Any]]:
|
|
7398
7398
|
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
@@ -7466,7 +7466,7 @@ class AiBuilderFacade:
|
|
|
7466
7466
|
result = self.apps.backend.request("POST", context, f"/app/{app_key}/asosChart", json_body=payload)
|
|
7467
7467
|
return result if isinstance(result, dict) else {"result": result}
|
|
7468
7468
|
|
|
7469
|
-
return self.apps._run(profile, runner)
|
|
7469
|
+
return self.apps._run(profile, runner, tool_name="关联资源创建")
|
|
7470
7470
|
|
|
7471
7471
|
def _associated_resource_update(
|
|
7472
7472
|
self,
|
|
@@ -7489,21 +7489,21 @@ class AiBuilderFacade:
|
|
|
7489
7489
|
result = self.apps.backend.request("POST", context, f"/app/{app_key}/asosChart/{associated_item_id}", json_body=payload)
|
|
7490
7490
|
return result if isinstance(result, dict) else {"result": result}
|
|
7491
7491
|
|
|
7492
|
-
return self.apps._run(profile, runner)
|
|
7492
|
+
return self.apps._run(profile, runner, tool_name="关联资源更新")
|
|
7493
7493
|
|
|
7494
7494
|
def _associated_resource_delete(self, *, profile: str, app_key: str, associated_item_id: int) -> JSONObject:
|
|
7495
7495
|
def runner(_: Any, context: BackendRequestContext) -> JSONObject:
|
|
7496
7496
|
result = self.apps.backend.request("DELETE", context, f"/app/{app_key}/asosChart/{associated_item_id}")
|
|
7497
7497
|
return result if isinstance(result, dict) else {"result": result}
|
|
7498
7498
|
|
|
7499
|
-
return self.apps._run(profile, runner)
|
|
7499
|
+
return self.apps._run(profile, runner, tool_name="关联资源删除")
|
|
7500
7500
|
|
|
7501
7501
|
def _associated_resource_reorder(self, *, profile: str, app_key: str, associated_item_ids: list[int]) -> JSONObject:
|
|
7502
7502
|
def runner(_: Any, context: BackendRequestContext) -> JSONObject:
|
|
7503
7503
|
result = self.apps.backend.request("POST", context, f"/app/{app_key}/asosChart/ordinal", json_body=list(associated_item_ids))
|
|
7504
7504
|
return result if isinstance(result, dict) else {"result": result}
|
|
7505
7505
|
|
|
7506
|
-
return self.apps._run(profile, runner)
|
|
7506
|
+
return self.apps._run(profile, runner, tool_name="关联资源排序")
|
|
7507
7507
|
|
|
7508
7508
|
def _update_view_associated_resources_config(
|
|
7509
7509
|
self,
|
|
@@ -13246,7 +13246,7 @@ class AiBuilderFacade:
|
|
|
13246
13246
|
params={"beingDraft": True},
|
|
13247
13247
|
)
|
|
13248
13248
|
|
|
13249
|
-
self.buttons._run(profile, runner)
|
|
13249
|
+
self.buttons._run(profile, runner, tool_name="自定义按钮删除校验")
|
|
13250
13250
|
except (QingflowApiError, RuntimeError) as error:
|
|
13251
13251
|
api_error = _coerce_api_error(error)
|
|
13252
13252
|
if _delete_readback_is_not_found(api_error):
|
|
@@ -6,15 +6,20 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
import anyio
|
|
8
8
|
import uvicorn
|
|
9
|
+
from mcp.server.fastmcp import FastMCP
|
|
9
10
|
|
|
10
11
|
from .backend_client import BackendClient
|
|
11
12
|
from .cloud_auth import CloudAuthBroker
|
|
12
13
|
from .cloud_context import current_cloud_context
|
|
13
14
|
from .config import get_cloud_http_allowed_hosts, get_cloud_http_host, get_cloud_http_port
|
|
14
15
|
from .errors import QingflowApiError, is_auth_like_error
|
|
16
|
+
from .server_app_builder import build_builder_server
|
|
15
17
|
from .server_app_user import build_user_server
|
|
16
18
|
|
|
17
19
|
|
|
20
|
+
BUILDER_TOOL_PREFIX = "builder_"
|
|
21
|
+
|
|
22
|
+
|
|
18
23
|
ASGIApp = Callable[
|
|
19
24
|
[
|
|
20
25
|
dict[str, Any],
|
|
@@ -94,6 +99,7 @@ class CloudMcpAuthApp:
|
|
|
94
99
|
|
|
95
100
|
token = current_cloud_context.set(cloud_context)
|
|
96
101
|
try:
|
|
102
|
+
scope = _ensure_streamable_http_accept(scope)
|
|
97
103
|
await self._app(scope, receive, send)
|
|
98
104
|
finally:
|
|
99
105
|
current_cloud_context.reset(token)
|
|
@@ -160,16 +166,67 @@ class LazyCloudApp:
|
|
|
160
166
|
send: Callable[[dict[str, Any]], Awaitable[None]],
|
|
161
167
|
) -> None:
|
|
162
168
|
if self._app is None:
|
|
163
|
-
self._app =
|
|
169
|
+
self._app = build_cloud_app()
|
|
164
170
|
await self._app(scope, receive, send)
|
|
165
171
|
|
|
166
172
|
|
|
167
173
|
def build_cloud_user_app() -> CloudMcpAuthApp:
|
|
168
|
-
|
|
174
|
+
return build_cloud_app()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def build_cloud_app() -> CloudMcpAuthApp:
|
|
178
|
+
server = build_combined_server()
|
|
169
179
|
broker = CloudAuthBroker.from_config(backend=BackendClient())
|
|
170
180
|
return CloudMcpAuthApp(server.streamable_http_app(), broker)
|
|
171
181
|
|
|
172
182
|
|
|
183
|
+
def build_combined_server() -> FastMCP:
|
|
184
|
+
server = build_user_server()
|
|
185
|
+
builder_server = build_builder_server()
|
|
186
|
+
server._mcp_server.name = "Qingflow Combined MCP"
|
|
187
|
+
_register_prefixed_builder_tools(server, builder_server)
|
|
188
|
+
_set_combined_instructions(server, builder_server)
|
|
189
|
+
return server
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _register_prefixed_builder_tools(target_server: FastMCP, builder_server: FastMCP) -> None:
|
|
193
|
+
for original_name, tool in builder_server._tool_manager._tools.items():
|
|
194
|
+
target_server._tool_manager.add_tool(
|
|
195
|
+
tool.fn,
|
|
196
|
+
name=f"{BUILDER_TOOL_PREFIX}{original_name}",
|
|
197
|
+
title=tool.title,
|
|
198
|
+
description=tool.description,
|
|
199
|
+
annotations=tool.annotations,
|
|
200
|
+
icons=tool.icons,
|
|
201
|
+
meta=tool.meta,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _set_combined_instructions(user_server: FastMCP, builder_server: FastMCP) -> None:
|
|
206
|
+
user_instructions = user_server.instructions or ""
|
|
207
|
+
builder_instructions = builder_server.instructions or ""
|
|
208
|
+
user_server._mcp_server.instructions = f"""Use this combined Qingflow MCP server for both user operational workflows and builder workflows.
|
|
209
|
+
|
|
210
|
+
## Tool Namespaces
|
|
211
|
+
|
|
212
|
+
User tools keep their original names, such as `app_get`, `record_list`, `task_list`, `portal_get`,
|
|
213
|
+
and `chart_get`.
|
|
214
|
+
Builder tools are exposed with the `{BUILDER_TOOL_PREFIX}` prefix. For example, Builder `app_get` is
|
|
215
|
+
`builder_app_get`, Builder `view_get` is `builder_view_get`, and Builder `builder_tool_contract` is
|
|
216
|
+
`builder_builder_tool_contract`.
|
|
217
|
+
When reading the Builder workflow instructions below, add `{BUILDER_TOOL_PREFIX}` to every Builder tool
|
|
218
|
+
name before calling it.
|
|
219
|
+
|
|
220
|
+
## User Workflows
|
|
221
|
+
|
|
222
|
+
{user_instructions}
|
|
223
|
+
|
|
224
|
+
## Builder Workflows
|
|
225
|
+
|
|
226
|
+
{builder_instructions}
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
|
|
173
230
|
def _extract_host(scope: dict[str, Any]) -> str:
|
|
174
231
|
for key, value in scope.get("headers", []):
|
|
175
232
|
if key.lower() == b"host":
|
|
@@ -222,11 +279,40 @@ def _rewrite_host(scope: dict[str, Any], internal_host: str) -> dict[str, Any]:
|
|
|
222
279
|
return rewritten
|
|
223
280
|
|
|
224
281
|
|
|
282
|
+
def _ensure_streamable_http_accept(scope: dict[str, Any]) -> dict[str, Any]:
|
|
283
|
+
existing_values: list[str] = []
|
|
284
|
+
headers_without_accept: list[tuple[bytes, bytes]] = []
|
|
285
|
+
for key, value in scope.get("headers", []):
|
|
286
|
+
if key.lower() == b"accept":
|
|
287
|
+
existing_values.append(value.decode("latin1"))
|
|
288
|
+
else:
|
|
289
|
+
headers_without_accept.append((key, value))
|
|
290
|
+
|
|
291
|
+
required = ["application/json", "text/event-stream"]
|
|
292
|
+
merged_values = [value.strip() for value in ",".join(existing_values).split(",") if value.strip()]
|
|
293
|
+
normalized_media_types = {
|
|
294
|
+
value.split(";", 1)[0].strip().lower()
|
|
295
|
+
for value in merged_values
|
|
296
|
+
}
|
|
297
|
+
if not merged_values or "*/*" in normalized_media_types:
|
|
298
|
+
merged_values = list(required)
|
|
299
|
+
else:
|
|
300
|
+
for media_type in required:
|
|
301
|
+
if media_type not in normalized_media_types:
|
|
302
|
+
merged_values.append(media_type)
|
|
303
|
+
|
|
304
|
+
rewritten = dict(scope)
|
|
305
|
+
rewritten["headers"] = headers_without_accept + [
|
|
306
|
+
(b"accept", ", ".join(merged_values).encode("latin1"))
|
|
307
|
+
]
|
|
308
|
+
return rewritten
|
|
309
|
+
|
|
310
|
+
|
|
225
311
|
app = LazyCloudApp()
|
|
226
312
|
|
|
227
313
|
|
|
228
314
|
def main() -> None:
|
|
229
|
-
uvicorn.run(
|
|
315
|
+
uvicorn.run(build_cloud_app(), host=get_cloud_http_host(), port=get_cloud_http_port())
|
|
230
316
|
|
|
231
317
|
|
|
232
318
|
if __name__ == "__main__":
|