@qingflow-tech/qingflow-cli 1.1.47-beta.5 → 1.1.48
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/skills/qingflow-cli/SKILL.md +4 -26
- package/skills/qingflow-common/SKILL.md +15 -5
- package/src/qingflow_mcp/builder_facade/service.py +1 -119
- package/src/qingflow_mcp/cli/commands/__init__.py +1 -2
- package/src/qingflow_mcp/cli/commands/auth.py +1 -1
- package/src/qingflow_mcp/cli/context.py +1 -4
- package/src/qingflow_mcp/cli/formatters.py +3 -62
- package/src/qingflow_mcp/cli/main.py +1 -4
- package/src/qingflow_mcp/public_surface.py +0 -6
- package/src/qingflow_mcp/response_trim.py +3 -3
- package/src/qingflow_mcp/server.py +0 -2
- package/src/qingflow_mcp/server_app_builder.py +0 -16
- package/src/qingflow_mcp/server_app_user.py +3 -10
- package/src/qingflow_mcp/tools/ai_builder_tools.py +39 -208
- package/src/qingflow_mcp/tools/app_tools.py +13 -52
- package/src/qingflow_mcp/tools/login_link_tools.py +6 -34
- package/src/qingflow_mcp/tools/record_tools.py +46 -11
- package/src/qingflow_mcp/tools/resource_read_tools.py +0 -13
- package/src/qingflow_mcp/cli/commands/data.py +0 -107
- package/src/qingflow_mcp/resource_uri.py +0 -241
- package/src/qingflow_mcp/tools/csv_export.py +0 -168
- package/src/qingflow_mcp/tools/data_tools.py +0 -240
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.48
|
|
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.48 qingflow
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
全局安装后可以直接运行:
|
|
@@ -62,7 +62,7 @@ qingflow-skills install --agent codex --scope user
|
|
|
62
62
|
如果你通过一次性 `npx -p` 执行 skills 安装,建议加 `--copy`,避免 symlink 指向 npm 临时执行缓存:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
npx -y -p @qingflow-tech/qingflow-cli@1.1.
|
|
65
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.48 qingflow-skills install --agent codex --scope user --copy
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
`qingflow-skills install` 默认使用 symlink,且遇到同名 skill 不会覆盖;只有显式传入 `--force` 才会替换。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qingflow-tech/qingflow-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.48",
|
|
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
|
@@ -124,27 +124,6 @@ qingflow auth login \
|
|
|
124
124
|
qingflow --json auth whoami
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
### 交付网页资源链接
|
|
128
|
-
|
|
129
|
-
创建、修改或安装轻流资源后,最终回复主动提供主要结果的可点击页面入口,无需等待用户再次索要。采用“完成结果 + 查看入口”,文案使用“在轻流中查看应用/应用包/视图”,不默认称为“继续编辑”。
|
|
130
|
-
|
|
131
|
-
默认只交付用户请求的主要结果:安装方案默认提供应用包入口;直接创建或修改应用、视图时提供对应入口;用户明确要求多个主要对象时分别提供。使用业务结果中的稳定 `uri`,按 URI 去重,不为全部子资源预签票据。主要对象没有可靠 URI 时说明限制,不猜地址,也不替换为不相关页面。
|
|
132
|
-
|
|
133
|
-
CLI 返回的稳定 `uri` 以 `QINGFLOW_BASE_URL` 为当前环境来源:去除末尾 API 路径后使用同一 HTTP(S) 域名;仅当该变量为空时回退 `https://qingflow.com`。若非空配置非法,保留 URI 不可用诊断,不自行替换或拼接其他环境地址。
|
|
134
|
-
|
|
135
|
-
部分成功时仅交付已确认成功且可定位的主要结果,保留失败或未验证说明。纯数据问答、中间读取、失败资源和删除操作不自动签发;用户明确要求不提供链接时遵从。用户明确要求查看或打开页面时也使用已确认的路径。
|
|
136
|
-
|
|
137
|
-
目标浏览器登录态未知时,临近交付取主要结果稳定 `uri` 的完整 pathname 执行,不得丢失应用包、应用或视图层级;视图任务不得退化成应用入口:
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
|
-
qingflow auth login-link --redirect-path "<主要结果 uri 的完整 pathname>"
|
|
141
|
-
# 自定义视图 pathname 示例:/resources/ws/<WS_ID>/app/<APP_KEY>/view/custom/<VIEW_KEY>
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
对每个实际展示的入口分别签发,使用本次返回的完整 URL,不额外追问登录态,不复用已消费链接。只有已有用户确认同一浏览器、网页域名、账号和工作区已登录、没有失效信号的事实,才直接交付稳定 `uri`。CLI 已登录、自动化浏览器已登录或曾经成功签发票据都不能证明目标浏览器已登录。
|
|
145
|
-
|
|
146
|
-
用户未要求打开时不要提前打开或消费链接;签发失败时说明免登录链接未生成,并提供标注“需登录”的稳定 `uri` 作为备用入口,不重做业务操作。机器结果继续保留稳定 URI,临时登录链接仅用于面向用户交付。
|
|
147
|
-
|
|
148
127
|
常用参数:`--credential-stdin`、`--password-stdin`、`--persist` / `--forget-persisted`;详见 `qingflow auth --help`。
|
|
149
128
|
|
|
150
129
|
---
|
|
@@ -470,18 +449,17 @@ qingflow auth logout --forget-persisted
|
|
|
470
449
|
|
|
471
450
|
### 临时网页登录链接
|
|
472
451
|
|
|
473
|
-
|
|
452
|
+
仅在用户需要进入网页查看或继续操作时生成,纯数据查询不自动调用:
|
|
474
453
|
|
|
475
454
|
```bash
|
|
476
455
|
qingflow --json auth login-link
|
|
477
|
-
qingflow --profile default --json auth login-link --redirect-path "
|
|
478
|
-
# 自定义视图 pathname 示例:/resources/ws/<WS_ID>/app/<APP_KEY>/view/custom/<VIEW_KEY>
|
|
456
|
+
qingflow --profile default --json auth login-link --redirect-path "/index/app/123?view=abc"
|
|
479
457
|
```
|
|
480
458
|
|
|
481
|
-
- 默认使用当前会话和已选工作区;缺失时按实际错误恢复,不额外预查或重新登录。
|
|
459
|
+
- 默认使用当前会话和已选工作区;缺失时按实际错误恢复,不额外预查或重新登录。
|
|
482
460
|
- `--redirect-path` 可选,仅接受已确认的站内业务路径,可包含 query/hash;不接受外域或认证入口。示例 ID 仅作格式展示,不得照抄或编造实际目标;省略时进入工作区首页。
|
|
483
461
|
- 将返回的 `url` 作为可点击链接交付,不修改或拼造。链接短时有效且仅可使用一次,失效后重新获取,不写入公共文档或日志。
|
|
484
|
-
-
|
|
462
|
+
- 命令不会自动打开浏览器;用户仅索取链接时不提前打开,明确要求打开时才调用浏览器。
|
|
485
463
|
|
|
486
464
|
**高发必败**:旧应用搜索入口 · **`record list`/`browse`/`access` 缺 `--view-id`** · **`task report` 缺 `--report-id`** · **`record update` 批量与单条参数混用**。
|
|
487
465
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qingflow-common
|
|
3
|
-
description:
|
|
3
|
+
description: 解释轻流公共业务概念并选择 CLI 或 MCP 入口。用于能力比较、记录与任务操作、表单与流程搭建的任务分流,以及从 AI 对话进入页面查看结果、继续编辑、处理业务或进入工作区的场景。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Qingflow Common Concepts
|
|
@@ -31,7 +31,7 @@ Do not require both surfaces. If only one is installed, complete the workflow on
|
|
|
31
31
|
| Form schema/read/apply/delete | `qingflow --json builder app-form ...` / `builder app delete` | `app_form_schema`, `app_form_get`, `app_form_apply`, `app_delete` |
|
|
32
32
|
| AppForm static validation | `qingflow --json builder app-form validate --file ...` | Performed inside `app_form_apply`; no separate tool |
|
|
33
33
|
| Record and task work | `qingflow --json record ...` / `task ...` | Qingflow MCP record and task tools |
|
|
34
|
-
|
|
|
34
|
+
| 免登录打开网页或指定业务页面 | `qingflow --json auth login-link [--redirect-path PATH]` | `get_login_link` |
|
|
35
35
|
| Package, view, portal, chart, workflow | `qingflow --json builder ...` | Qingflow MCP configuration tools |
|
|
36
36
|
|
|
37
37
|
The operation names and payload schemas are defined by the public surface. Use the current Schema or tool input schema as the source of truth, not a legacy command name or backend payload.
|
|
@@ -43,10 +43,20 @@ The operation names and payload schemas are defined by the public surface. Use t
|
|
|
43
43
|
|
|
44
44
|
Each package includes this common Skill. The CLI package installs its CLI guidance; the MCP package installs every MCP domain Skill. The source of every packaged Skill is `qingflow-support/mcp-server/skills/`.
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## 从 AI 对话进入轻流页面
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
用户需要查看结果、继续编辑、处理业务或进入工作区页面时,MCP 调用 `get_login_link`,CLI 调用 `qingflow --json auth login-link`。纯数据查询无需页面,不自动生成链接。
|
|
49
|
+
|
|
50
|
+
- 目标路径使用用户提供或业务工具确认的站内路径,不根据应用、记录标识猜测地址。
|
|
51
|
+
- 用户明确指定页面但路径未知时继续定位或说明限制,不猜测,也不静默替换成首页。搜索未命中时继续搜索 `login` 或查看服务工具列表,不擅自打开固定正式站点;失败时如实说明。
|
|
52
|
+
|
|
53
|
+
- 默认使用当前会话及已选工作区;需要指定会话时使用 MCP 的 `profile` 或 CLI 的 `--profile`。
|
|
54
|
+
- `redirect_path` 可选,默认空:省略时进入工作区首页;传入时仅支持以单个 `/` 开头的站内业务路径,可带 query/hash。不得传完整 URL、外部地址、认证入口或多层编码路径。
|
|
55
|
+
- 示例:`get_login_link({})`;指定目标示例:`get_login_link({"redirect_path":"/index/app/123?view=abc"})`。示例标识仅作格式展示,实际目标必须来自用户提供或已经确认的页面信息,不得编造应用、记录标识。
|
|
56
|
+
- 临近交付时再生成,使用返回的 `url`,文案贴合“查看记录”“继续编辑”等上下文,不展示工作区数字 ID 或原始 JSON。
|
|
57
|
+
- CLI 示例:`qingflow --json auth login-link`;指定目标使用 `qingflow --profile default --json auth login-link --redirect-path "/index/app/123?view=abc"`,同样不得编造实际路径。
|
|
58
|
+
- 链接短时有效且仅可使用一次,失效后重新获取,不修改或拼造链接;只交付给请求用户,不写入公共文档或日志。
|
|
59
|
+
- 用户要求“给我链接”时仅交付,不提前消费;要求“打开”时才使用可用浏览器打开。浏览器不可用时提供可点击链接,不假称已经打开。
|
|
50
60
|
|
|
51
61
|
## Route Next
|
|
52
62
|
|
|
@@ -19,7 +19,6 @@ from ..backend_client import BackendRequestContext
|
|
|
19
19
|
from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error
|
|
20
20
|
from ..json_types import JSONObject
|
|
21
21
|
from ..list_type_labels import RECORD_LIST_TYPE_LABELS, SYSTEM_VIEW_ID_TO_LIST_TYPE
|
|
22
|
-
from ..resource_uri import validate_positive_resource_id, validate_resource_identity
|
|
23
22
|
from ..solution.build_assembly_store import BuildAssemblyStore, default_artifacts, default_manifest
|
|
24
23
|
from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
|
|
25
24
|
from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
|
|
@@ -1492,74 +1491,6 @@ class AiBuilderFacade:
|
|
|
1492
1491
|
details={"solution_key": requested_solution_key},
|
|
1493
1492
|
suggested_next_call={"tool_name": "solution_install", "arguments": {"profile": profile, **normalized_args}},
|
|
1494
1493
|
)
|
|
1495
|
-
raw_result = created.get("result") if isinstance(created.get("result"), dict) else {}
|
|
1496
|
-
raw_tag_ids = raw_result.get("tagIds")
|
|
1497
|
-
raw_app_keys = raw_result.get("appKeys")
|
|
1498
|
-
tag_ids, tag_ids_complete = _unique_resource_tag_ids(raw_tag_ids)
|
|
1499
|
-
app_keys, app_keys_complete = _unique_resource_app_keys(raw_app_keys)
|
|
1500
|
-
resource_list_complete = tag_ids_complete and app_keys_complete
|
|
1501
|
-
resources: list[JSONObject] = []
|
|
1502
|
-
warnings: list[JSONObject] = []
|
|
1503
|
-
|
|
1504
|
-
for tag_id in tag_ids:
|
|
1505
|
-
name = None
|
|
1506
|
-
try:
|
|
1507
|
-
detail = self.packages.package_get(profile=profile, tag_id=tag_id, include_raw=False)
|
|
1508
|
-
package = detail.get("result") if isinstance(detail.get("result"), dict) else {}
|
|
1509
|
-
name = str(package.get("tagName") or package.get("tag_name") or "").strip() or None
|
|
1510
|
-
except (QingflowApiError, RuntimeError):
|
|
1511
|
-
pass
|
|
1512
|
-
if not name:
|
|
1513
|
-
warnings.append(_warning("RESOURCE_NAME_UNAVAILABLE", "installed package name could not be read", package_id=tag_id))
|
|
1514
|
-
resources.append(
|
|
1515
|
-
_compact_dict(
|
|
1516
|
-
{
|
|
1517
|
-
"resource_type": "package",
|
|
1518
|
-
"operation": "installed",
|
|
1519
|
-
"status": "success",
|
|
1520
|
-
"id": tag_id,
|
|
1521
|
-
"key": str(tag_id),
|
|
1522
|
-
"name": name,
|
|
1523
|
-
"ids": {"package_id": tag_id},
|
|
1524
|
-
}
|
|
1525
|
-
)
|
|
1526
|
-
)
|
|
1527
|
-
|
|
1528
|
-
for app_key in app_keys:
|
|
1529
|
-
name = None
|
|
1530
|
-
parent = None
|
|
1531
|
-
try:
|
|
1532
|
-
detail = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=False)
|
|
1533
|
-
app = detail.get("result") if isinstance(detail.get("result"), dict) else {}
|
|
1534
|
-
name = str(app.get("formTitle") or app.get("appName") or "").strip() or None
|
|
1535
|
-
matching_tag_ids = [tag_id for tag_id in _unique_positive_ints(app.get("tagIds")) if tag_id in tag_ids]
|
|
1536
|
-
if len(matching_tag_ids) == 1:
|
|
1537
|
-
parent = {"resource_type": "package", "id": matching_tag_ids[0], "key": str(matching_tag_ids[0])}
|
|
1538
|
-
except (QingflowApiError, RuntimeError):
|
|
1539
|
-
pass
|
|
1540
|
-
if not name:
|
|
1541
|
-
warnings.append(_warning("RESOURCE_NAME_UNAVAILABLE", "installed application name could not be read", app_key=app_key))
|
|
1542
|
-
resources.append(
|
|
1543
|
-
_compact_dict(
|
|
1544
|
-
{
|
|
1545
|
-
"resource_type": "app",
|
|
1546
|
-
"operation": "installed",
|
|
1547
|
-
"status": "success",
|
|
1548
|
-
"key": app_key,
|
|
1549
|
-
"name": name,
|
|
1550
|
-
"ids": {"app_key": app_key},
|
|
1551
|
-
"parent": parent,
|
|
1552
|
-
}
|
|
1553
|
-
)
|
|
1554
|
-
)
|
|
1555
|
-
|
|
1556
|
-
if not resource_list_complete:
|
|
1557
|
-
warnings.append(
|
|
1558
|
-
_warning(
|
|
1559
|
-
"RESOURCE_LIST_INCOMPLETE",
|
|
1560
|
-
"solution installation succeeded, but its returned package/application identity list was incomplete",
|
|
1561
|
-
)
|
|
1562
|
-
)
|
|
1563
1494
|
return {
|
|
1564
1495
|
"status": "success",
|
|
1565
1496
|
"error_code": None,
|
|
@@ -1572,15 +1503,9 @@ class AiBuilderFacade:
|
|
|
1572
1503
|
"request_id": created.get("request_id"),
|
|
1573
1504
|
"suggested_next_call": None,
|
|
1574
1505
|
"noop": False,
|
|
1575
|
-
"
|
|
1576
|
-
"verified": False,
|
|
1577
|
-
"verification": {
|
|
1578
|
-
"resource_list_complete": resource_list_complete,
|
|
1579
|
-
"business_verified": False,
|
|
1580
|
-
},
|
|
1506
|
+
"verification": {},
|
|
1581
1507
|
"write_executed": True,
|
|
1582
1508
|
"safe_to_retry": False,
|
|
1583
|
-
"resources": resources,
|
|
1584
1509
|
}
|
|
1585
1510
|
|
|
1586
1511
|
def package_list(self, *, profile: str, trial_status: str = "all", query: str = "") -> JSONObject:
|
|
@@ -17223,49 +17148,6 @@ def _coerce_positive_int(value: Any) -> int | None:
|
|
|
17223
17148
|
return None
|
|
17224
17149
|
|
|
17225
17150
|
|
|
17226
|
-
def _unique_positive_ints(value: Any) -> list[int]:
|
|
17227
|
-
if not isinstance(value, list):
|
|
17228
|
-
return []
|
|
17229
|
-
result: list[int] = []
|
|
17230
|
-
for item in value:
|
|
17231
|
-
number = _coerce_positive_int(item)
|
|
17232
|
-
if number is not None and number not in result:
|
|
17233
|
-
result.append(number)
|
|
17234
|
-
return result
|
|
17235
|
-
|
|
17236
|
-
|
|
17237
|
-
def _unique_resource_tag_ids(value: Any) -> tuple[list[int], bool]:
|
|
17238
|
-
if not isinstance(value, list):
|
|
17239
|
-
return [], False
|
|
17240
|
-
result: list[int] = []
|
|
17241
|
-
complete = True
|
|
17242
|
-
for item in value:
|
|
17243
|
-
try:
|
|
17244
|
-
tag_id = validate_positive_resource_id(item, "package_id")
|
|
17245
|
-
except (ValueError, UnicodeError):
|
|
17246
|
-
complete = False
|
|
17247
|
-
continue
|
|
17248
|
-
if tag_id not in result:
|
|
17249
|
-
result.append(tag_id)
|
|
17250
|
-
return result, complete
|
|
17251
|
-
|
|
17252
|
-
|
|
17253
|
-
def _unique_resource_app_keys(value: Any) -> tuple[list[str], bool]:
|
|
17254
|
-
if not isinstance(value, list):
|
|
17255
|
-
return [], False
|
|
17256
|
-
result: list[str] = []
|
|
17257
|
-
complete = True
|
|
17258
|
-
for item in value:
|
|
17259
|
-
try:
|
|
17260
|
-
app_key = validate_resource_identity(item, "app_key")
|
|
17261
|
-
except (ValueError, UnicodeError):
|
|
17262
|
-
complete = False
|
|
17263
|
-
continue
|
|
17264
|
-
if app_key not in result:
|
|
17265
|
-
result.append(app_key)
|
|
17266
|
-
return result, complete
|
|
17267
|
-
|
|
17268
|
-
|
|
17269
17151
|
def _coerce_nonnegative_int(value: Any) -> int | None:
|
|
17270
17152
|
if isinstance(value, bool) or value is None:
|
|
17271
17153
|
return None
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
|
|
5
|
-
from . import app, auth, builder, chart,
|
|
5
|
+
from . import app, auth, builder, chart, exports, imports, portal, record, task, view, workspace
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def register_all_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
@@ -13,7 +13,6 @@ def register_all_commands(subparsers: argparse._SubParsersAction[argparse.Argume
|
|
|
13
13
|
view.register(subparsers)
|
|
14
14
|
chart.register(subparsers)
|
|
15
15
|
record.register(subparsers)
|
|
16
|
-
data.register(subparsers)
|
|
17
16
|
imports.register(subparsers)
|
|
18
17
|
exports.register(subparsers)
|
|
19
18
|
task.register(subparsers)
|
|
@@ -41,7 +41,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
41
41
|
whoami.set_defaults(handler=_handle_whoami, format_hint="auth_whoami")
|
|
42
42
|
|
|
43
43
|
login_link = auth_subparsers.add_parser("login-link", help="生成当前工作区的临时一次性登录链接(不自动打开)")
|
|
44
|
-
login_link.add_argument("--redirect-path", default="", help="
|
|
44
|
+
login_link.add_argument("--redirect-path", default="", help="登录后跳转的站内路径,可包含查询参数和锚点")
|
|
45
45
|
login_link.set_defaults(handler=_handle_login_link, format_hint="login_link")
|
|
46
46
|
|
|
47
47
|
logout = auth_subparsers.add_parser("logout", help="退出登录")
|
|
@@ -10,13 +10,12 @@ from ..session_store import SessionStore
|
|
|
10
10
|
from ..tools.ai_builder_tools import AiBuilderTools
|
|
11
11
|
from ..tools.app_tools import AppTools
|
|
12
12
|
from ..tools.auth_tools import AuthTools
|
|
13
|
+
from ..tools.login_link_tools import LoginLinkTools
|
|
13
14
|
from ..tools.code_block_tools import CodeBlockTools
|
|
14
|
-
from ..tools.data_tools import DataTools
|
|
15
15
|
from ..tools.export_tools import ExportTools
|
|
16
16
|
from ..tools.feedback_tools import FeedbackTools
|
|
17
17
|
from ..tools.file_tools import FileTools
|
|
18
18
|
from ..tools.import_tools import ImportTools
|
|
19
|
-
from ..tools.login_link_tools import LoginLinkTools
|
|
20
19
|
from ..tools.record_tools import RecordTools
|
|
21
20
|
from ..tools.resource_read_tools import ResourceReadTools
|
|
22
21
|
from ..tools.task_context_tools import TaskContextTools
|
|
@@ -34,7 +33,6 @@ class CliContext:
|
|
|
34
33
|
app: AppTools
|
|
35
34
|
resource: ResourceReadTools
|
|
36
35
|
record: RecordTools
|
|
37
|
-
data: DataTools
|
|
38
36
|
code_block: CodeBlockTools
|
|
39
37
|
imports: ImportTools
|
|
40
38
|
exports: ExportTools
|
|
@@ -66,7 +64,6 @@ def build_cli_context() -> CliContext:
|
|
|
66
64
|
app=AppTools(sessions, backend),
|
|
67
65
|
resource=ResourceReadTools(sessions, backend),
|
|
68
66
|
record=RecordTools(sessions, backend),
|
|
69
|
-
data=DataTools(sessions, backend),
|
|
70
67
|
code_block=CodeBlockTools(sessions, backend),
|
|
71
68
|
imports=ImportTools(sessions, backend),
|
|
72
69
|
exports=ExportTools(sessions, backend),
|
|
@@ -191,10 +191,9 @@ def _format_app_items(result: dict[str, Any]) -> str:
|
|
|
191
191
|
str(item.get("app_key") or item.get("appKey") or ""),
|
|
192
192
|
str(item.get("app_name") or item.get("name") or item.get("title") or ""),
|
|
193
193
|
str(item.get("package_name") or item.get("tag_name") or ""),
|
|
194
|
-
str(item.get("uri") or ""),
|
|
195
194
|
]
|
|
196
195
|
)
|
|
197
|
-
return _render_titled_table("Apps", ["app_key", "app_name", "package"
|
|
196
|
+
return _render_titled_table("Apps", ["app_key", "app_name", "package"], rows)
|
|
198
197
|
|
|
199
198
|
|
|
200
199
|
def _format_app_get(result: dict[str, Any]) -> str:
|
|
@@ -202,7 +201,6 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
202
201
|
lines = [
|
|
203
202
|
f"App: {data.get('app_name') or '-'}",
|
|
204
203
|
f"App Key: {data.get('app_key') or '-'}",
|
|
205
|
-
f"URI: {data.get('uri') or '-'}",
|
|
206
204
|
f"Can Create: {data.get('can_create')}",
|
|
207
205
|
]
|
|
208
206
|
import_capability = data.get("import_capability")
|
|
@@ -226,7 +224,7 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
226
224
|
lines.append(f"Accessible Views: {len(views)}")
|
|
227
225
|
for item in views[:10]:
|
|
228
226
|
if isinstance(item, dict):
|
|
229
|
-
lines.append(f"- {item.get('view_id')}: {item.get('name')}
|
|
227
|
+
lines.append(f"- {item.get('view_id')}: {item.get('name')}")
|
|
230
228
|
_append_warnings(lines, result.get("warnings"))
|
|
231
229
|
return "\n".join(lines) + "\n"
|
|
232
230
|
|
|
@@ -298,7 +296,6 @@ def _format_view_get(result: dict[str, Any]) -> str:
|
|
|
298
296
|
f"View Key: {data.get('view_key') or '-'}",
|
|
299
297
|
f"App Key: {data.get('app_key') or '-'}",
|
|
300
298
|
f"View Type: {data.get('view_type') or '-'}",
|
|
301
|
-
f"URI: {data.get('uri') or '-'}",
|
|
302
299
|
f"Analysis Supported: {data.get('analysis_supported')}",
|
|
303
300
|
]
|
|
304
301
|
if export_capability:
|
|
@@ -1187,7 +1184,6 @@ def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
|
1187
1184
|
("view_key", "View Key"),
|
|
1188
1185
|
("chart_id", "Chart ID"),
|
|
1189
1186
|
("package_id", "Package ID"),
|
|
1190
|
-
("uri", "URI"),
|
|
1191
1187
|
("verified", "Verified"),
|
|
1192
1188
|
("write_executed", "Write Executed"),
|
|
1193
1189
|
("delete_executed", "Delete Executed"),
|
|
@@ -1220,7 +1216,6 @@ def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
|
1220
1216
|
"associations",
|
|
1221
1217
|
"package_ids",
|
|
1222
1218
|
"app_keys",
|
|
1223
|
-
"resources",
|
|
1224
1219
|
):
|
|
1225
1220
|
value = result.get(key)
|
|
1226
1221
|
if not isinstance(value, list) and isinstance(data, dict):
|
|
@@ -1231,15 +1226,6 @@ def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
|
1231
1226
|
lines.append("- " + _builder_item_summary(item))
|
|
1232
1227
|
if len(value) > 8:
|
|
1233
1228
|
lines.append(f"... {len(value) - 8} more")
|
|
1234
|
-
if key == "apps":
|
|
1235
|
-
uri_lines = [
|
|
1236
|
-
line
|
|
1237
|
-
for item in value
|
|
1238
|
-
for line in _builder_batch_item_uri_lines(item)
|
|
1239
|
-
]
|
|
1240
|
-
if uri_lines:
|
|
1241
|
-
lines.append("Resource URIs:")
|
|
1242
|
-
lines.extend(f"- {line}" for line in uri_lines)
|
|
1243
1229
|
components = result.get("components") if isinstance(result.get("components"), list) else []
|
|
1244
1230
|
if components:
|
|
1245
1231
|
lines.append("Component Refs:")
|
|
@@ -1308,7 +1294,6 @@ def _builder_item_summary(item: Any) -> str:
|
|
|
1308
1294
|
"uid",
|
|
1309
1295
|
"userId",
|
|
1310
1296
|
"email",
|
|
1311
|
-
"uri",
|
|
1312
1297
|
)
|
|
1313
1298
|
parts = [
|
|
1314
1299
|
f"{key}={item.get(key)}"
|
|
@@ -1320,50 +1305,6 @@ def _builder_item_summary(item: Any) -> str:
|
|
|
1320
1305
|
return " / ".join(parts) if parts else json.dumps(item, ensure_ascii=False)
|
|
1321
1306
|
|
|
1322
1307
|
|
|
1323
|
-
def _builder_batch_item_uri_lines(item: Any) -> list[str]:
|
|
1324
|
-
if not isinstance(item, dict):
|
|
1325
|
-
return []
|
|
1326
|
-
item_status = str(item.get("status") or "").lower()
|
|
1327
|
-
if item_status not in {"success", "partial_success"}:
|
|
1328
|
-
return []
|
|
1329
|
-
data = item.get("data") if isinstance(item.get("data"), dict) else item.get("result")
|
|
1330
|
-
if not isinstance(data, dict) or not _builder_uri_is_confirmed(data, fallback_status=item_status):
|
|
1331
|
-
return []
|
|
1332
|
-
lines: list[str] = []
|
|
1333
|
-
if data.get("uri"):
|
|
1334
|
-
lines.append(f"URI: {data['uri']}")
|
|
1335
|
-
views = data.get("views") if isinstance(data.get("views"), list) else []
|
|
1336
|
-
for view in views:
|
|
1337
|
-
if (
|
|
1338
|
-
isinstance(view, dict)
|
|
1339
|
-
and view.get("uri")
|
|
1340
|
-
and _builder_uri_is_confirmed(view, fallback_status=item_status)
|
|
1341
|
-
):
|
|
1342
|
-
lines.append(f"View URI: {view['uri']}")
|
|
1343
|
-
return lines
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
def _builder_uri_is_confirmed(value: dict[str, Any], *, fallback_status: str) -> bool:
|
|
1347
|
-
status = str(value.get("status") or fallback_status).lower()
|
|
1348
|
-
operation = str(value.get("operation") or "").lower()
|
|
1349
|
-
return (
|
|
1350
|
-
status in {"success", "partial_success", "unchanged", "verified"}
|
|
1351
|
-
and operation
|
|
1352
|
-
not in {
|
|
1353
|
-
"failed",
|
|
1354
|
-
"pending",
|
|
1355
|
-
"planned",
|
|
1356
|
-
"readback_pending",
|
|
1357
|
-
"pending_readback",
|
|
1358
|
-
"unknown",
|
|
1359
|
-
"remove",
|
|
1360
|
-
"removed",
|
|
1361
|
-
"delete",
|
|
1362
|
-
"deleted",
|
|
1363
|
-
}
|
|
1364
|
-
)
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
1308
|
def _format_file_upload_local(result: dict[str, Any]) -> str:
|
|
1368
1309
|
lines = [
|
|
1369
1310
|
f"Upload Kind: {result.get('upload_kind') or result.get('requested_upload_kind') or '-'}",
|
|
@@ -1626,7 +1567,7 @@ def _task_action_debug_lines(result: dict[str, Any]) -> list[str]:
|
|
|
1626
1567
|
|
|
1627
1568
|
|
|
1628
1569
|
def _format_login_link(result: dict[str, Any]) -> str:
|
|
1629
|
-
return f"登录链接:{result['url']}\n过期时间(Unix 毫秒):{result['expires_at']}\n
|
|
1570
|
+
return f"登录链接:{result['url']}\n过期时间(Unix 毫秒):{result['expires_at']}\n链接仅可使用一次,请勿泄露。\n"
|
|
1630
1571
|
|
|
1631
1572
|
|
|
1632
1573
|
_FORMATTERS = {
|
|
@@ -2,13 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
5
|
-
import os
|
|
6
5
|
import sys
|
|
7
6
|
from typing import Any, Callable, TextIO
|
|
8
7
|
|
|
9
8
|
from ..errors import QingflowApiError, backend_code_value_int, message_looks_like_invalid_token
|
|
10
9
|
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
11
|
-
from ..resource_uri import cli_resource_base_url
|
|
12
10
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
13
11
|
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
14
12
|
from ..version import get_cli_version, get_cli_version_info
|
|
@@ -96,8 +94,7 @@ def run(
|
|
|
96
94
|
context = context_factory()
|
|
97
95
|
if not bool(args.json):
|
|
98
96
|
_emit_cli_effective_context_notice(args, context, stream=err)
|
|
99
|
-
|
|
100
|
-
result = handler(args, context)
|
|
97
|
+
result = handler(args, context)
|
|
101
98
|
except SystemExit as exc:
|
|
102
99
|
return int(exc.code or 0)
|
|
103
100
|
except RuntimeError as exc:
|
|
@@ -40,11 +40,6 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
40
40
|
PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
|
|
41
41
|
PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
|
|
42
42
|
PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
|
|
43
|
-
PublicToolSpec(USER_DOMAIN, "data_query", ("data_query",), ("data", "query"), cli_show_effective_context=True),
|
|
44
|
-
PublicToolSpec(USER_DOMAIN, "data_get", ("data_get",), ("data", "get"), cli_show_effective_context=True),
|
|
45
|
-
PublicToolSpec(USER_DOMAIN, "data_insert", ("data_insert",), ("data", "insert"), cli_show_effective_context=True, cli_context_write=True),
|
|
46
|
-
PublicToolSpec(USER_DOMAIN, "data_update", ("data_update",), ("data", "update"), cli_show_effective_context=True, cli_context_write=True),
|
|
47
|
-
PublicToolSpec(USER_DOMAIN, "data_delete", ("data_delete",), ("data", "delete"), cli_show_effective_context=True, cli_context_write=True),
|
|
48
43
|
PublicToolSpec(USER_DOMAIN, "portal_list", ("portal_list",), ("portal", "list"), cli_show_effective_context=True),
|
|
49
44
|
PublicToolSpec(USER_DOMAIN, "portal_get", ("portal_get",), ("portal", "get"), cli_show_effective_context=True),
|
|
50
45
|
PublicToolSpec(USER_DOMAIN, "view_get", ("view_get",), ("view", "get"), cli_show_effective_context=True),
|
|
@@ -133,7 +128,6 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
133
128
|
PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("builder", "auth", "use-credential"), cli_public=False),
|
|
134
129
|
PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
|
|
135
130
|
PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
|
|
136
|
-
PublicToolSpec(BUILDER_DOMAIN, "get_login_link", ("get_login_link",), mcp_public=True, cli_public=False),
|
|
137
131
|
PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), ("builder", "workspace", "list"), cli_public=False),
|
|
138
132
|
PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",), ("builder", "workspace", "get"), cli_public=False),
|
|
139
133
|
PublicToolSpec(BUILDER_DOMAIN, "file_upload_local", ("file_upload_local",), ("builder", "file", "upload-local"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
@@ -326,7 +326,7 @@ def _trim_workspace_get(payload: JSONObject) -> None:
|
|
|
326
326
|
|
|
327
327
|
def _trim_app_list_like(payload: JSONObject) -> None:
|
|
328
328
|
payload.pop("apps", None)
|
|
329
|
-
_trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"
|
|
329
|
+
_trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
|
|
330
330
|
|
|
331
331
|
|
|
332
332
|
def _trim_app_get(payload: JSONObject) -> None:
|
|
@@ -334,7 +334,7 @@ def _trim_app_get(payload: JSONObject) -> None:
|
|
|
334
334
|
_trim_nested_item_list(
|
|
335
335
|
payload,
|
|
336
336
|
("data", "accessible_views"),
|
|
337
|
-
allowed=("view_id", "name", "analysis_supported", "list_supported", "kind"
|
|
337
|
+
allowed=("view_id", "name", "analysis_supported", "list_supported", "kind"),
|
|
338
338
|
)
|
|
339
339
|
|
|
340
340
|
|
|
@@ -1148,6 +1148,7 @@ def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform
|
|
|
1148
1148
|
|
|
1149
1149
|
|
|
1150
1150
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_whoami"), _trim_auth_payload)
|
|
1151
|
+
_register_policy((USER_DOMAIN,), ("get_login_link",), _trim_login_link)
|
|
1151
1152
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
|
|
1152
1153
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
|
|
1153
1154
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get", "workspace_select"), _trim_workspace_get)
|
|
@@ -1222,7 +1223,6 @@ _register_policy(
|
|
|
1222
1223
|
_trim_directory,
|
|
1223
1224
|
)
|
|
1224
1225
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("feedback_submit",), _trim_feedback)
|
|
1225
|
-
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("get_login_link",), _trim_login_link)
|
|
1226
1226
|
_register_policy(
|
|
1227
1227
|
(USER_DOMAIN,),
|
|
1228
1228
|
(
|
|
@@ -7,7 +7,6 @@ from mcp.server.fastmcp import FastMCP
|
|
|
7
7
|
from .backend_client import BackendClient
|
|
8
8
|
from .session_store import SessionStore
|
|
9
9
|
from .tools.app_tools import AppTools
|
|
10
|
-
from .tools.data_tools import DataTools
|
|
11
10
|
from .tools.auth_tools import AuthTools
|
|
12
11
|
from .tools.code_block_tools import CodeBlockTools
|
|
13
12
|
from .tools.feedback_tools import FeedbackTools
|
|
@@ -250,7 +249,6 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
250
249
|
TaskContextTools(sessions, backend).register(server)
|
|
251
250
|
RoleTools(sessions, backend).register(server)
|
|
252
251
|
AppTools(sessions, backend).register(server)
|
|
253
|
-
DataTools(sessions, backend).register(server)
|
|
254
252
|
QingbiReportTools(sessions, backend).register(server)
|
|
255
253
|
PackageTools(sessions, backend).register(server)
|
|
256
254
|
NavigationTools(sessions, backend).register(server)
|
|
@@ -9,7 +9,6 @@ from .config import DEFAULT_PROFILE
|
|
|
9
9
|
from .response_trim import BUILDER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
|
|
10
10
|
from .session_store import SessionStore
|
|
11
11
|
from .tools.ai_builder_tools import AiBuilderTools
|
|
12
|
-
from .tools.login_link_tools import LOGIN_LINK_DESCRIPTION, LoginLinkTools
|
|
13
12
|
from .tools.auth_tools import AuthTools
|
|
14
13
|
from .tools.feedback_tools import FeedbackTools
|
|
15
14
|
from .tools.file_tools import FileTools
|
|
@@ -42,16 +41,6 @@ def build_builder_server() -> FastMCP:
|
|
|
42
41
|
"member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
|
|
43
42
|
"then app_form_apply for complete app form targets, explicit app_delete for application deletion, and app_flow_apply/app_views_apply/app_associated_resources_apply/app_charts_apply/portal_apply/portal_delete for non-form patches. app_custom_buttons_apply is maintenance-only for explicit standalone button work outside normal view design. These apply/delete tools perform validation, normalization, writes, and readback verification where applicable. AppForm publishes only after a draft change or when the published form lags behind; views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal delete separates DELETE execution from readback verification, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
|
|
44
43
|
"Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
|
|
45
|
-
"创建、修改或安装轻流资源后,最终回复主动提供主要结果的可点击页面入口,无需等待用户再次索要。"
|
|
46
|
-
"采用“完成结果 + 查看入口”,文案使用“在轻流中查看应用/应用包/视图”,不默认称为“继续编辑”。"
|
|
47
|
-
"默认只交付用户请求的主要结果:安装方案默认提供应用包入口;直接创建或修改应用、视图时提供对应入口;用户明确要求多个主要对象时分别提供。"
|
|
48
|
-
"使用业务结果中的稳定 `uri`,按 URI 去重,不为全部子资源预签票据;主要对象没有可靠 URI 时说明限制,不猜地址,也不替换为不相关页面。"
|
|
49
|
-
"部分成功时仅交付已确认成功且可定位的主要结果,保留失败或未验证说明。"
|
|
50
|
-
"纯数据问答、中间读取、失败资源和删除操作不自动签发;用户明确要求不提供链接时遵从。用户明确要求查看或打开页面时也使用已确认的路径。"
|
|
51
|
-
"目标浏览器登录态未知时,临近交付用资源 URI 的同源 pathname 调用 get_login_link,为每个实际展示的入口分别签发,使用返回的完整 URL,不额外追问登录态。"
|
|
52
|
-
"只有已有用户确认同一浏览器、网页域名、账号和工作区已登录且无失效信号的事实时,才直接交付稳定 URI;调用过票据工具、自动化浏览器已登录或之前签发过票据均不能证明用户浏览器已登录,已消费链接不可复用。"
|
|
53
|
-
"用户未要求打开时不提前打开或消费链接;浏览器不可用时提供可点击链接,不假称已经打开。"
|
|
54
|
-
"签发失败时说明免登录链接未生成,提供标注“需登录”的稳定 URI 作为备用入口,不重做业务操作。机器结果继续保留稳定 URI,临时登录链接仅用于面向用户交付。 "
|
|
55
44
|
"For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
|
|
56
45
|
"For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
|
|
57
46
|
"For builder write/apply results, separate write execution from final readback verification: status=partial_success with write_executed=true and safe_to_retry=false means the write was sent or succeeded, while final readback, publish verification, or metadata confirmation is pending or permission-restricted. Report it as written but unverified; do not repeat the same write blindly. "
|
|
@@ -75,7 +64,6 @@ def build_builder_server() -> FastMCP:
|
|
|
75
64
|
workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
|
|
76
65
|
files = wrap_trimmed_methods(FileTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
|
|
77
66
|
ai_builder = wrap_trimmed_methods(AiBuilderTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
|
|
78
|
-
login_link = LoginLinkTools(sessions, backend)
|
|
79
67
|
feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
|
|
80
68
|
|
|
81
69
|
@server.tool()
|
|
@@ -102,10 +90,6 @@ def build_builder_server() -> FastMCP:
|
|
|
102
90
|
def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict:
|
|
103
91
|
return auth.auth_logout(profile=profile, forget_persisted=forget_persisted)
|
|
104
92
|
|
|
105
|
-
@server.tool(description=LOGIN_LINK_DESCRIPTION)
|
|
106
|
-
def get_login_link(profile: str = DEFAULT_PROFILE, redirect_path: str = "") -> dict:
|
|
107
|
-
return login_link.get_login_link(profile=profile, redirect_path=redirect_path)
|
|
108
|
-
|
|
109
93
|
@server.tool()
|
|
110
94
|
def workspace_list(
|
|
111
95
|
profile: str = DEFAULT_PROFILE,
|