@qingflow-tech/qingflow-cli 1.1.47-beta.3 → 1.1.47-beta.4
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
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.47-beta.
|
|
10
|
+
npm install @qingflow-tech/qingflow-cli@1.1.47-beta.4
|
|
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.47-beta.
|
|
24
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.47-beta.4 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.47-beta.
|
|
65
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.47-beta.4 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.47-beta.
|
|
3
|
+
"version": "1.1.47-beta.4",
|
|
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
|
@@ -130,6 +130,8 @@ qingflow --json auth whoami
|
|
|
130
130
|
|
|
131
131
|
默认只交付用户请求的主要结果:安装方案默认提供应用包入口;直接创建或修改应用、视图时提供对应入口;用户明确要求多个主要对象时分别提供。使用业务结果中的稳定 `uri`,按 URI 去重,不为全部子资源预签票据。主要对象没有可靠 URI 时说明限制,不猜地址,也不替换为不相关页面。
|
|
132
132
|
|
|
133
|
+
CLI 返回的稳定 `uri` 以 `QINGFLOW_BASE_URL` 为当前环境来源:去除末尾 API 路径后使用同一 HTTP(S) 域名;仅当该变量为空时回退 `https://qingflow.com`。若非空配置非法,保留 URI 不可用诊断,不自行替换或拼接其他环境地址。
|
|
134
|
+
|
|
133
135
|
部分成功时仅交付已确认成功且可定位的主要结果,保留失败或未验证说明。纯数据问答、中间读取、失败资源和删除操作不自动签发;用户明确要求不提供链接时遵从。用户明确要求查看或打开页面时也使用已确认的路径。
|
|
134
136
|
|
|
135
137
|
目标浏览器登录态未知时,临近交付取该 URI 的 pathname 执行:
|
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
from typing import Any, Callable, TextIO
|
|
7
8
|
|
|
8
9
|
from ..errors import QingflowApiError, backend_code_value_int, message_looks_like_invalid_token
|
|
9
10
|
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
11
|
+
from ..resource_uri import cli_resource_base_url
|
|
10
12
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
11
13
|
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
12
14
|
from ..version import get_cli_version, get_cli_version_info
|
|
@@ -94,7 +96,8 @@ def run(
|
|
|
94
96
|
context = context_factory()
|
|
95
97
|
if not bool(args.json):
|
|
96
98
|
_emit_cli_effective_context_notice(args, context, stream=err)
|
|
97
|
-
|
|
99
|
+
with cli_resource_base_url(os.getenv("QINGFLOW_BASE_URL") or ""):
|
|
100
|
+
result = handler(args, context)
|
|
98
101
|
except SystemExit as exc:
|
|
99
102
|
return int(exc.code or 0)
|
|
100
103
|
except RuntimeError as exc:
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
4
7
|
from dataclasses import dataclass
|
|
5
8
|
from typing import Any
|
|
6
9
|
from urllib.parse import quote, unquote, urlsplit, urlunsplit
|
|
7
10
|
|
|
8
|
-
from .config import get_web_base_url
|
|
11
|
+
from .config import DEFAULT_WEB_BASE_URL, get_web_base_url
|
|
9
12
|
from .errors import QingflowApiError
|
|
10
13
|
from .json_types import JSONObject
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
SYSTEM_VIEW_NAMES = frozenset({"all", "initiated", "todo", "done", "cc"})
|
|
14
17
|
_ENCODED_OCTET = re.compile(r"%[0-9A-Fa-f]{2}")
|
|
18
|
+
_CLI_RESOURCE_BASE_URL: ContextVar[str | None] = ContextVar(
|
|
19
|
+
"qingflow_cli_resource_base_url",
|
|
20
|
+
default=None,
|
|
21
|
+
)
|
|
15
22
|
|
|
16
23
|
|
|
17
24
|
@dataclass(frozen=True, slots=True)
|
|
@@ -24,6 +31,16 @@ class ResourcePath:
|
|
|
24
31
|
view_key: str | None = None
|
|
25
32
|
|
|
26
33
|
|
|
34
|
+
@contextmanager
|
|
35
|
+
def cli_resource_base_url(value: str) -> Iterator[None]:
|
|
36
|
+
"""让一次 CLI 调用的资源 URI 使用 QINGFLOW_BASE_URL 对应的网页环境。"""
|
|
37
|
+
token = _CLI_RESOURCE_BASE_URL.set(value)
|
|
38
|
+
try:
|
|
39
|
+
yield
|
|
40
|
+
finally:
|
|
41
|
+
_CLI_RESOURCE_BASE_URL.reset(token)
|
|
42
|
+
|
|
43
|
+
|
|
27
44
|
def validate_resource_identity(value: object, field: str = "resource_id") -> str:
|
|
28
45
|
"""按资源 URI 单路径段合同校验并返回规范身份,不读取网页域名配置。"""
|
|
29
46
|
return _validate_identity(value, field)
|
|
@@ -64,7 +81,7 @@ def build_resource_uri(
|
|
|
64
81
|
path = f"/resources/ws/{workspace}/app/{app_segment}/view/{view_kind}/{view_segment}"
|
|
65
82
|
else:
|
|
66
83
|
raise ValueError("resource_type is not supported")
|
|
67
|
-
return f"{
|
|
84
|
+
return f"{_resolve_web_origin(web_origin)}{path}"
|
|
68
85
|
|
|
69
86
|
|
|
70
87
|
def parse_resource_path(value: str) -> ResourcePath:
|
|
@@ -142,6 +159,45 @@ def _normalize_web_origin(value: str) -> str:
|
|
|
142
159
|
return urlunsplit((parsed.scheme, parsed.netloc, "", "", ""))
|
|
143
160
|
|
|
144
161
|
|
|
162
|
+
def _resolve_web_origin(web_origin: str | None) -> str:
|
|
163
|
+
if web_origin is not None:
|
|
164
|
+
return _normalize_web_origin(web_origin)
|
|
165
|
+
cli_base_url = _CLI_RESOURCE_BASE_URL.get()
|
|
166
|
+
if cli_base_url is None:
|
|
167
|
+
return get_web_base_url()
|
|
168
|
+
return _web_origin_from_api_base_url(cli_base_url)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _web_origin_from_api_base_url(value: str) -> str:
|
|
172
|
+
normalized = value.strip()
|
|
173
|
+
if not normalized:
|
|
174
|
+
return DEFAULT_WEB_BASE_URL
|
|
175
|
+
try:
|
|
176
|
+
parsed = urlsplit(normalized)
|
|
177
|
+
host = parsed.hostname or ""
|
|
178
|
+
valid_host = bool(host) and (":" in host or all(
|
|
179
|
+
re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?", label)
|
|
180
|
+
for label in host.rstrip(".").split(".")
|
|
181
|
+
))
|
|
182
|
+
if (
|
|
183
|
+
parsed.scheme not in {"http", "https"}
|
|
184
|
+
or not valid_host
|
|
185
|
+
or host.lower() in {"null", "undefined"}
|
|
186
|
+
or parsed.username is not None
|
|
187
|
+
or parsed.password is not None
|
|
188
|
+
or parsed.query
|
|
189
|
+
or parsed.fragment
|
|
190
|
+
or any(character.isspace() or ord(character) < 32 for character in normalized)
|
|
191
|
+
):
|
|
192
|
+
raise ValueError
|
|
193
|
+
_ = parsed.port
|
|
194
|
+
return _normalize_web_origin(urlunsplit((parsed.scheme, parsed.netloc, "", "", "")))
|
|
195
|
+
except ValueError:
|
|
196
|
+
raise QingflowApiError.config_error(
|
|
197
|
+
"QINGFLOW_BASE_URL 必须是有效的 HTTP(S) API 地址,不包含查询参数、片段或用户凭证"
|
|
198
|
+
) from None
|
|
199
|
+
|
|
200
|
+
|
|
145
201
|
def _identity_segment(value: object, field: str) -> str:
|
|
146
202
|
raw = _validate_identity(value, field)
|
|
147
203
|
return quote(raw, safe="")
|