@qingflow-tech/qingflow-cli 1.1.47 → 1.1.48-beta.1
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/npm/lib/runtime.mjs +29 -13
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +37 -1
- package/skills/qingflow-common/SKILL.md +7 -1
- package/src/qingflow_mcp/builder_facade/service.py +119 -1
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/auth.py +8 -0
- package/src/qingflow_mcp/cli/commands/data.py +134 -0
- package/src/qingflow_mcp/cli/context.py +6 -0
- package/src/qingflow_mcp/cli/formatters.py +163 -3
- package/src/qingflow_mcp/cli/main.py +4 -1
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/public_surface.py +7 -0
- package/src/qingflow_mcp/resource_uri.py +241 -0
- package/src/qingflow_mcp/response_trim.py +10 -2
- package/src/qingflow_mcp/server.py +4 -0
- package/src/qingflow_mcp/server_app_builder.py +16 -0
- package/src/qingflow_mcp/server_app_user.py +24 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +208 -39
- package/src/qingflow_mcp/tools/app_tools.py +52 -13
- package/src/qingflow_mcp/tools/csv_export.py +168 -0
- package/src/qingflow_mcp/tools/data_tools.py +271 -0
- package/src/qingflow_mcp/tools/login_link_tools.py +142 -0
- package/src/qingflow_mcp/tools/record_tools.py +11 -46
- package/src/qingflow_mcp/tools/resource_read_tools.py +13 -0
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-beta.1
|
|
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-beta.1 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-beta.1 qingflow-skills install --agent codex --scope user --copy
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
`qingflow-skills install` 默认使用 symlink,且遇到同名 skill 不会覆盖;只有显式传入 `--force` 才会替换。
|
package/npm/lib/runtime.mjs
CHANGED
|
@@ -569,6 +569,34 @@ function findOfflineProjectWheel(findLinksDir) {
|
|
|
569
569
|
return path.join(findLinksDir, wheels[wheels.length - 1]);
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
+
function hasEnv(env, name) {
|
|
573
|
+
return Object.prototype.hasOwnProperty.call(env, name);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export function buildPipInstallArgs(env = process.env) {
|
|
577
|
+
const standardNoIndexConfigured = hasEnv(env, "PIP_NO_INDEX");
|
|
578
|
+
const standardFindLinksConfigured = hasEnv(env, "PIP_FIND_LINKS");
|
|
579
|
+
|
|
580
|
+
const noIndex = (standardNoIndexConfigured ? env.PIP_NO_INDEX : env.QINGFLOW_MCP_PIP_NO_INDEX) === "1";
|
|
581
|
+
const findLinks = String(
|
|
582
|
+
standardFindLinksConfigured ? env.PIP_FIND_LINKS ?? "" : env.QINGFLOW_MCP_PIP_FIND_LINKS ?? "",
|
|
583
|
+
).trim();
|
|
584
|
+
|
|
585
|
+
const pipArgs = ["install", "--disable-pip-version-check"];
|
|
586
|
+
if (noIndex) {
|
|
587
|
+
pipArgs.push("--no-index");
|
|
588
|
+
}
|
|
589
|
+
if (findLinks) {
|
|
590
|
+
pipArgs.push("--find-links", findLinks);
|
|
591
|
+
}
|
|
592
|
+
if (noIndex && findLinks) {
|
|
593
|
+
pipArgs.push(findOfflineProjectWheel(findLinks));
|
|
594
|
+
} else {
|
|
595
|
+
pipArgs.push(".");
|
|
596
|
+
}
|
|
597
|
+
return pipArgs;
|
|
598
|
+
}
|
|
599
|
+
|
|
572
600
|
export function findPython() {
|
|
573
601
|
const preferred = process.env.QINGFLOW_MCP_PYTHON?.trim();
|
|
574
602
|
const candidates = preferred
|
|
@@ -629,19 +657,7 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
|
|
|
629
657
|
}
|
|
630
658
|
|
|
631
659
|
const pip = getVenvPip(packageRoot);
|
|
632
|
-
const pipArgs =
|
|
633
|
-
const offlineFindLinks = process.env.QINGFLOW_MCP_PIP_FIND_LINKS?.trim();
|
|
634
|
-
if (process.env.QINGFLOW_MCP_PIP_NO_INDEX === "1") {
|
|
635
|
-
pipArgs.push("--no-index");
|
|
636
|
-
}
|
|
637
|
-
if (offlineFindLinks) {
|
|
638
|
-
pipArgs.push("--find-links", offlineFindLinks);
|
|
639
|
-
}
|
|
640
|
-
if (process.env.QINGFLOW_MCP_PIP_NO_INDEX === "1" && offlineFindLinks) {
|
|
641
|
-
pipArgs.push(findOfflineProjectWheel(offlineFindLinks));
|
|
642
|
-
} else {
|
|
643
|
-
pipArgs.push(".");
|
|
644
|
-
}
|
|
660
|
+
const pipArgs = buildPipInstallArgs();
|
|
645
661
|
runChecked(pip, pipArgs, { cwd: packageRoot });
|
|
646
662
|
|
|
647
663
|
const probe = probePythonRuntime(packageRoot, commandName);
|
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-beta.1",
|
|
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,6 +124,27 @@ 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
|
+
|
|
127
148
|
常用参数:`--credential-stdin`、`--password-stdin`、`--persist` / `--forget-persisted`;详见 `qingflow auth --help`。
|
|
128
149
|
|
|
129
150
|
---
|
|
@@ -439,7 +460,7 @@ qingflow auth logout --forget-persisted
|
|
|
439
460
|
|
|
440
461
|
| 分组 | 骨架(细则见上文章节 + 「关联文件」) |
|
|
441
462
|
| ---- | ------------------------------------ |
|
|
442
|
-
| **auth** | `auth whoami` · `auth use-credential --base-url "$QINGFLOW_BASE_URL" --credential-stdin --persist` · `auth login …`(见「失败恢复时的认证方式」)· `auth logout --forget-persisted` |
|
|
463
|
+
| **auth** | `auth whoami` · `auth use-credential --base-url "$QINGFLOW_BASE_URL" --credential-stdin --persist` · `auth login …`(见「失败恢复时的认证方式」)· `auth login-link [--redirect-path PATH]`(临时网页登录链接)· `auth logout --forget-persisted` |
|
|
443
464
|
| **workspace** | `workspace list` · `workspace get --ws-id` · `workspace select`(慎用,可读数前不必;可能 **59004**) |
|
|
444
465
|
| **app / portal / view / chart** | `app list [--query 关键词]` → **`app get --app-key`**(`accessible_views` / `view_id`、`import_capability`)· `builder package list [--query 关键词]` → `builder package get --package-id` · `portal list` · `portal get --dash-key` · `view get --view-id`(只读视图元信息;`baseInfo` 或 `viewConfig` 只有 40002 / 40027 / 404 这类权限/缺失边界可用另一侧降级,非权限类后端错误不隐藏;解析 app 仅依赖同源 view/form 与可见应用树,不回退旧应用搜索)· `chart get --chart-id`(图表 `baseInfo` / 数据 / 配置分阶段读取;单个辅助阶段 40002 / 40027 / 404 只作为 warning,非权限类后端错误不隐藏) |
|
|
445
466
|
| **record** | `record schema insert`(新建记录 insert-ready schema,仅 `--app-key`)· **`browse`/`list`/`access`:`--app-key` + `--view-id`(缺一常 CONFIG)** · `record list --query ... [--query-field FIELD_ID] [--page-size N]` 用于候选定位/样本浏览 · `record get --record-id` 读详情页首屏上下文 · `record logs --record-id` 读完整数据/流程日志 JSONL · `record insert --items-file`(单条也是 items 一行)· `schema update --record-id [--view-id]` / **`update`**(单条 `--fields-file` **或** 批量 `--items-file`/`--dry-run`,二者勿混)· `record delete --app-key --record-id --view-id system:*` / `--record-ids-file` · `export start/direct --view-id` 仅用于明确导出文件 · **大 schema**:先 **[FIELD_DATA_TYPES](./reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md)** / **[RECORD_CREATE](./reference/record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md)** / **[RECORD_UPDATE](./reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md)** / **[RECORD_DELETE](./reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md)**,再 `-h` |
|
|
@@ -447,6 +468,21 @@ qingflow auth logout --forget-persisted
|
|
|
447
468
|
| **builder** | **搭建/系统化配置**:先按 **[INDEX](./reference/00-INDEX.md)** / **[BUILDER](./reference/builder/README.md)** 选主路径,再读 `builder contract`。**完整系统/应用包**:`builder package apply --config-file` → 每个应用依次执行 `builder app-form schema`、更新时 `get --app-key APP_KEY --being-draft`、`validate --schema-version VERSION --file DECLARATION.json`、`apply --file DECLARATION.json`(固定版本、完整声明、记录返回的 `appKey`)→ 视图(按钮随视图 `action_buttons` 写)→ 门户(缺报表时写 `chart`)→ 发布/回读。旧 `schema apply` / `layout apply` 不在当前 CLI parser 中。超时、`partial_success`、`write_executed=true`、`safe_to_retry=false` 或回读不完整时先读取 AppForm draft/published,再恢复,不得重复创建或创建 V2/测试/随机后缀绕过重名。**单应用**:新建和更新遵循同一 AppForm CLI 链路,再按需求配置流程/视图。更新动作归入对应资源文档:AppForm [30](./reference/builder/30-schema-fields.md)、视图 [50](./reference/builder/50-views.md)、报表 [60](./reference/builder/60-charts.md)、门户 [70](./reference/builder/70-portal.md)、关联资源/非主链路按钮维护 [80](./reference/builder/80-buttons-associated-resources.md)、流程 [90](./reference/builder/90-workflow.md)、发布 [99](./reference/builder/99-publish-verify.md)。**AI 公式/代码块校验**:`builder app-form validate-formula --app-key KEY --formula '<公式>' --test-case <文件路径>`(`--formula` 内联;`--test-case` **只收文件路径或 `-` stdin,不收内联 JSON**)与 `builder app-form validate-code-block --app-key KEY --code <内联文本|@文件路径> --test-case <文件路径>`(本地静态 + 后端真跑,输出 `staticErrors` / `runResult` / `runError`);两者是公式/代码块生成场景的真实校验命令,校验结论必须来自其真实执行,不得以本地模拟替代。代码内占位符 `qf_field.{字段名$$queId$$}`(**字段名不带引号**,带引号会被 CLI 自动剥离但勿依赖)与 `keyQueId` 均使用**十进制** queId(CLI 真跑前自动编码为运行时形态;错误回显自动还原为十进制,勿逆向编码 id)。test-case values 按类型:文本/电话=字符串、数字=数字字面量、日期=yyyy-MM-dd、附件=URL 字符串数组、单选/下拉/多选=选项文案(CLI 自动译为选项 id)、成员/部门=不可提供(代码判空,覆盖校验豁免)。 |
|
|
448
469
|
| **task**(须落盘见上文) | `task list [--task-box][--flow-status][--app-key][--query] --page --page-size`(**枚举、`--query`、与 `record` 视图口径勿混**:见上文 SOP · **[TASK_CONTEXT](./reference/task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md)**)· **`get`/`log`/`report`/`action`**:默认只传 `task list` 返回的 **`data.items[].task_id`**;`report` 另须必选 **`--report-id`**;`action` **无 `--dry-run`** |
|
|
449
470
|
|
|
471
|
+
### 临时网页登录链接
|
|
472
|
+
|
|
473
|
+
按上文“交付网页资源链接”规则,在创建、修改或安装完成后主动为主要结果提供查看入口;用户明确要求进入页面时也适用,纯数据问答不自动调用:
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
qingflow --json auth login-link
|
|
477
|
+
qingflow --profile default --json auth login-link --redirect-path "<主要结果 uri 的完整 pathname>"
|
|
478
|
+
# 自定义视图 pathname 示例:/resources/ws/<WS_ID>/app/<APP_KEY>/view/custom/<VIEW_KEY>
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
- 默认使用当前会话和已选工作区;缺失时按实际错误恢复,不额外预查或重新登录。pathname 必须保留主要对象的完整层级,视图任务不得改用应用 pathname。
|
|
482
|
+
- `--redirect-path` 可选,仅接受已确认的站内业务路径,可包含 query/hash;不接受外域或认证入口。示例 ID 仅作格式展示,不得照抄或编造实际目标;省略时进入工作区首页。
|
|
483
|
+
- 将返回的 `url` 作为可点击链接交付,不修改或拼造。链接短时有效且仅可使用一次,失效后重新获取,不写入公共文档或日志。
|
|
484
|
+
- 命令不会自动打开浏览器;用户未要求打开时不提前打开或消费链接,明确要求打开时才调用浏览器。
|
|
485
|
+
|
|
450
486
|
**高发必败**:旧应用搜索入口 · **`record list`/`browse`/`access` 缺 `--view-id`** · **`task report` 缺 `--report-id`** · **`record update` 批量与单条参数混用**。
|
|
451
487
|
|
|
452
488
|
更多成员策略:**[reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md](./reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md)** · 一次性长模板:**[reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md](./reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md)** · 对齐说明:**[reference/QINGFLOW_CLI_EXPLORATION_REPORT.md](./reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md)**。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qingflow-common
|
|
3
|
-
description:
|
|
3
|
+
description: Explain shared Qingflow concepts, choose the CLI or MCP surface, and route resource work or page-link delivery to the relevant domain Skill.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Qingflow Common Concepts
|
|
@@ -31,6 +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
|
+
| Open a workspace or confirmed business page | `qingflow --json auth login-link [--redirect-path PATH]` | `get_login_link` |
|
|
34
35
|
| Package, view, portal, chart, workflow | `qingflow --json builder ...` | Qingflow MCP configuration tools |
|
|
35
36
|
|
|
36
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.
|
|
@@ -42,6 +43,11 @@ The operation names and payload schemas are defined by the public surface. Use t
|
|
|
42
43
|
|
|
43
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/`.
|
|
44
45
|
|
|
46
|
+
## Deliver Qingflow Page Links
|
|
47
|
+
|
|
48
|
+
- After creating, changing, or installing resources, finish with the result and a clickable entry for each requested primary result. Use confirmed stable `uri` values, deduplicate them, and link only confirmed successes. Do not auto-issue links for data-only answers, intermediate reads, failed or deleted resources, or when the user declines links.
|
|
49
|
+
- When browser login is unknown, issue one fresh link per displayed destination near delivery through the installed surface. Never guess, replace, open, or consume a destination unless authorized. If issuance fails, keep the business result and offer the stable URI as requiring login when available. Follow the CLI Skill or MCP tool description for signing details.
|
|
50
|
+
|
|
45
51
|
## Route Next
|
|
46
52
|
|
|
47
53
|
Use the installed package's relevant domain Skill. In the unified MCP package, select the Skill by the requested work, not by an assumed user or builder server role. Do not link to or invoke a domain Skill that the current package did not install.
|
|
@@ -19,6 +19,7 @@ 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
|
|
22
23
|
from ..solution.build_assembly_store import BuildAssemblyStore, default_artifacts, default_manifest
|
|
23
24
|
from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
|
|
24
25
|
from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
|
|
@@ -1491,6 +1492,74 @@ class AiBuilderFacade:
|
|
|
1491
1492
|
details={"solution_key": requested_solution_key},
|
|
1492
1493
|
suggested_next_call={"tool_name": "solution_install", "arguments": {"profile": profile, **normalized_args}},
|
|
1493
1494
|
)
|
|
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
|
+
)
|
|
1494
1563
|
return {
|
|
1495
1564
|
"status": "success",
|
|
1496
1565
|
"error_code": None,
|
|
@@ -1503,9 +1572,15 @@ class AiBuilderFacade:
|
|
|
1503
1572
|
"request_id": created.get("request_id"),
|
|
1504
1573
|
"suggested_next_call": None,
|
|
1505
1574
|
"noop": False,
|
|
1506
|
-
"
|
|
1575
|
+
"warnings": warnings,
|
|
1576
|
+
"verified": False,
|
|
1577
|
+
"verification": {
|
|
1578
|
+
"resource_list_complete": resource_list_complete,
|
|
1579
|
+
"business_verified": False,
|
|
1580
|
+
},
|
|
1507
1581
|
"write_executed": True,
|
|
1508
1582
|
"safe_to_retry": False,
|
|
1583
|
+
"resources": resources,
|
|
1509
1584
|
}
|
|
1510
1585
|
|
|
1511
1586
|
def package_list(self, *, profile: str, trial_status: str = "all", query: str = "") -> JSONObject:
|
|
@@ -17148,6 +17223,49 @@ def _coerce_positive_int(value: Any) -> int | None:
|
|
|
17148
17223
|
return None
|
|
17149
17224
|
|
|
17150
17225
|
|
|
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
|
+
|
|
17151
17269
|
def _coerce_nonnegative_int(value: Any) -> int | None:
|
|
17152
17270
|
if isinstance(value, bool) or value is None:
|
|
17153
17271
|
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, exports, imports, portal, record, task, view, workspace
|
|
5
|
+
from . import app, auth, builder, chart, data, exports, imports, portal, record, task, view, workspace
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def register_all_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
@@ -13,6 +13,7 @@ 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)
|
|
16
17
|
imports.register(subparsers)
|
|
17
18
|
exports.register(subparsers)
|
|
18
19
|
task.register(subparsers)
|
|
@@ -40,11 +40,19 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
40
40
|
whoami = auth_subparsers.add_parser("whoami", help="查看当前登录态")
|
|
41
41
|
whoami.set_defaults(handler=_handle_whoami, format_hint="auth_whoami")
|
|
42
42
|
|
|
43
|
+
login_link = auth_subparsers.add_parser("login-link", help="生成当前工作区的临时一次性登录链接(不自动打开)")
|
|
44
|
+
login_link.add_argument("--redirect-path", default="", help="登录后跳转的站内路径;资源路径必须属于当前工作区,其他业务路径可包含查询参数和锚点")
|
|
45
|
+
login_link.set_defaults(handler=_handle_login_link, format_hint="login_link")
|
|
46
|
+
|
|
43
47
|
logout = auth_subparsers.add_parser("logout", help="退出登录")
|
|
44
48
|
logout.add_argument("--forget-persisted", action="store_true")
|
|
45
49
|
logout.set_defaults(handler=_handle_logout, format_hint="")
|
|
46
50
|
|
|
47
51
|
|
|
52
|
+
def _handle_login_link(args: argparse.Namespace, context: CliContext) -> dict:
|
|
53
|
+
return context.login_link.get_login_link(profile=args.profile, redirect_path=args.redirect_path)
|
|
54
|
+
|
|
55
|
+
|
|
48
56
|
def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
|
|
49
57
|
if args.method == "oauth":
|
|
50
58
|
return _handle_oauth_login(args, context)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import textwrap
|
|
5
|
+
|
|
6
|
+
from ..context import CliContext
|
|
7
|
+
from .common import load_list_arg, require_list_arg, require_object_arg, raise_config_error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
11
|
+
parser = subparsers.add_parser(
|
|
12
|
+
"data",
|
|
13
|
+
help="视图无关的应用数据面",
|
|
14
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
15
|
+
epilog=textwrap.dedent("""
|
|
16
|
+
JSON 文件约定:
|
|
17
|
+
columns.json [101, 102]
|
|
18
|
+
where.json [{"field_id": 101, "op": "eq", "value": "A"}]
|
|
19
|
+
sort.json [{"field_id": 101, "direction": "asc"}]
|
|
20
|
+
items.json [{"fields": {"101": "A"}}]
|
|
21
|
+
fields.json {"101": "B"}
|
|
22
|
+
query/get/insert/update 默认返回 field_capabilities;可用 --no-schema 关闭。
|
|
23
|
+
""").strip(),
|
|
24
|
+
)
|
|
25
|
+
commands = parser.add_subparsers(dest="data_command", required=True, metavar="{query,get,insert,update,delete}")
|
|
26
|
+
|
|
27
|
+
query = commands.add_parser(
|
|
28
|
+
"query", help="分页查询应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
29
|
+
epilog='最小示例:qingflow data query --app-key APP_1 --where-file where.json\n'
|
|
30
|
+
'where.json: [{"field_id": 101, "op": "eq", "value": "A"}]',
|
|
31
|
+
)
|
|
32
|
+
query.add_argument("--app-key", required=True)
|
|
33
|
+
query.add_argument("--columns-file", help='JSON 数组,例如 [101, 102]')
|
|
34
|
+
query.add_argument(
|
|
35
|
+
"--where-file",
|
|
36
|
+
help='JSON 数组;项为 {"field_id":101,"op":"eq","value":"A"},in/not_in 的 value 必须为数组',
|
|
37
|
+
)
|
|
38
|
+
query.add_argument("--sort-file", help='JSON 数组,例如 [{"field_id":101,"direction":"asc"}]')
|
|
39
|
+
query.add_argument("--search", default="")
|
|
40
|
+
query.add_argument("--page", type=int, default=1)
|
|
41
|
+
query.add_argument("--page-size", type=int, default=100)
|
|
42
|
+
query.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
43
|
+
query.add_argument("--all", action="store_true", dest="all_pages")
|
|
44
|
+
query.add_argument("--output", choices=("json", "csv"), default="json")
|
|
45
|
+
query.set_defaults(handler=_handle_query, format_hint="data_query")
|
|
46
|
+
|
|
47
|
+
get = commands.add_parser("get", help="读取单条应用数据")
|
|
48
|
+
get.add_argument("--app-key", required=True)
|
|
49
|
+
get.add_argument("--record-id", required=True)
|
|
50
|
+
get.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
51
|
+
get.set_defaults(handler=_handle_get, format_hint="data_get")
|
|
52
|
+
|
|
53
|
+
insert = commands.add_parser(
|
|
54
|
+
"insert", help="新增应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
55
|
+
epilog='最小示例:items.json = [{"fields": {"101": "A"}}]',
|
|
56
|
+
)
|
|
57
|
+
insert.add_argument("--app-key", required=True)
|
|
58
|
+
insert.add_argument("--items-file", required=True, help='JSON 数组,项为 {"fields":{"字段ID":值}}')
|
|
59
|
+
insert.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
60
|
+
insert.set_defaults(handler=_handle_insert, format_hint="data_insert")
|
|
61
|
+
|
|
62
|
+
update = commands.add_parser(
|
|
63
|
+
"update", help="更新应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
64
|
+
epilog='最小示例:fields.json = {"101": "B"}',
|
|
65
|
+
)
|
|
66
|
+
update.add_argument("--app-key", required=True)
|
|
67
|
+
update.add_argument("--record-id", required=True)
|
|
68
|
+
update.add_argument("--fields-file", required=True, help='JSON 对象,键为字符串形式 field_id')
|
|
69
|
+
update.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
70
|
+
update.set_defaults(handler=_handle_update, format_hint="data_update")
|
|
71
|
+
|
|
72
|
+
delete = commands.add_parser("delete", help="删除应用数据")
|
|
73
|
+
delete.add_argument("--app-key", required=True)
|
|
74
|
+
delete.add_argument(
|
|
75
|
+
"--record-ids", action="append", default=[], metavar="ID[,ID...]",
|
|
76
|
+
help="待删除记录 ID;支持逗号分隔,也可重复传入",
|
|
77
|
+
)
|
|
78
|
+
delete.add_argument("--record-ids-file", help="附加的记录 ID JSON 数组文件")
|
|
79
|
+
delete.set_defaults(handler=_handle_delete, format_hint="data_delete")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _handle_query(args: argparse.Namespace, context: CliContext) -> dict:
|
|
83
|
+
if args.all_pages != (args.output == "csv"):
|
|
84
|
+
raise_config_error(
|
|
85
|
+
"--all 与 --output csv 必须同时使用。",
|
|
86
|
+
fix_hint="使用 `qingflow data query ... --all --output csv`,普通分页查询不要传这两个参数。",
|
|
87
|
+
)
|
|
88
|
+
return context.data.data_query(
|
|
89
|
+
profile=args.profile,
|
|
90
|
+
app_key=args.app_key,
|
|
91
|
+
columns=load_list_arg(args.columns_file, option_name="--columns-file"),
|
|
92
|
+
where=load_list_arg(args.where_file, option_name="--where-file"),
|
|
93
|
+
sort=load_list_arg(args.sort_file, option_name="--sort-file"),
|
|
94
|
+
search=args.search,
|
|
95
|
+
page=args.page,
|
|
96
|
+
page_size=args.page_size,
|
|
97
|
+
include_schema=args.include_schema,
|
|
98
|
+
all_pages=args.all_pages,
|
|
99
|
+
output=args.output,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
104
|
+
return context.data.data_get(
|
|
105
|
+
profile=args.profile, app_key=args.app_key, record_id=args.record_id, include_schema=args.include_schema
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
|
|
110
|
+
return context.data.data_insert(
|
|
111
|
+
profile=args.profile, app_key=args.app_key,
|
|
112
|
+
items=require_list_arg(args.items_file, option_name="--items-file"), include_schema=args.include_schema,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
|
|
117
|
+
return context.data.data_update(
|
|
118
|
+
profile=args.profile, app_key=args.app_key, record_id=args.record_id,
|
|
119
|
+
fields=require_object_arg(args.fields_file, option_name="--fields-file"), include_schema=args.include_schema,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _handle_delete(args: argparse.Namespace, context: CliContext) -> dict:
|
|
124
|
+
record_ids = [item.strip() for group in args.record_ids for item in group.split(",") if item.strip()]
|
|
125
|
+
record_ids.extend(load_list_arg(args.record_ids_file, option_name="--record-ids-file"))
|
|
126
|
+
if not record_ids:
|
|
127
|
+
raise_config_error(
|
|
128
|
+
"至少传入一个记录 ID。",
|
|
129
|
+
fix_hint="使用 `--record-ids ID[,ID...]`;也可附加 `--record-ids-file ids.json`。",
|
|
130
|
+
)
|
|
131
|
+
return context.data.data_delete(
|
|
132
|
+
profile=args.profile, app_key=args.app_key,
|
|
133
|
+
record_ids=record_ids,
|
|
134
|
+
)
|
|
@@ -11,10 +11,12 @@ from ..tools.ai_builder_tools import AiBuilderTools
|
|
|
11
11
|
from ..tools.app_tools import AppTools
|
|
12
12
|
from ..tools.auth_tools import AuthTools
|
|
13
13
|
from ..tools.code_block_tools import CodeBlockTools
|
|
14
|
+
from ..tools.data_tools import DataTools
|
|
14
15
|
from ..tools.export_tools import ExportTools
|
|
15
16
|
from ..tools.feedback_tools import FeedbackTools
|
|
16
17
|
from ..tools.file_tools import FileTools
|
|
17
18
|
from ..tools.import_tools import ImportTools
|
|
19
|
+
from ..tools.login_link_tools import LoginLinkTools
|
|
18
20
|
from ..tools.record_tools import RecordTools
|
|
19
21
|
from ..tools.resource_read_tools import ResourceReadTools
|
|
20
22
|
from ..tools.task_context_tools import TaskContextTools
|
|
@@ -27,10 +29,12 @@ class CliContext:
|
|
|
27
29
|
sessions: CliSessionFacade
|
|
28
30
|
backend: BackendClient
|
|
29
31
|
auth: AuthTools
|
|
32
|
+
login_link: LoginLinkTools
|
|
30
33
|
workspace: WorkspaceTools
|
|
31
34
|
app: AppTools
|
|
32
35
|
resource: ResourceReadTools
|
|
33
36
|
record: RecordTools
|
|
37
|
+
data: DataTools
|
|
34
38
|
code_block: CodeBlockTools
|
|
35
39
|
imports: ImportTools
|
|
36
40
|
exports: ExportTools
|
|
@@ -57,10 +61,12 @@ def build_cli_context() -> CliContext:
|
|
|
57
61
|
sessions=sessions,
|
|
58
62
|
backend=backend,
|
|
59
63
|
auth=AuthTools(sessions, backend),
|
|
64
|
+
login_link=LoginLinkTools(sessions, backend),
|
|
60
65
|
workspace=WorkspaceTools(sessions, backend),
|
|
61
66
|
app=AppTools(sessions, backend),
|
|
62
67
|
resource=ResourceReadTools(sessions, backend),
|
|
63
68
|
record=RecordTools(sessions, backend),
|
|
69
|
+
data=DataTools(sessions, backend),
|
|
64
70
|
code_block=CodeBlockTools(sessions, backend),
|
|
65
71
|
imports=ImportTools(sessions, backend),
|
|
66
72
|
exports=ExportTools(sessions, backend),
|