@josephyan/qingflow-app-builder-mcp 1.1.25 → 1.1.26
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 +5 -6
- package/docs/local-agent-install.md +6 -57
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
- package/npm/lib/runtime.mjs +101 -21
- package/npm/scripts/postinstall.mjs +10 -1
- package/package.json +2 -3
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +42 -104
- package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
- package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
- package/skills/qingflow-app-builder/references/create-app.md +11 -30
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +9 -31
- package/skills/qingflow-app-builder/references/match-rules.md +0 -15
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
- package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
- package/skills/qingflow-app-builder/references/update-flow.md +38 -22
- package/skills/qingflow-app-builder/references/update-schema.md +0 -14
- package/skills/qingflow-app-builder/references/update-views.md +14 -83
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +111 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +61 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-workflow-builder/SKILL.md +96 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +2 -6
- package/src/qingflow_mcp/builder_facade/models.py +108 -485
- package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
- package/src/qingflow_mcp/cli/commands/builder.py +364 -322
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +3 -12
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +27 -101
- package/src/qingflow_mcp/cli/commands/task.py +47 -28
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +16 -784
- package/src/qingflow_mcp/cli/main.py +33 -117
- package/src/qingflow_mcp/errors.py +2 -43
- package/src/qingflow_mcp/public_surface.py +17 -26
- package/src/qingflow_mcp/response_trim.py +17 -81
- package/src/qingflow_mcp/server.py +12 -14
- package/src/qingflow_mcp/server_app_builder.py +36 -79
- package/src/qingflow_mcp/server_app_user.py +16 -22
- package/src/qingflow_mcp/session_store.py +7 -11
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/executor.py +18 -245
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
- package/src/qingflow_mcp/tools/app_tools.py +43 -184
- package/src/qingflow_mcp/tools/approval_tools.py +35 -197
- package/src/qingflow_mcp/tools/auth_tools.py +16 -92
- package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
- package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
- package/src/qingflow_mcp/tools/directory_tools.py +72 -236
- package/src/qingflow_mcp/tools/export_tools.py +34 -244
- package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
- package/src/qingflow_mcp/tools/file_tools.py +3 -9
- package/src/qingflow_mcp/tools/import_tools.py +49 -336
- package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
- package/src/qingflow_mcp/tools/package_tools.py +6 -118
- package/src/qingflow_mcp/tools/portal_tools.py +3 -39
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
- package/src/qingflow_mcp/tools/record_tools.py +360 -1145
- package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
- package/src/qingflow_mcp/tools/role_tools.py +9 -80
- package/src/qingflow_mcp/tools/solution_tools.py +45 -59
- package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
- package/src/qingflow_mcp/tools/task_tools.py +29 -113
- package/src/qingflow_mcp/tools/view_tools.py +3 -106
- package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
- package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
- package/npm/bin/qingflow-skills.mjs +0 -5
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
- package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
# Complete System Development Guide
|
|
2
|
-
|
|
3
|
-
Use this when the user asks for a system, app package, workspace module set, or several related apps/forms.
|
|
4
|
-
Do not compress several business objects into one app.
|
|
5
|
-
|
|
6
|
-
## Recommended Completeness
|
|
7
|
-
|
|
8
|
-
Required:
|
|
9
|
-
|
|
10
|
-
- package exists or is created through `package_apply`
|
|
11
|
-
- core business objects are modeled as separate apps
|
|
12
|
-
- all new apps are created in one multi-app `app_schema_apply(package_id=..., apps=[...])`
|
|
13
|
-
- every app has stable `client_key`, `app_name`, `icon`, `color`, and one readable `as_data_title: true`
|
|
14
|
-
- no app creates platform system fields such as `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`
|
|
15
|
-
- same-call relations use `target_app_ref` to point to another `apps[].client_key`
|
|
16
|
-
- basic operating views exist for each core app
|
|
17
|
-
- package/apps/fields/relations are read back before repair or retry
|
|
18
|
-
|
|
19
|
-
Strongly recommended:
|
|
20
|
-
|
|
21
|
-
- cross-app relation fields for the main business links
|
|
22
|
-
- 1-3 concrete business buttons on core operating views through `app_views_apply.action_buttons`
|
|
23
|
-
- core metric charts and BI charts using semantic `metric`, `metrics`, `group_by`, and `where`
|
|
24
|
-
- a standard portal: business entry area, core metrics, BI charts, then concrete data views
|
|
25
|
-
- portal grid/business-entry sections contain real `config.items[]`, not empty containers
|
|
26
|
-
|
|
27
|
-
Workflow preflight:
|
|
28
|
-
|
|
29
|
-
- If an app will have approval/fill/copy workflow nodes, create an explicit business status `select` field in schema first, such as `状态`, `处理状态`, `审批状态`, `工单状态`, or `流程阶段`.
|
|
30
|
-
- Do not create platform workflow system fields such as `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`.
|
|
31
|
-
- If `app_flow_apply` returns `FLOW_DEPENDENCY_MISSING`, fix schema with the returned `suggested_next_call`, read fields back, then retry the flow. Do not skip the workflow silently.
|
|
32
|
-
|
|
33
|
-
Optional:
|
|
34
|
-
|
|
35
|
-
- workflow, reminders, buttons, associated resources, sample data, roles, and permissions when the user asks for them or the business process clearly depends on them
|
|
36
|
-
|
|
37
|
-
## Standard Path
|
|
38
|
-
|
|
39
|
-
1. Resolve or create package: `package_get` / `package_apply`.
|
|
40
|
-
2. Draft all apps and relations with stable `client_key` values.
|
|
41
|
-
3. Run one multi-app `app_schema_apply` with `apps[]`.
|
|
42
|
-
4. If write result is uncertain, run readback before retrying.
|
|
43
|
-
5. For each app, apply layout.
|
|
44
|
-
6. Apply workflow with `app_flow_apply` when the user asks for approval, fill, copy, reminders, or process routing.
|
|
45
|
-
7. Apply views; declare ordinary business buttons in `action_buttons` while designing the core operating views.
|
|
46
|
-
8. Create charts before portals when the portal needs metrics or BI sections. Keep chart payloads business-scoped, commonly 4-8 upserts by app or topic. Larger calls are accepted; if `CHART_UPSERT_BATCH_SIZE_RECOMMENDED` appears, read back before retrying and only repair failed items.
|
|
47
|
-
9. Add associated resources and advanced standalone buttons when they are part of the requested business process.
|
|
48
|
-
10. Create portal only after referenced apps, views, and charts are known.
|
|
49
|
-
11. Before the final response, write the complete-system delivery summary file described below.
|
|
50
|
-
|
|
51
|
-
## Field Modeling Rules
|
|
52
|
-
|
|
53
|
-
- Use agent-friendly field types in drafts: `text`, `multiline`, `select`, `multi_select`, `number`, `amount`, `date`, `datetime`, `member`, `department`, `attachment`, `relation`. The tool normalizes aliases such as `multiline -> long_text` and `select -> single_select`.
|
|
54
|
-
- Readback may show canonical field types such as `long_text` or `single_select`; do not rewrite fields just because aliases were normalized.
|
|
55
|
-
- Create only business fields. Do not create platform system fields listed in Required; reference them only where a tool explicitly supports system fields, such as button `source_field: "数据ID"`.
|
|
56
|
-
- Use `number` for ratios, completion rates, scores, percentages, durations, and quantities that may need decimals. Use `amount` only for currency/money semantics. This prevents sample data from producing decimal values for a field that the backend treats as integer money.
|
|
57
|
-
- For select fields, define the option set during schema design. Later sample data must use only those option labels or ids.
|
|
58
|
-
- Relation fields must include `target_app_ref` for same-batch apps or `target_app_key` for existing apps, plus `display_field: {"name": "..."}` and `visible_fields: [{"name": "..."}]`; do not write bare strings for display/visible field selectors.
|
|
59
|
-
- Do not impose a one-relation-field-per-app limit. Model the real business links and let backend validation/readback decide whether a specific relation is unsupported.
|
|
60
|
-
|
|
61
|
-
## View Query Rules
|
|
62
|
-
|
|
63
|
-
- Use `filters` for fixed saved filters that apply when the view opens.
|
|
64
|
-
- Use `query_conditions` only for the frontend query panel layout. It is not another filter DSL and it does not express OR.
|
|
65
|
-
- `query_conditions.rows` should contain query-panel fields such as text, long text, number, amount, date/datetime, select, member, department, phone, email, or boolean.
|
|
66
|
-
- Do not put relation, attachment, subtable/subfield, address, Q-Linker, or code-block fields in `query_conditions`.
|
|
67
|
-
- For current-record related views/reports, use `app_associated_resources_apply.match_mappings`, not `query_conditions`.
|
|
68
|
-
|
|
69
|
-
## Chart Rules
|
|
70
|
-
|
|
71
|
-
- Read `app_get_fields.chart_fields` before chart apply; normal form fields are not enough for QingBI dimensions, metrics, or filters.
|
|
72
|
-
- Use semantic chart input first: `metric`, `metrics`, `group_by`, and `where`.
|
|
73
|
-
- One KPI card uses `target` / `indicator` with `metric: "count(*)"` or `metric: "sum(金额)"`.
|
|
74
|
-
- Grouped distribution uses `bar` / `columnar` / `pie` with `group_by + metric`.
|
|
75
|
-
- Trend uses `line` / `area` / `columnar` with a date/month `group_by`.
|
|
76
|
-
- Two-metric comparison uses `dualaxes` with `left_metric` and `right_metric`.
|
|
77
|
-
- Do not use `summary` for a single KPI; `summary` is for table-style grouped summaries.
|
|
78
|
-
- Do not handwrite raw `indicator_field_ids`, `selectedMetrics`, or QingBI filter matrices on the main path.
|
|
79
|
-
- For complete systems, keep new chart writes business-scoped, commonly 4-8 charts by app or topic; larger calls are accepted but should be read back before retrying.
|
|
80
|
-
|
|
81
|
-
## Button Rules
|
|
82
|
-
|
|
83
|
-
- Design ordinary business buttons with the view: `app_views_apply.upsert_views[].action_buttons` or `patch_views[].set.action_buttons`.
|
|
84
|
-
- Use `app_custom_buttons_apply` only for style/icon changes, cross-view reuse, deleting button bodies, bulk binding reorder, or exact qRobot/wings configs.
|
|
85
|
-
- Use `add_data` when a current record creates a downstream/child record, such as order -> acceptance, work order -> inspection, or customer -> follow-up. Configure `target_app_key + field_mappings`; map `source_field: "数据ID"` to the target relation/reference field when linking back to the source record.
|
|
86
|
-
- Use `link` only for SOP, help, external system, or fixed URL navigation. Do not use `link` to fake approval, status transition, or data writeback.
|
|
87
|
-
- Use `qRobot`/`wings` only when the user provides exact existing automation config. Do not invent ids, parameters, or hidden automation.
|
|
88
|
-
- Prefer `detail` for current-record add-data actions, `list` for row actions, and `header` for global actions with no current-row context. Do not use current-record `field_mappings` on `header`.
|
|
89
|
-
- Approval, rejection, close-task, and status transition should be modeled as workflow/task behavior or existing automation; they are not generic custom-button types.
|
|
90
|
-
- Do not claim a complete operational system if all core operating views have no buttons and no reason is given.
|
|
91
|
-
|
|
92
|
-
## Delivery Summary File
|
|
93
|
-
|
|
94
|
-
For complete-system builds, create or update one local JSON file before the final response:
|
|
95
|
-
|
|
96
|
-
`tmp/qingflow_system_build_summary.json`
|
|
97
|
-
|
|
98
|
-
Use this file as the structured source of truth for the final report. Include at least:
|
|
99
|
-
|
|
100
|
-
```json
|
|
101
|
-
{
|
|
102
|
-
"package_id": 0,
|
|
103
|
-
"package_name": "",
|
|
104
|
-
"portal_dash_key": "",
|
|
105
|
-
"portal_live_status": "verified | unverified | not_created",
|
|
106
|
-
"front_end_visible": true,
|
|
107
|
-
"apps": [
|
|
108
|
-
{
|
|
109
|
-
"app_key": "",
|
|
110
|
-
"app_name": "",
|
|
111
|
-
"fields_count": 0,
|
|
112
|
-
"views_count": 0,
|
|
113
|
-
"flows_count": 0,
|
|
114
|
-
"charts_count": 0,
|
|
115
|
-
"publish_verify_status": "verified | unverified | failed"
|
|
116
|
-
}
|
|
117
|
-
],
|
|
118
|
-
"warnings": [],
|
|
119
|
-
"partial_items": [],
|
|
120
|
-
"needs_followup": []
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Rules:
|
|
125
|
-
|
|
126
|
-
- Populate IDs and counts from readback, not from intended payload only.
|
|
127
|
-
- If a required resource is not verified, put it in `partial_items` or `needs_followup` instead of reporting the system as fully complete.
|
|
128
|
-
- If the user later adds a data-entry task, keep that separate from the system-build summary unless the user explicitly wants one combined report.
|
|
129
|
-
- The final response should match this file: completed resources, unverified items, frontend visibility, and follow-up needs must not contradict the JSON.
|
|
130
|
-
- `front_end_visible=true` only when the relevant portal/app publish or live readback proves it. Backend write success alone is not enough.
|
|
131
|
-
- Do not mark workflow as missing when the user did not ask for approval, fill, copy, reminders, or process routing. Report it as "not requested/not configured".
|
|
132
|
-
- If `partial_items` or `needs_followup` is non-empty, the final status is partial, not complete.
|
|
133
|
-
- When running from the CLI repo or a test harness, validate the file with:
|
|
134
|
-
`python scripts/validate_system_build_summary.py tmp/qingflow_system_build_summary.json`.
|
|
135
|
-
|
|
136
|
-
Final response shape:
|
|
137
|
-
|
|
138
|
-
```text
|
|
139
|
-
已完成:...
|
|
140
|
-
未验证/待确认:...
|
|
141
|
-
前端可见:是/否/未确认
|
|
142
|
-
需要继续修复:是/否;原因...
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
## Recovery Rules
|
|
146
|
-
|
|
147
|
-
- Timeout, `partial_success`, `write_executed=true`, `safe_to_retry=false`, or incomplete readback means `write_may_have_succeeded`.
|
|
148
|
-
- Next action is always `readback_before_retry`: read package, resolve intended apps, then read fields/relations.
|
|
149
|
-
- Retry only verified missing apps, fields, or relations.
|
|
150
|
-
- Do not rebuild the system as separate single-app creates.
|
|
151
|
-
- Do not create `V2`, `测试`, timestamp, or random-suffix apps to bypass duplicate names.
|
|
152
|
-
|
|
153
|
-
## Portal Template
|
|
154
|
-
|
|
155
|
-
- Top area: one business entry grid plus one todo/common/frequent component when useful.
|
|
156
|
-
- Metrics: one row of 4-6 indicator/target cards with portal section `role: "metric"`; recommended height 5.
|
|
157
|
-
- BI charts: one row of 2-3 charts, 1-2 rows; recommended height 7.
|
|
158
|
-
- Data views: one row of 1-2 concrete views, 1-2 rows; recommended height 11.
|
|
159
|
-
- PC layout uses 24 columns. Components in the same row share the same `y`, must have the same `rows`, and their `cols` should sum to 24.
|
|
160
|
-
- Business-entry grid components must include real `config.items[]` entries linking to apps or portals; never submit an empty grid container.
|
|
161
|
-
- Build the portal only after referenced apps, raw `view_key`s, and chart ids/keys have been read back.
|
|
162
|
-
- Standard section order is business entry -> metric cards -> BI charts -> concrete business views.
|
|
163
|
-
- Do not reference platform default views such as `全部数据` / `我的数据` as main portal data views.
|
|
164
|
-
- Do not use portal `source_type: "filter"` as the normal automation path; it is raw/unstable and not part of the unified filter DSL.
|
|
165
|
-
|
|
166
|
-
## Stop Conditions
|
|
167
|
-
|
|
168
|
-
- If the user only asked for one app, switch to the single-app guide.
|
|
169
|
-
- If a same/similar package or app set already exists and the user did not say whether to extend, repair, replace, or create new, stop and ask. Do not start probing with new names.
|
|
170
|
-
- If readback shows part of the requested system already exists, list existing apps/fields/views/charts/portal, identify gaps, and patch only the missing or incorrect slices.
|
|
171
|
-
- If required package/app/field/relation readback contradicts the intended model, repair the specific missing slice before building dependent resources.
|
|
172
|
-
- If a public tool cannot express a needed business feature, state the gap and ask before using any fallback or submitting feedback.
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Single App Development Guide
|
|
2
|
-
|
|
3
|
-
Use this when the user asks for one app/form, or gives one `app_key`.
|
|
4
|
-
If the user asks for a package, system, or several related modules, use `complete-system-development-guide.md` instead.
|
|
5
|
-
|
|
6
|
-
## Recommended Completeness
|
|
7
|
-
|
|
8
|
-
Required:
|
|
9
|
-
|
|
10
|
-
- target package/app resolved by `package_get`, `app_resolve`, or `app_get`
|
|
11
|
-
- business fields created or updated through `app_schema_apply`
|
|
12
|
-
- exactly one readable top-level `as_data_title: true`
|
|
13
|
-
- no platform system fields in `add_fields`
|
|
14
|
-
- layout places the important fields
|
|
15
|
-
- at least one business view beyond platform default views when the task includes user-facing list work
|
|
16
|
-
|
|
17
|
-
Strongly recommended:
|
|
18
|
-
|
|
19
|
-
- saved filters and query panel fields for the main operating views
|
|
20
|
-
- 1-3 concrete business buttons on core operating views through `app_views_apply.action_buttons`, when the view has real actions
|
|
21
|
-
- simple charts when the app has status, amount, date, or owner fields worth tracking
|
|
22
|
-
- associated resources or advanced standalone buttons only when they support a concrete workflow
|
|
23
|
-
|
|
24
|
-
Optional:
|
|
25
|
-
|
|
26
|
-
- workflow, portal entry, sample data, roles, or visibility changes when requested by the user or clearly needed by the app's job
|
|
27
|
-
|
|
28
|
-
Workflow preflight:
|
|
29
|
-
|
|
30
|
-
- If the app will have approval/fill/copy workflow nodes, add one explicit business status `select` field first, such as `状态`, `处理状态`, `审批状态`, `工单状态`, or `流程阶段`.
|
|
31
|
-
- Do not create platform workflow system fields: `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`.
|
|
32
|
-
|
|
33
|
-
## Standard Path
|
|
34
|
-
|
|
35
|
-
1. Read current target: `app_resolve` or `app_get`.
|
|
36
|
-
2. Read current fields: `app_get_fields`.
|
|
37
|
-
3. Apply fields with `app_schema_apply`.
|
|
38
|
-
4. Apply layout with `app_layout_apply`.
|
|
39
|
-
5. Apply workflow with `app_flow_apply` when the user asks for approval, fill, copy, reminders, or process routing.
|
|
40
|
-
6. Apply views with `app_views_apply` when list/table/card/gantt behavior is part of the request; declare ordinary business buttons in `action_buttons` while designing the view.
|
|
41
|
-
7. Apply charts/associated resources/advanced standalone buttons only if they are part of the app's actual workflow.
|
|
42
|
-
8. Use `app_publish_verify` only when explicit live verification is required.
|
|
43
|
-
|
|
44
|
-
## Field Rules
|
|
45
|
-
|
|
46
|
-
- Use agent-friendly field types where possible: `text`, `multiline`, `select`, `multi_select`, `number`, `amount`, `date`, `datetime`, `member`, `department`, `attachment`, `relation`.
|
|
47
|
-
- The tool normalizes aliases: `multiline -> long_text`, `select -> single_select`, `checkbox -> multi_select`. Readback may show canonical types; do not treat that as a failed write.
|
|
48
|
-
- Select fields must define `options` during schema design. Options may be strings or objects such as `{"label": "进行中"}`; later sample data should use those option labels or ids.
|
|
49
|
-
- Relation fields are for real cross-object links. If used, include `target_app_key` for an existing app or `target_app_ref` for a same-batch app, plus `display_field: {"name": "..."}` and `visible_fields: [{"name": "..."}]`.
|
|
50
|
-
- Do not impose a one-relation-field limit. Add the relation fields the business needs, then rely on backend validation and readback.
|
|
51
|
-
- Do not create `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, or `流程标题`.
|
|
52
|
-
- Do not create built-in default views such as `全部数据` or `我的数据`; Qingflow provides system views.
|
|
53
|
-
|
|
54
|
-
## View Query Rules
|
|
55
|
-
|
|
56
|
-
- `filters` are fixed saved filters; `query_conditions` only configures frontend query-panel fields.
|
|
57
|
-
- Put only query-panel fields in `query_conditions.rows`: text, long text, number, amount, date/datetime, select, member, department, phone, email, or boolean.
|
|
58
|
-
- Do not put relation, attachment, subtable/subfield, address, Q-Linker, or code-block fields in `query_conditions`; use fixed `filters` or associated-resource `match_mappings` instead.
|
|
59
|
-
|
|
60
|
-
## Chart Rules
|
|
61
|
-
|
|
62
|
-
- Read `app_get_fields.chart_fields` before chart apply; normal form fields are not enough for QingBI dimensions, metrics, or filters.
|
|
63
|
-
- Use semantic chart input first: `metric`, `metrics`, `group_by`, and `where`.
|
|
64
|
-
- One KPI card uses `target` / `indicator` with `metric: "count(*)"` or `metric: "sum(金额)"`.
|
|
65
|
-
- Grouped distribution uses `bar` / `columnar` / `pie` with `group_by + metric`.
|
|
66
|
-
- Trend uses `line` / `area` / `columnar` with a date/month `group_by`.
|
|
67
|
-
- Do not use `summary` for a single KPI; `summary` is for table-style grouped summaries.
|
|
68
|
-
- Do not handwrite raw `indicator_field_ids`, `selectedMetrics`, or QingBI filter matrices on the main path.
|
|
69
|
-
|
|
70
|
-
## Button Rules
|
|
71
|
-
|
|
72
|
-
- Use `action_buttons` for ordinary buttons tied to one view.
|
|
73
|
-
- Use `app_custom_buttons_apply` only for style/icon changes, cross-view reuse, deleting button bodies, bulk binding reorder, or exact qRobot/wings configs.
|
|
74
|
-
- Use `add_data` when a current record creates a downstream/child record; configure `target_app_key + field_mappings`, usually mapping `source_field: "数据ID"` to the target relation/reference field.
|
|
75
|
-
- Use `link` only for SOP, help, external system, or fixed URL navigation. Do not use it to fake approval, status transition, or data writeback.
|
|
76
|
-
- Use `qRobot`/`wings` only when the user provides exact existing automation config.
|
|
77
|
-
- Prefer `detail` for current-record add-data actions, `list` for row actions, and `header` for global actions with no current-row context. Do not use current-record `field_mappings` on `header`.
|
|
78
|
-
- Approval, rejection, close-task, and status transition should be workflow/task behavior or existing automation; they are not generic custom-button types.
|
|
79
|
-
|
|
80
|
-
## Stop Conditions
|
|
81
|
-
|
|
82
|
-
- If the task actually needs multiple business objects, stop and switch to the complete-system guide.
|
|
83
|
-
- If an existing app with the same or very similar name appears and the user did not explicitly say update, extend, or create new, stop and ask which target to use.
|
|
84
|
-
- If readback shows the requested app already has some resources, patch the verified gaps instead of recreating the app.
|
|
85
|
-
- If a write returns `partial_success`, `write_executed=true`, or `safe_to_retry=false`, read back before retrying.
|
|
86
|
-
- If the same validation error repeats twice, re-read `builder_tool_contract` instead of guessing.
|
|
87
|
-
- Do not create `V2`, `测试`, timestamp, or random-suffix apps to bypass duplicate names in a real business package.
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
5
|
-
import json
|
|
6
|
-
import sys
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
VALID_PORTAL_STATUSES = {"verified", "unverified", "not_created"}
|
|
12
|
-
VALID_APP_VERIFY_STATUSES = {"verified", "unverified", "failed"}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def main(argv: list[str] | None = None) -> int:
|
|
16
|
-
parser = argparse.ArgumentParser(description="Validate a Qingflow complete-system delivery summary JSON file.")
|
|
17
|
-
parser.add_argument("summary_file", nargs="?", default="tmp/qingflow_system_build_summary.json")
|
|
18
|
-
args = parser.parse_args(argv)
|
|
19
|
-
path = Path(args.summary_file)
|
|
20
|
-
issues = validate_summary_file(path)
|
|
21
|
-
if issues:
|
|
22
|
-
_emit({"status": "failed", "error_code": "SYSTEM_BUILD_SUMMARY_INVALID", "path": str(path), "issues": issues})
|
|
23
|
-
return 1
|
|
24
|
-
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
25
|
-
_emit(
|
|
26
|
-
{
|
|
27
|
-
"status": "success",
|
|
28
|
-
"path": str(path),
|
|
29
|
-
"summary": {
|
|
30
|
-
"package_id": payload.get("package_id"),
|
|
31
|
-
"package_name": payload.get("package_name"),
|
|
32
|
-
"portal_dash_key": payload.get("portal_dash_key"),
|
|
33
|
-
"portal_live_status": payload.get("portal_live_status"),
|
|
34
|
-
"app_count": len(payload.get("apps") or []),
|
|
35
|
-
"partial_count": len(payload.get("partial_items") or []),
|
|
36
|
-
"needs_followup_count": len(payload.get("needs_followup") or []),
|
|
37
|
-
},
|
|
38
|
-
}
|
|
39
|
-
)
|
|
40
|
-
return 0
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def validate_summary_file(path: Path) -> list[dict[str, Any]]:
|
|
44
|
-
if not path.exists():
|
|
45
|
-
return [_issue("MISSING_FILE", "$", f"summary file does not exist: {path}")]
|
|
46
|
-
try:
|
|
47
|
-
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
48
|
-
except OSError as exc:
|
|
49
|
-
return [_issue("READ_FAILED", "$", str(exc))]
|
|
50
|
-
except json.JSONDecodeError as exc:
|
|
51
|
-
return [_issue("INVALID_JSON", "$", exc.msg)]
|
|
52
|
-
if not isinstance(payload, dict):
|
|
53
|
-
return [_issue("INVALID_ROOT", "$", "summary root must be a JSON object")]
|
|
54
|
-
|
|
55
|
-
issues: list[dict[str, Any]] = []
|
|
56
|
-
_require_positive_int(payload, "package_id", issues)
|
|
57
|
-
_require_nonempty_string(payload, "package_name", issues)
|
|
58
|
-
_require_bool(payload, "front_end_visible", issues)
|
|
59
|
-
_require_string_enum(payload, "portal_live_status", VALID_PORTAL_STATUSES, issues)
|
|
60
|
-
if payload.get("portal_live_status") == "verified":
|
|
61
|
-
_require_nonempty_string(payload, "portal_dash_key", issues)
|
|
62
|
-
elif "portal_dash_key" in payload and payload.get("portal_dash_key") is not None and not isinstance(payload.get("portal_dash_key"), str):
|
|
63
|
-
issues.append(_issue("INVALID_TYPE", "$.portal_dash_key", "portal_dash_key must be a string when present"))
|
|
64
|
-
|
|
65
|
-
apps = payload.get("apps")
|
|
66
|
-
if not isinstance(apps, list) or not apps:
|
|
67
|
-
issues.append(_issue("INVALID_APPS", "$.apps", "apps must be a non-empty array"))
|
|
68
|
-
else:
|
|
69
|
-
for index, app in enumerate(apps):
|
|
70
|
-
prefix = f"$.apps[{index}]"
|
|
71
|
-
if not isinstance(app, dict):
|
|
72
|
-
issues.append(_issue("INVALID_APP_ITEM", prefix, "app item must be an object"))
|
|
73
|
-
continue
|
|
74
|
-
_require_nonempty_string(app, "app_key", issues, prefix)
|
|
75
|
-
_require_nonempty_string(app, "app_name", issues, prefix)
|
|
76
|
-
for key in ("fields_count", "views_count", "flows_count", "charts_count"):
|
|
77
|
-
_require_nonnegative_int(app, key, issues, prefix)
|
|
78
|
-
_require_string_enum(app, "publish_verify_status", VALID_APP_VERIFY_STATUSES, issues, prefix)
|
|
79
|
-
|
|
80
|
-
for key in ("warnings", "partial_items", "needs_followup"):
|
|
81
|
-
if not isinstance(payload.get(key), list):
|
|
82
|
-
issues.append(_issue("INVALID_TYPE", f"$.{key}", f"{key} must be an array"))
|
|
83
|
-
return issues
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _require_positive_int(payload: dict[str, Any], key: str, issues: list[dict[str, Any]], prefix: str = "$") -> None:
|
|
87
|
-
value = payload.get(key)
|
|
88
|
-
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
89
|
-
issues.append(_issue("INVALID_TYPE", f"{prefix}.{key}", f"{key} must be a positive integer"))
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def _require_nonnegative_int(payload: dict[str, Any], key: str, issues: list[dict[str, Any]], prefix: str = "$") -> None:
|
|
93
|
-
value = payload.get(key)
|
|
94
|
-
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
95
|
-
issues.append(_issue("INVALID_TYPE", f"{prefix}.{key}", f"{key} must be a non-negative integer"))
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _require_nonempty_string(payload: dict[str, Any], key: str, issues: list[dict[str, Any]], prefix: str = "$") -> None:
|
|
99
|
-
value = payload.get(key)
|
|
100
|
-
if not isinstance(value, str) or not value.strip():
|
|
101
|
-
issues.append(_issue("INVALID_TYPE", f"{prefix}.{key}", f"{key} must be a non-empty string"))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _require_bool(payload: dict[str, Any], key: str, issues: list[dict[str, Any]], prefix: str = "$") -> None:
|
|
105
|
-
if not isinstance(payload.get(key), bool):
|
|
106
|
-
issues.append(_issue("INVALID_TYPE", f"{prefix}.{key}", f"{key} must be a boolean"))
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _require_string_enum(payload: dict[str, Any], key: str, allowed: set[str], issues: list[dict[str, Any]], prefix: str = "$") -> None:
|
|
110
|
-
value = payload.get(key)
|
|
111
|
-
if value not in allowed:
|
|
112
|
-
issues.append(_issue("INVALID_VALUE", f"{prefix}.{key}", f"{key} must be one of {sorted(allowed)}"))
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _issue(code: str, path: str, message: str) -> dict[str, Any]:
|
|
116
|
-
return {"code": code, "path": path, "message": message}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _emit(payload: dict[str, Any]) -> None:
|
|
120
|
-
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if __name__ == "__main__":
|
|
124
|
-
sys.exit(main())
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from ..spec_models import EntitySpec, WorkflowNodeType
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
WORKFLOW_TYPE_MAP = {
|
|
9
|
-
WorkflowNodeType.start: {"type": 0, "dealType": 3},
|
|
10
|
-
WorkflowNodeType.branch: {"type": 1, "dealType": None},
|
|
11
|
-
WorkflowNodeType.audit: {"type": 0, "dealType": 0},
|
|
12
|
-
WorkflowNodeType.fill: {"type": 0, "dealType": 1},
|
|
13
|
-
WorkflowNodeType.copy: {"type": 0, "dealType": 2},
|
|
14
|
-
WorkflowNodeType.webhook: {"type": 3, "dealType": 10},
|
|
15
|
-
WorkflowNodeType.condition: {"type": 2, "dealType": None},
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _default_audit_user_infos() -> dict[str, Any]:
|
|
20
|
-
return {
|
|
21
|
-
"member": [],
|
|
22
|
-
"depart": [],
|
|
23
|
-
"role": [],
|
|
24
|
-
"dynamic": [],
|
|
25
|
-
"includeSubDeparts": None,
|
|
26
|
-
"externalMemberList": [],
|
|
27
|
-
"externalDepartList": [],
|
|
28
|
-
"role_refs": [],
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def compile_workflow(entity: EntitySpec) -> dict[str, Any] | None:
|
|
33
|
-
workflow = entity.workflow
|
|
34
|
-
if workflow is None or not workflow.enabled:
|
|
35
|
-
return None
|
|
36
|
-
actions: list[dict[str, Any]] = []
|
|
37
|
-
seen_node_ids: set[str] = set()
|
|
38
|
-
created_extra_branch_lanes: set[str] = set()
|
|
39
|
-
start_node_ids = {
|
|
40
|
-
node.node_id
|
|
41
|
-
for node in workflow.nodes
|
|
42
|
-
if node.node_type == WorkflowNodeType.start
|
|
43
|
-
}
|
|
44
|
-
for node in workflow.nodes:
|
|
45
|
-
if node.node_type == WorkflowNodeType.start:
|
|
46
|
-
seen_node_ids.add(node.node_id)
|
|
47
|
-
continue
|
|
48
|
-
if node.parent_node_id and node.parent_node_id not in seen_node_ids:
|
|
49
|
-
raise ValueError(f"workflow node '{node.node_id}' must appear after parent node '{node.parent_node_id}'")
|
|
50
|
-
if node.branch_parent_id and node.branch_parent_id not in seen_node_ids:
|
|
51
|
-
raise ValueError(f"workflow node '{node.node_id}' must appear after branch node '{node.branch_parent_id}'")
|
|
52
|
-
branch_index = _branch_index(node)
|
|
53
|
-
branch_lane_ref = _branch_lane_ref(node.branch_parent_id, branch_index) if node.branch_parent_id else None
|
|
54
|
-
if branch_lane_ref and branch_index > 2 and branch_lane_ref not in created_extra_branch_lanes:
|
|
55
|
-
actions.append(
|
|
56
|
-
{
|
|
57
|
-
"action": "create_sub_branch",
|
|
58
|
-
"node_id": branch_lane_ref,
|
|
59
|
-
"payload": {
|
|
60
|
-
"editVersionNo": 1,
|
|
61
|
-
"auditNodeRef": node.branch_parent_id,
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
)
|
|
65
|
-
created_extra_branch_lanes.add(branch_lane_ref)
|
|
66
|
-
lane_only = bool((node.config or {}).get("__lane_only__")) and node.node_type == WorkflowNodeType.condition and branch_lane_ref
|
|
67
|
-
if lane_only:
|
|
68
|
-
lane_payload = {key: value for key, value in node.config.items() if key != "__lane_only__"}
|
|
69
|
-
actions.append(
|
|
70
|
-
{
|
|
71
|
-
"action": "update_node",
|
|
72
|
-
"node_id": branch_lane_ref,
|
|
73
|
-
"node_name": node.name,
|
|
74
|
-
"node_type": node.node_type.value,
|
|
75
|
-
"payload": {
|
|
76
|
-
"editVersionNo": 1,
|
|
77
|
-
"auditNodeName": node.name,
|
|
78
|
-
"type": WORKFLOW_TYPE_MAP[node.node_type]["type"],
|
|
79
|
-
"dealType": WORKFLOW_TYPE_MAP[node.node_type]["dealType"],
|
|
80
|
-
**lane_payload,
|
|
81
|
-
},
|
|
82
|
-
}
|
|
83
|
-
)
|
|
84
|
-
seen_node_ids.add(node.node_id)
|
|
85
|
-
continue
|
|
86
|
-
actions.append(
|
|
87
|
-
{
|
|
88
|
-
"action": "add_node",
|
|
89
|
-
"node_id": node.node_id,
|
|
90
|
-
"node_name": node.name,
|
|
91
|
-
"node_type": node.node_type.value,
|
|
92
|
-
"payload": {
|
|
93
|
-
"editVersionNo": 1,
|
|
94
|
-
"auditNodeName": node.name,
|
|
95
|
-
"type": WORKFLOW_TYPE_MAP[node.node_type]["type"],
|
|
96
|
-
"dealType": WORKFLOW_TYPE_MAP[node.node_type]["dealType"],
|
|
97
|
-
"prevNodeRef": _prev_node_ref(node, branch_lane_ref, start_node_ids),
|
|
98
|
-
"auditUserInfos": _build_audit_user_infos(node)
|
|
99
|
-
if node.node_type in {WorkflowNodeType.audit, WorkflowNodeType.fill, WorkflowNodeType.copy}
|
|
100
|
-
else None,
|
|
101
|
-
**node.config,
|
|
102
|
-
},
|
|
103
|
-
}
|
|
104
|
-
)
|
|
105
|
-
seen_node_ids.add(node.node_id)
|
|
106
|
-
return {
|
|
107
|
-
"global_settings": {
|
|
108
|
-
"editVersionNo": 1,
|
|
109
|
-
**workflow.global_settings,
|
|
110
|
-
},
|
|
111
|
-
"actions": actions,
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _build_audit_user_infos(node) -> dict[str, Any]:
|
|
116
|
-
audit_user_infos = _default_audit_user_infos()
|
|
117
|
-
assignees = node.assignees or {}
|
|
118
|
-
member_uids = assignees.get("member_uids") or []
|
|
119
|
-
if member_uids:
|
|
120
|
-
audit_user_infos["member"] = [
|
|
121
|
-
{"uid": uid, "beingFrontendConfig": True}
|
|
122
|
-
for uid in member_uids
|
|
123
|
-
if isinstance(uid, int) and uid > 0
|
|
124
|
-
]
|
|
125
|
-
role_refs = assignees.get("role_refs") or []
|
|
126
|
-
if role_refs:
|
|
127
|
-
audit_user_infos["role_refs"] = [role_ref for role_ref in role_refs if role_ref]
|
|
128
|
-
role_entries = assignees.get("role_entries") or []
|
|
129
|
-
if role_entries:
|
|
130
|
-
audit_user_infos["role"] = [
|
|
131
|
-
{
|
|
132
|
-
"roleId": int(entry.get("roleId") or entry.get("role_id")),
|
|
133
|
-
"roleName": entry.get("roleName") or entry.get("role_name") or str(entry.get("roleId") or entry.get("role_id")),
|
|
134
|
-
"roleIcon": entry.get("roleIcon") or entry.get("role_icon") or "ex-user-outlined",
|
|
135
|
-
"beingFrontendConfig": True,
|
|
136
|
-
}
|
|
137
|
-
for entry in role_entries
|
|
138
|
-
if isinstance(entry, dict) and isinstance(entry.get("roleId") or entry.get("role_id"), int) and int(entry.get("roleId") or entry.get("role_id")) > 0
|
|
139
|
-
]
|
|
140
|
-
include_sub_departs = assignees.get("include_sub_departs")
|
|
141
|
-
if include_sub_departs is not None:
|
|
142
|
-
audit_user_infos["includeSubDeparts"] = bool(include_sub_departs)
|
|
143
|
-
return audit_user_infos
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def _prev_node_ref(node, branch_lane_ref: str | None, start_node_ids: set[str]) -> str:
|
|
147
|
-
if branch_lane_ref:
|
|
148
|
-
if node.parent_node_id and node.parent_node_id != node.branch_parent_id:
|
|
149
|
-
return node.parent_node_id
|
|
150
|
-
return branch_lane_ref
|
|
151
|
-
if node.parent_node_id in start_node_ids:
|
|
152
|
-
return "__applicant__"
|
|
153
|
-
return node.parent_node_id or "__applicant__"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def _branch_index(node) -> int:
|
|
157
|
-
config = node.config or {}
|
|
158
|
-
raw_value = getattr(node, "branch_index", None)
|
|
159
|
-
if raw_value is None:
|
|
160
|
-
raw_value = config.get("branch_index", config.get("branchIndex", config.get("lane_index", config.get("laneIndex", 1))))
|
|
161
|
-
try:
|
|
162
|
-
branch_index = int(raw_value)
|
|
163
|
-
except (TypeError, ValueError) as exc:
|
|
164
|
-
raise ValueError(f"workflow node '{node.node_id}' has invalid branch index '{raw_value}'") from exc
|
|
165
|
-
if branch_index <= 0:
|
|
166
|
-
raise ValueError(f"workflow node '{node.node_id}' branch index must be positive")
|
|
167
|
-
return branch_index
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def _branch_lane_ref(branch_parent_id: str | None, branch_index: int) -> str:
|
|
171
|
-
if not branch_parent_id:
|
|
172
|
-
raise ValueError("branch_parent_id is required for branch lane references")
|
|
173
|
-
return f"__branch_lane__{branch_parent_id}__{branch_index}"
|