@qingflow-tech/qingflow-app-user-mcp 1.0.43 → 1.1.0
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/npm/bin/qingflow-app-user-mcp.mjs +31 -2
- package/npm/lib/runtime.mjs +43 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +1 -1
- package/skills/qingflow-record-analysis/SKILL.md +1 -1
- package/skills/qingflow-record-delete/SKILL.md +1 -1
- package/skills/qingflow-record-import/SKILL.md +1 -1
- package/skills/qingflow-record-insert/SKILL.md +5 -2
- package/skills/qingflow-record-update/SKILL.md +1 -1
- package/skills/qingflow-task-ops/SKILL.md +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +0 -39
- package/src/qingflow_mcp/builder_facade/service.py +532 -859
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
- package/src/qingflow_mcp/cli/commands/builder.py +44 -12
- package/src/qingflow_mcp/cli/main.py +4 -6
- package/src/qingflow_mcp/public_surface.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +16 -8
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/executor.py +3 -133
- package/src/qingflow_mcp/tools/ai_builder_tools.py +177 -241
- package/src/qingflow_mcp/tools/solution_tools.py +30 -2
- package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
- package/src/qingflow_mcp/version.py +71 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-user-mcp@1.0
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-user-mcp@1.1.0
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.1.0 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
const PKG_BY_BIN = {
|
|
7
|
+
qingflow: "qingflow-cli",
|
|
8
|
+
"qingflow-app-user-mcp": "qingflow-app-user-mcp",
|
|
9
|
+
"qingflow-app-builder-mcp": "qingflow-app-builder-mcp",
|
|
10
|
+
};
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
function resolvePackageModule(metaUrl, ...segments) {
|
|
13
|
+
const scriptPath = fileURLToPath(metaUrl);
|
|
14
|
+
const scriptDir = path.dirname(scriptPath);
|
|
15
|
+
const direct = path.join(scriptDir, ...segments);
|
|
16
|
+
if (fs.existsSync(direct)) {
|
|
17
|
+
return pathToFileURL(direct).href;
|
|
18
|
+
}
|
|
19
|
+
if (path.basename(scriptDir) === ".bin") {
|
|
20
|
+
const binName = path.basename(scriptPath);
|
|
21
|
+
const pkgName = PKG_BY_BIN[binName];
|
|
22
|
+
if (!pkgName) {
|
|
23
|
+
throw new Error(`Unknown qingflow command: ${binName}`);
|
|
24
|
+
}
|
|
25
|
+
const scope = process.env.QINGFLOW_NPM_SCOPE || "@qingflow-tech";
|
|
26
|
+
const fromBin = path.join(scriptDir, "..", scope, pkgName, "npm", ...segments);
|
|
27
|
+
if (fs.existsSync(fromBin)) {
|
|
28
|
+
return pathToFileURL(fromBin).href;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Cannot locate ${segments.join("/")} from ${scriptPath}`);
|
|
32
|
+
}
|
|
6
33
|
|
|
34
|
+
const { getPackageRoot, spawnServer } = await import(resolvePackageModule(import.meta.url, "lib", "runtime.mjs"));
|
|
35
|
+
const packageRoot = getPackageRoot(import.meta.url);
|
|
7
36
|
spawnServer(packageRoot, process.argv.slice(2), "qingflow-app-user-mcp", { allowRuntimeBootstrap: false });
|
package/npm/lib/runtime.mjs
CHANGED
|
@@ -5,6 +5,12 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
const WINDOWS = process.platform === "win32";
|
|
7
7
|
|
|
8
|
+
const PKG_BY_BIN = {
|
|
9
|
+
qingflow: "qingflow-cli",
|
|
10
|
+
"qingflow-app-user-mcp": "qingflow-app-user-mcp",
|
|
11
|
+
"qingflow-app-builder-mcp": "qingflow-app-builder-mcp",
|
|
12
|
+
};
|
|
13
|
+
|
|
8
14
|
function runChecked(command, args, options = {}) {
|
|
9
15
|
const result = spawnSync(command, args, {
|
|
10
16
|
encoding: "utf8",
|
|
@@ -28,7 +34,18 @@ function commandWorks(command, args) {
|
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export function getPackageRoot(metaUrl) {
|
|
31
|
-
|
|
37
|
+
const scriptPath = fileURLToPath(metaUrl);
|
|
38
|
+
const scriptDir = path.dirname(scriptPath);
|
|
39
|
+
if (path.basename(scriptDir) === ".bin") {
|
|
40
|
+
const binName = path.basename(scriptPath);
|
|
41
|
+
const pkgName = PKG_BY_BIN[binName];
|
|
42
|
+
if (!pkgName) {
|
|
43
|
+
throw new Error(`Unknown qingflow command: ${binName}`);
|
|
44
|
+
}
|
|
45
|
+
const scope = process.env.QINGFLOW_NPM_SCOPE || "@qingflow-tech";
|
|
46
|
+
return path.join(scriptDir, "..", scope, pkgName);
|
|
47
|
+
}
|
|
48
|
+
return path.resolve(scriptDir, "..", "..");
|
|
32
49
|
}
|
|
33
50
|
|
|
34
51
|
export function getCodexHome() {
|
|
@@ -480,6 +497,17 @@ function getVenvPip(packageRoot) {
|
|
|
480
497
|
: path.join(getVenvDir(packageRoot), "bin", "pip");
|
|
481
498
|
}
|
|
482
499
|
|
|
500
|
+
function findOfflineProjectWheel(findLinksDir) {
|
|
501
|
+
const wheels = fs
|
|
502
|
+
.readdirSync(findLinksDir)
|
|
503
|
+
.filter((name) => /^qingflow_mcp-.*\.whl$/i.test(name))
|
|
504
|
+
.sort();
|
|
505
|
+
if (wheels.length === 0) {
|
|
506
|
+
throw new Error(`Offline install expected a qingflow_mcp wheel in ${findLinksDir}`);
|
|
507
|
+
}
|
|
508
|
+
return path.join(findLinksDir, wheels[wheels.length - 1]);
|
|
509
|
+
}
|
|
510
|
+
|
|
483
511
|
export function findPython() {
|
|
484
512
|
const preferred = process.env.QINGFLOW_MCP_PYTHON?.trim();
|
|
485
513
|
const candidates = preferred
|
|
@@ -528,7 +556,20 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
|
|
|
528
556
|
}
|
|
529
557
|
|
|
530
558
|
const pip = getVenvPip(packageRoot);
|
|
531
|
-
|
|
559
|
+
const pipArgs = ["install", "--disable-pip-version-check"];
|
|
560
|
+
const offlineFindLinks = process.env.QINGFLOW_MCP_PIP_FIND_LINKS?.trim();
|
|
561
|
+
if (process.env.QINGFLOW_MCP_PIP_NO_INDEX === "1") {
|
|
562
|
+
pipArgs.push("--no-index");
|
|
563
|
+
}
|
|
564
|
+
if (offlineFindLinks) {
|
|
565
|
+
pipArgs.push("--find-links", offlineFindLinks);
|
|
566
|
+
}
|
|
567
|
+
if (process.env.QINGFLOW_MCP_PIP_NO_INDEX === "1" && offlineFindLinks) {
|
|
568
|
+
pipArgs.push(findOfflineProjectWheel(offlineFindLinks));
|
|
569
|
+
} else {
|
|
570
|
+
pipArgs.push(".");
|
|
571
|
+
}
|
|
572
|
+
runChecked(pip, pipArgs, { cwd: packageRoot });
|
|
532
573
|
|
|
533
574
|
fs.writeFileSync(
|
|
534
575
|
stampPath,
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow App User
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.24.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow MCP Setup
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.24.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Record Analysis
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.24.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
Use this skill only for final statistical conclusions: counts, distributions, ratios, averages, rankings, trends, comparisons, and analysis reports.
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Record Delete
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.24.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Default Path
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Record Import
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.24.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Default Path
|
|
13
13
|
|
|
@@ -8,7 +8,7 @@ metadata:
|
|
|
8
8
|
|
|
9
9
|
# Qingflow Record Insert
|
|
10
10
|
|
|
11
|
-
> **Skill 版本**:`qingflow-skills-2026.06.24.
|
|
11
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
12
12
|
|
|
13
13
|
## Default Path
|
|
14
14
|
|
|
@@ -84,7 +84,7 @@ Without `record_id` / `workflow_node_id` / `fields-file`, the result is a static
|
|
|
84
84
|
## Working Rules
|
|
85
85
|
|
|
86
86
|
1. Start with `record_insert_schema_get`
|
|
87
|
-
2. Read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `
|
|
87
|
+
2. Read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, `payload_template`, field-level `expected_format`, `example_value`, and `options`
|
|
88
88
|
3. Inside every field bucket, read field-level `linkage` first when present; it is the canonical static hint for linked visibility, reference-driven auto fill, or formula-driven fields
|
|
89
89
|
4. Inside `optional_fields`, pay special attention to any field with `may_become_required=true`; these are writable fields that can become required when linked visibility or option-driven rules activate
|
|
90
90
|
5. Build `items` as `[{"fields": {...}}]`, where each `fields` map uses field titles from the insert schema
|
|
@@ -102,6 +102,8 @@ Without `record_id` / `workflow_node_id` / `fields-file`, the result is a static
|
|
|
102
102
|
17. Treat nested schema shape as guidance, not a brittle contract; do not hard-code transient implementation details like optional nested `field_id` shape when composing inserts
|
|
103
103
|
18. For `partial_success`, read `created_record_ids`, then repair only the failed `items[].row_number` using `failed_fields`; never retry the whole batch after any row has `write_executed=true`
|
|
104
104
|
19. Do not put Qingflow system fields in `fields`: `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, `流程标题`. They are generated by the platform and can be read after creation, not manually inserted.
|
|
105
|
+
20. When the user asks to add sample records after system setup, still generate values from schema: required fields first, select values from `options`, scalar/date/amount values from `expected_format` and `example_value`; do not invent values outside the insert schema.
|
|
106
|
+
21. For ratio, completion-rate, score, and percentage-like fields, obey the schema's `expected_format/example_value`. If the field was modeled as money/amount or otherwise only accepts integers, do not invent decimal percentages; either use an integer value that matches the schema or report that the field should be modeled as `number` for decimal ratios.
|
|
105
107
|
|
|
106
108
|
## Field Notes
|
|
107
109
|
|
|
@@ -146,6 +148,7 @@ qingflow --json record insert --app-key APP_KEY --items-file records.json
|
|
|
146
148
|
- Do not fill platform system fields such as `数据ID`, `编号`, `申请人`, `创建时间`, or `更新时间`
|
|
147
149
|
- Do not flatten subtable leaf fields to the top level
|
|
148
150
|
- Do not invent member / department / relation candidates outside schema or candidate scope
|
|
151
|
+
- Do not invent select option labels outside schema `options`
|
|
149
152
|
- Do not pre-query or silently guess member / department / relation ids when a natural string is enough
|
|
150
153
|
- Do not retry a whole batch after `created_record_ids` is non-empty
|
|
151
154
|
- Do not bind logic to a transient nested schema serialization detail when the field title and parent table already identify the legal payload shape
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Record Update
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.24.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Default Path
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Qingflow Task Ops
|
|
9
9
|
|
|
10
|
-
> **Skill 版本**:`qingflow-skills-2026.06.24.
|
|
10
|
+
> **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
@@ -203,10 +203,6 @@ class LayoutPreset(str, Enum):
|
|
|
203
203
|
single_section = "single_section"
|
|
204
204
|
|
|
205
205
|
|
|
206
|
-
class FlowPreset(str, Enum):
|
|
207
|
-
basic_approval = "basic_approval"
|
|
208
|
-
basic_fill_then_approve = "basic_fill_then_approve"
|
|
209
|
-
|
|
210
206
|
|
|
211
207
|
class ViewsPreset(str, Enum):
|
|
212
208
|
default_table = "default_table"
|
|
@@ -2342,7 +2338,6 @@ AppReadSummaryResponse = AppGetResponse
|
|
|
2342
2338
|
AppFieldsReadResponse = AppGetFieldsResponse
|
|
2343
2339
|
AppLayoutReadResponse = AppGetLayoutResponse
|
|
2344
2340
|
AppViewsReadResponse = AppGetViewsResponse
|
|
2345
|
-
AppFlowReadResponse = AppGetFlowResponse
|
|
2346
2341
|
AppChartsReadResponse = AppGetChartsResponse
|
|
2347
2342
|
|
|
2348
2343
|
|
|
@@ -2432,40 +2427,6 @@ class LayoutPlanRequest(StrictModel):
|
|
|
2432
2427
|
return payload
|
|
2433
2428
|
|
|
2434
2429
|
|
|
2435
|
-
class FlowPlanRequest(StrictModel):
|
|
2436
|
-
app_key: str
|
|
2437
|
-
mode: str = "replace"
|
|
2438
|
-
nodes: list[FlowNodePatch] = Field(default_factory=list)
|
|
2439
|
-
transitions: list[FlowTransitionPatch] = Field(default_factory=list)
|
|
2440
|
-
preset: FlowPreset | None = None
|
|
2441
|
-
|
|
2442
|
-
@model_validator(mode="before")
|
|
2443
|
-
@classmethod
|
|
2444
|
-
def normalize_mode_alias(cls, value: Any) -> Any:
|
|
2445
|
-
if not isinstance(value, dict):
|
|
2446
|
-
return value
|
|
2447
|
-
payload = dict(value)
|
|
2448
|
-
if str(payload.get("mode") or "").strip().lower() == "overwrite":
|
|
2449
|
-
payload["mode"] = "replace"
|
|
2450
|
-
raw_preset = payload.get("preset")
|
|
2451
|
-
if raw_preset is None and isinstance(payload.get("base_preset"), str):
|
|
2452
|
-
raw_preset = payload["base_preset"]
|
|
2453
|
-
payload["preset"] = raw_preset
|
|
2454
|
-
if isinstance(raw_preset, str):
|
|
2455
|
-
normalized_preset = raw_preset.strip().lower()
|
|
2456
|
-
preset_aliases = {
|
|
2457
|
-
"default_approval": FlowPreset.basic_approval.value,
|
|
2458
|
-
"approval": FlowPreset.basic_approval.value,
|
|
2459
|
-
"basic approval": FlowPreset.basic_approval.value,
|
|
2460
|
-
"default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
2461
|
-
"default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
|
|
2462
|
-
"fill_then_approve": FlowPreset.basic_fill_then_approve.value,
|
|
2463
|
-
}
|
|
2464
|
-
if normalized_preset in preset_aliases:
|
|
2465
|
-
payload["preset"] = preset_aliases[normalized_preset]
|
|
2466
|
-
return payload
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
2430
|
class ViewsPlanRequest(StrictModel):
|
|
2470
2431
|
app_key: str
|
|
2471
2432
|
upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
|