@josephyan/qingflow-app-builder-mcp 0.2.0-beta.11 → 0.2.0-beta.13
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/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +69 -24
- package/skills/qingflow-app-builder/references/create-app.md +36 -0
- package/skills/qingflow-app-builder/references/gotchas.md +12 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +19 -2
- package/skills/qingflow-app-builder/references/update-flow.md +38 -9
- package/skills/qingflow-app-builder/references/update-views.md +12 -6
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +250 -33
- package/src/qingflow_mcp/tools/ai_builder_tools.py +12 -0
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.13
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.13 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -17,6 +17,29 @@ Before any write or verification flow, identify whether the task targets `test`
|
|
|
17
17
|
|
|
18
18
|
Pick the smallest tool layer that can finish the task.
|
|
19
19
|
|
|
20
|
+
## Mental Model
|
|
21
|
+
|
|
22
|
+
Model builder requests in four layers. Do not flatten them.
|
|
23
|
+
|
|
24
|
+
- `package`: the solution container or app bundle, for example “研发项目管理” or “费控管理系统”
|
|
25
|
+
- `app`: one form/app inside that package, for example “项目”, “需求”, “任务”, “缺陷”, “团队”
|
|
26
|
+
- `field`: one field inside one app
|
|
27
|
+
- `relation`: a field that links one app to another app
|
|
28
|
+
|
|
29
|
+
Interpret user intent with this hierarchy:
|
|
30
|
+
|
|
31
|
+
- If the user says “应用包”, “系统”, “包含多个表单”, “多个模块”, or asks for several named forms that relate to each other, treat it as a `package` with multiple `apps`
|
|
32
|
+
- Do not compress a multi-app system into one app with several text fields
|
|
33
|
+
- Names like “项目/需求/任务/缺陷/团队” or “费用申请/预算管理/报销审批” are usually separate apps, not text fields
|
|
34
|
+
- Build the apps first, then add `relation` fields to connect them
|
|
35
|
+
|
|
36
|
+
Default modeling rules:
|
|
37
|
+
|
|
38
|
+
- One business object -> one app
|
|
39
|
+
- Attributes of that object -> fields inside that app
|
|
40
|
+
- Another business object -> a separate app, not a text field
|
|
41
|
+
- Cross-object links -> relation fields, not text fields
|
|
42
|
+
|
|
20
43
|
- Authentication and workspace: `auth_*`, `workspace_*`
|
|
21
44
|
- File upload: `file_upload_local`
|
|
22
45
|
- Resource resolve/read: `package_list`, `package_resolve`, `package_create`, `builder_tool_contract`, `member_search`, `role_search`, `app_resolve`, `app_read_summary`, `app_read_fields`, `app_read_layout_summary`, `app_read_views_summary`, `app_read_flow_summary`
|
|
@@ -34,6 +57,7 @@ Note:
|
|
|
34
57
|
- For flow presets, map natural language to canonical values before calling MCP:
|
|
35
58
|
- “默认审批/基础审批/普通审批” -> `basic_approval`
|
|
36
59
|
- “先填报再审批/提交后审批” -> `basic_fill_then_approve`
|
|
60
|
+
- For first-time flow or view work in a session, read `builder_tool_contract` before planning so keys, aliases, presets, and minimal examples come from MCP instead of memory.
|
|
37
61
|
- For workflow assignees, prefer roles over explicit members:
|
|
38
62
|
- use `role_search` first
|
|
39
63
|
- use `member_search` only when the user explicitly names members or no stable role exists
|
|
@@ -44,6 +68,7 @@ Default policy:
|
|
|
44
68
|
|
|
45
69
|
- Creating or updating one app inside an existing package: resolve the package/app, read compact state, plan patches on the server, then apply schema/layout/flow/views patches explicitly.
|
|
46
70
|
- If package creation looks necessary or beneficial, ask the user to confirm before calling `package_create`.
|
|
71
|
+
- If the user describes a system/package with multiple forms or modules, do not start with `app_schema_apply` on the package name. Resolve or create the package first, then create each app separately.
|
|
47
72
|
|
|
48
73
|
## Standard Operating Order
|
|
49
74
|
|
|
@@ -56,38 +81,49 @@ Before any business tool:
|
|
|
56
81
|
For builder work:
|
|
57
82
|
|
|
58
83
|
1. Resolve the target package with `package_resolve`; if resolution is ambiguous or you need a read-only fallback, use `package_list`. If you believe a new package should be created, ask the user to confirm before calling `package_create`.
|
|
59
|
-
2.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
2. Decide whether the target is one app or a multi-app package:
|
|
85
|
+
- one app: continue with `app_resolve`
|
|
86
|
+
- multi-app package/system: create or resolve the package, then create each app separately before adding relations
|
|
87
|
+
3. Resolve the target app with `app_resolve` if the request is an update.
|
|
88
|
+
4. Read only the smallest summary you need: `app_read_summary`, `app_read_fields`, `app_read_layout_summary`, `app_read_views_summary`, `app_read_flow_summary`.
|
|
89
|
+
5. Use `app_schema_plan`, `app_layout_plan`, `app_flow_plan`, or `app_views_plan` before any non-trivial write.
|
|
90
|
+
6. Use `app_schema_apply` for create/upsert/remove field work. It publishes by default after the patch lands.
|
|
91
|
+
7. If the app must belong to a package, use `package_attach_app` explicitly after schema work unless readback already shows the target `tag_id`.
|
|
92
|
+
8. Use `app_layout_apply` only when the user is explicitly changing layout. Prefer the default `mode=merge`; use `mode=replace` only when you intend to place every field explicitly. It publishes by default.
|
|
93
|
+
9. Use `app_flow_apply` after schema exists. It publishes by default.
|
|
94
|
+
10. Use `app_views_apply` when the user wants explicit table/card/board/gantt views. It publishes by default.
|
|
95
|
+
11. Use `app_publish_verify` only when the user explicitly wants final publish/live verification or you need an explicit verification pass.
|
|
96
|
+
12. If a write fails with `APP_EDIT_LOCKED`, stop normal writes. Only use `app_release_edit_lock_if_mine` when the failed result shows the lock owner is the current logged-in user.
|
|
69
97
|
|
|
70
98
|
For view work, keep the order strict:
|
|
71
99
|
|
|
72
|
-
1. `
|
|
73
|
-
2. `
|
|
74
|
-
3. `
|
|
75
|
-
4. `
|
|
100
|
+
1. `builder_tool_contract`
|
|
101
|
+
2. `app_read_fields`
|
|
102
|
+
3. `app_read_views_summary`
|
|
103
|
+
4. `app_views_plan`
|
|
104
|
+
5. `app_views_apply`
|
|
105
|
+
6. `app_read_views_summary` again whenever `app_views_apply` returns `failed` or `partial_success`
|
|
76
106
|
|
|
77
107
|
For flow work, keep the order strict:
|
|
78
108
|
|
|
79
|
-
1. `
|
|
80
|
-
2. `
|
|
81
|
-
3. `
|
|
82
|
-
4. `
|
|
83
|
-
5.
|
|
84
|
-
6.
|
|
85
|
-
7.
|
|
109
|
+
1. `builder_tool_contract`
|
|
110
|
+
2. `app_read_fields`
|
|
111
|
+
3. `app_read_flow_summary`
|
|
112
|
+
4. `role_search` or `member_search` if assignees need to come from the directory
|
|
113
|
+
5. `role_create` if the user wants a reusable role and no suitable role exists yet
|
|
114
|
+
6. Start from a canonical preset when possible
|
|
115
|
+
7. Use patch-style edits to that skeleton instead of freehand full-graph generation
|
|
116
|
+
8. When patching a preset skeleton, reuse the preset node ids:
|
|
117
|
+
- `basic_approval` -> patch `approve_1`
|
|
118
|
+
- `basic_fill_then_approve` -> patch `fill_1` and `approve_1`
|
|
119
|
+
Do not invent a second approval/fill node id unless you are intentionally replacing the skeleton and removing the preset node.
|
|
120
|
+
9. Declare approver/fill/copy assignees explicitly:
|
|
86
121
|
- prefer `assignees.role_names`
|
|
87
122
|
- support `assignees.member_names` / `assignees.member_emails` / `assignees.member_uids`
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
123
|
+
10. When a node must edit specific fields, declare `permissions.editable_fields`
|
|
124
|
+
11. `app_flow_plan`
|
|
125
|
+
12. `app_flow_apply`
|
|
126
|
+
13. `app_read_flow_summary` after apply whenever the user asked for verification or apply returns `partial_success`
|
|
91
127
|
|
|
92
128
|
In `prod`, keep `plan` and `apply` as separate phases unless the user explicitly asks for a direct live execution.
|
|
93
129
|
|
|
@@ -104,18 +140,23 @@ For additive work on existing systems:
|
|
|
104
140
|
- `app_schema_apply` is the only public patch tool allowed to create an app shell; `app_layout_apply`, `app_flow_apply`, and `app_views_apply` require an existing app.
|
|
105
141
|
- Prefer `*_plan` before `*_apply`; the plan tools do server-side normalization and return the next executable call skeleton.
|
|
106
142
|
- For abstract requests like “默认视图”, “基础审批流”, “灵活流程”, or “美观布局”, first translate the intent into a stable preset or explicit patch. Do not send those phrases to MCP unchanged.
|
|
143
|
+
- If the user asks for a business system or package that contains several forms, do not use the system/package name as `app_name` and do not try to store the child app names as text fields.
|
|
107
144
|
- For flexible workflow requests, split the work into two steps:
|
|
108
145
|
1. create a base skeleton with a preset
|
|
109
146
|
2. apply explicit business-specific changes as patchable nodes/transitions
|
|
147
|
+
- For preset-based flows, treat preset node ids as part of the public contract. Patch the skeleton nodes by the same ids instead of creating a parallel node with a new id and leaving the preset node unassigned.
|
|
110
148
|
- Approval, fill, and copy nodes must declare at least one assignee. Treat this as a hard requirement, not an optional detail.
|
|
111
149
|
- For workflow nodes, use the canonical public shape:
|
|
112
150
|
- `assignees.role_names`
|
|
113
151
|
- `assignees.member_names`
|
|
114
152
|
- `permissions.editable_fields`
|
|
153
|
+
- Reuse `app_flow_plan` output directly when it succeeds. Do not rewrite it into internal keys such as `role_entries` or `editable_que_ids`.
|
|
154
|
+
- Reuse `app_views_plan` output directly when it succeeds. Do not re-expand aliases such as `column_names`.
|
|
115
155
|
- Do not guess role ids or member ids. Resolve them from the directory first.
|
|
116
156
|
- `app_schema_apply` does not treat package attachment as success criteria; if package ownership matters, verify `tag_ids_after` and call `package_attach_app` explicitly.
|
|
117
157
|
- `package_attach_app` is the source of truth for package ownership; do not assume app creation or publish implicitly attaches the app.
|
|
118
158
|
- `relation` and `subtable` must be explicit; do not infer them from vague natural language.
|
|
159
|
+
- Another app is not a field. If two business objects should both have their own records, build two apps and connect them with relation fields.
|
|
119
160
|
- In `prod`, prefer explicit patch tools and avoid any speculative create flow.
|
|
120
161
|
- Never try to bypass collaborative edit locks. `app_release_edit_lock_if_mine` is only for the case where the lock owner is the current authenticated user.
|
|
121
162
|
|
|
@@ -125,7 +166,11 @@ For additive work on existing systems:
|
|
|
125
166
|
- `app_publish_verify` is the publish source of truth.
|
|
126
167
|
- If readback mismatches the UI, compare `request_route` and do not assume the builder hit the same `qf_version` as the browser
|
|
127
168
|
- Treat post-write readback as the source of truth, not just write status codes
|
|
169
|
+
- For views, a top-level `VIEW_APPLY_FAILED` does not prove all requested views failed. Read back the view list and verify which views actually landed.
|
|
128
170
|
- In final user-facing summaries, distinguish clearly between:
|
|
171
|
+
- contract is visible / canonical shape is known
|
|
172
|
+
- plan succeeded
|
|
173
|
+
- apply landed and readback verified it
|
|
129
174
|
- base template/skeleton applied
|
|
130
175
|
- business-specific rules completed
|
|
131
176
|
- remaining gaps or follow-up patches
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Use this when the user wants one new app inside an existing package.
|
|
4
4
|
|
|
5
|
+
Do not use this playbook when the user is really asking for a system/package with multiple forms or modules. In that case:
|
|
6
|
+
|
|
7
|
+
1. resolve or create the package
|
|
8
|
+
2. create each app separately
|
|
9
|
+
3. attach each app to the package
|
|
10
|
+
4. add relation fields between apps
|
|
11
|
+
|
|
12
|
+
Hierarchy reminder:
|
|
13
|
+
|
|
14
|
+
- package -> app -> field -> relation
|
|
15
|
+
- another business object is another app, not a text field
|
|
16
|
+
|
|
5
17
|
If creating a brand new package would help, ask the user to confirm package creation first. After confirmation, call `package_create` before this sequence.
|
|
6
18
|
|
|
7
19
|
## Minimal sequence
|
|
@@ -13,6 +25,22 @@ If creating a brand new package would help, ask the user to confirm package crea
|
|
|
13
25
|
5. `package_attach_app`
|
|
14
26
|
6. `app_publish_verify` only when the user asks for explicit final verification
|
|
15
27
|
|
|
28
|
+
## Multi-app systems are different
|
|
29
|
+
|
|
30
|
+
If the user says things like:
|
|
31
|
+
|
|
32
|
+
- “创建一个完整应用包”
|
|
33
|
+
- “包含项目、需求、任务、缺陷、团队这几个表单”
|
|
34
|
+
- “这些表单之间建立关联关系”
|
|
35
|
+
|
|
36
|
+
then do not treat that as one app.
|
|
37
|
+
|
|
38
|
+
Use this pattern instead:
|
|
39
|
+
|
|
40
|
+
1. `package_resolve` or `package_create`
|
|
41
|
+
2. for each app name, run `app_schema_plan -> app_schema_apply -> package_attach_app`
|
|
42
|
+
3. once the apps exist, add `relation` fields between them
|
|
43
|
+
|
|
16
44
|
## Example
|
|
17
45
|
|
|
18
46
|
Create a new package only after the user confirms package creation:
|
|
@@ -110,3 +138,11 @@ The create route did not resolve in the current backend route context. Re-run `w
|
|
|
110
138
|
### `PACKAGE_ATTACH_FAILED`
|
|
111
139
|
|
|
112
140
|
Do not retry schema creation. Re-run only `package_attach_app`, then verify with `app_read_summary`.
|
|
141
|
+
|
|
142
|
+
### Hierarchy modeling mistake
|
|
143
|
+
|
|
144
|
+
If the user asked for several named forms/apps but the draft patch turns them into text fields inside one app, stop and remodel the task as:
|
|
145
|
+
|
|
146
|
+
- one package
|
|
147
|
+
- many apps
|
|
148
|
+
- relation fields between those apps
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
- Treat `package_attach_app` as the source of truth for package ownership
|
|
12
12
|
- Check `tag_ids_after` after schema writes
|
|
13
13
|
|
|
14
|
+
## Package vs app vs field
|
|
15
|
+
|
|
16
|
+
- A package/system is not an app
|
|
17
|
+
- Another app is not a field
|
|
18
|
+
- If the user names multiple forms/modules that relate to each other, create multiple apps and connect them with relation fields
|
|
19
|
+
- Do not use child app names like “项目/需求/任务/缺陷/团队” as plain text fields inside one app
|
|
20
|
+
|
|
14
21
|
## Auto publish
|
|
15
22
|
|
|
16
23
|
- `app_schema_apply`, `app_layout_apply`, `app_flow_apply`, and `app_views_apply` publish by default
|
|
@@ -32,6 +39,7 @@
|
|
|
32
39
|
- Prefer roles over explicit members unless the user explicitly names people
|
|
33
40
|
- Resolve assignees with `role_search` / `member_search` before flow apply
|
|
34
41
|
- Use `permissions.editable_fields` for node-level editable field permissions; do not guess field ids
|
|
42
|
+
- Preset node ids matter. When patching `basic_approval` or `basic_fill_then_approve`, reuse `approve_1` and `fill_1` unless you are explicitly replacing the skeleton.
|
|
35
43
|
- If `app_flow_plan` reports `FLOW_DEPENDENCY_MISSING`, fix schema first
|
|
36
44
|
- Do not switch to hidden `solution_*` tools from public builder flows
|
|
37
45
|
|
|
@@ -41,6 +49,10 @@
|
|
|
41
49
|
- Do not repeat create steps after `app_key` already exists
|
|
42
50
|
- For backend rejects, keep the retry narrow: retry only the failed tool, not the whole chain
|
|
43
51
|
- For `VALIDATION_ERROR`, do not keep guessing. Reuse `suggested_next_call`, `canonical_arguments`, `allowed_keys`, and `allowed_values` first.
|
|
52
|
+
- For flow work, do not replay internal keys from old logs or plan outputs. Public builder calls should stay on:
|
|
53
|
+
- `assignees.role_ids` / `assignees.member_uids` / `assignees.member_emails`
|
|
54
|
+
- `permissions.editable_fields`
|
|
55
|
+
- For view work, treat `columns` as the only canonical public key. `app_read_views_summary` and `app_views_plan/apply` should all be read and written in that shape.
|
|
44
56
|
- If a view or flow write fails, report the smallest next action:
|
|
45
57
|
- wrong key -> switch to canonical key
|
|
46
58
|
- unsupported preset -> switch to allowed canonical preset
|
|
@@ -8,6 +8,17 @@ Use the smallest v2 builder tool chain that can finish the task.
|
|
|
8
8
|
|
|
9
9
|
Use `plan` before `apply` unless the patch is trivial and already normalized.
|
|
10
10
|
|
|
11
|
+
## Hierarchy first
|
|
12
|
+
|
|
13
|
+
Before picking tools, decide which layer the request targets:
|
|
14
|
+
|
|
15
|
+
- `package`: a solution/app bundle like “研发项目管理” or “费控管理系统”
|
|
16
|
+
- `app`: one form/app inside that package
|
|
17
|
+
- `field`: one field inside one app
|
|
18
|
+
- `relation`: a field that links two apps
|
|
19
|
+
|
|
20
|
+
If the user asks for multiple forms/modules that relate to each other, this is a package-level multi-app task, not a single-app create.
|
|
21
|
+
|
|
11
22
|
## Resolve
|
|
12
23
|
|
|
13
24
|
- `package_create`: create a new package only after the user confirms package creation; exact-name duplicates return `noop=true`
|
|
@@ -57,22 +68,28 @@ These execute normalized patches and publish by default unless `publish=false`.
|
|
|
57
68
|
`package_resolve -> app_resolve -> app_schema_plan -> app_schema_apply -> package_attach_app`
|
|
58
69
|
- Create a brand new package, then create one app in it:
|
|
59
70
|
`package_create -> package_resolve -> app_schema_plan -> app_schema_apply -> package_attach_app`
|
|
71
|
+
- Create a brand new multi-app system/package:
|
|
72
|
+
`package_create/resolve -> per-app app_schema_plan/apply -> package_attach_app per app -> relation field patches`
|
|
60
73
|
- Update fields on an existing app:
|
|
61
74
|
`app_resolve -> app_read_fields -> app_schema_plan -> app_schema_apply`
|
|
62
75
|
- Tidy layout:
|
|
63
76
|
`app_read_layout_summary -> app_layout_plan -> app_layout_apply`
|
|
64
77
|
- Add workflow:
|
|
65
|
-
`app_read_fields -> app_read_flow_summary -> role_search/member_search -> app_flow_plan -> app_flow_apply`
|
|
78
|
+
`builder_tool_contract -> app_read_fields -> app_read_flow_summary -> role_search/member_search -> app_flow_plan -> app_flow_apply -> app_read_flow_summary`
|
|
66
79
|
- Add views:
|
|
67
|
-
`app_read_fields -> app_read_views_summary -> app_views_plan -> app_views_apply`
|
|
80
|
+
`builder_tool_contract -> app_read_fields -> app_read_views_summary -> app_views_plan -> app_views_apply -> app_read_views_summary`
|
|
68
81
|
|
|
69
82
|
## Avoid
|
|
70
83
|
|
|
71
84
|
- Do not handcraft raw Qingflow schema payloads
|
|
72
85
|
- Do not rely on internal `solution_*` tools in public builder flows
|
|
73
86
|
- Do not create a new package without first asking the user to confirm package creation
|
|
87
|
+
- Do not treat a package/system name as `app_name` when the user clearly wants multiple apps inside it
|
|
88
|
+
- Do not compress multiple business objects into one app with several text fields
|
|
74
89
|
- Do not skip summary reads before flow or view work
|
|
75
90
|
- Do not emit `column_names`; always use `columns`
|
|
91
|
+
- Do not reuse internal flow keys such as `role_entries` or `editable_que_ids` in public builder calls
|
|
76
92
|
- Do not pass natural-language preset guesses such as `default_approval`; map them to canonical preset values first
|
|
77
93
|
- Do not omit assignees on approval/fill/copy nodes
|
|
94
|
+
- Do not patch preset flows with brand new approval/fill node ids unless you are intentionally replacing the skeleton; reuse preset ids like `approve_1` and `fill_1`
|
|
78
95
|
- Do not guess role ids, member ids, or editable field ids; resolve names first
|
|
@@ -4,14 +4,21 @@ Use this when the app already exists and the task is only about workflow.
|
|
|
4
4
|
|
|
5
5
|
## Minimal sequence
|
|
6
6
|
|
|
7
|
-
1. `
|
|
8
|
-
2. `
|
|
9
|
-
3. `
|
|
10
|
-
4. `
|
|
11
|
-
5.
|
|
12
|
-
6.
|
|
13
|
-
7.
|
|
14
|
-
8.
|
|
7
|
+
1. `builder_tool_contract(tool_name="app_flow_plan")`
|
|
8
|
+
2. `app_read_fields`
|
|
9
|
+
3. `app_read_flow_summary`
|
|
10
|
+
4. `role_search` or `member_search`
|
|
11
|
+
5. `role_create` if the user wants a reusable directory role and no good role exists
|
|
12
|
+
6. start from a canonical preset when possible
|
|
13
|
+
7. patch the skeleton instead of freehanding a full graph
|
|
14
|
+
8. reuse preset node ids when patching:
|
|
15
|
+
- `basic_approval` -> patch `approve_1`
|
|
16
|
+
- `basic_fill_then_approve` -> patch `fill_1` and `approve_1`
|
|
17
|
+
Do not add a second approval/fill node with a new id unless you are intentionally replacing the skeleton.
|
|
18
|
+
The MCP now auto-aligns the simplest single-node preset overrides, but still prefer explicit preset ids so the merged graph stays predictable.
|
|
19
|
+
9. `app_flow_plan`
|
|
20
|
+
10. `app_flow_apply`
|
|
21
|
+
11. `app_read_flow_summary` when apply returns `partial_success` or the user asked for verification
|
|
15
22
|
|
|
16
23
|
If you are unsure about presets or node shapes, call `builder_tool_contract(tool_name="app_flow_apply")` before guessing.
|
|
17
24
|
|
|
@@ -142,23 +149,40 @@ Only after that should you use explicit nodes:
|
|
|
142
149
|
}
|
|
143
150
|
```
|
|
144
151
|
|
|
152
|
+
After `app_flow_plan` succeeds, prefer reusing its `suggested_next_call.arguments` directly. Do not rewrite the result into internal fields such as `role_entries` or `editable_que_ids`.
|
|
153
|
+
When you patch a preset, patch the preset node itself. Do not leave the preset approval node unassigned while adding a second custom approval node.
|
|
154
|
+
|
|
145
155
|
## Common failures
|
|
146
156
|
|
|
147
157
|
### `FLOW_ASSIGNEE_REQUIRED`
|
|
148
158
|
|
|
149
159
|
Approval, fill, and copy nodes must declare at least one assignee.
|
|
150
160
|
|
|
161
|
+
If this happens after using a preset, check for this specific mistake first:
|
|
162
|
+
|
|
163
|
+
- the preset created `approve_1` or `fill_1`
|
|
164
|
+
- your patch created a different node id instead of patching that preset node
|
|
165
|
+
- the original preset node remained in the graph without assignees
|
|
166
|
+
|
|
151
167
|
Preferred fix order:
|
|
152
168
|
|
|
153
169
|
1. `role_search`
|
|
154
170
|
2. `member_search` only if the user explicitly named members
|
|
155
171
|
3. `role_create` if the business needs a reusable role
|
|
156
|
-
4.
|
|
172
|
+
4. patch the preset node id itself with canonical `assignees.*`
|
|
173
|
+
5. retry `app_flow_plan` or `app_flow_apply`
|
|
157
174
|
|
|
158
175
|
### `FLOW_DEPENDENCY_MISSING`
|
|
159
176
|
|
|
160
177
|
The workflow depends on fields that do not exist yet, usually `status`. Fix schema first.
|
|
161
178
|
|
|
179
|
+
Preferred recovery:
|
|
180
|
+
|
|
181
|
+
1. use the returned `suggested_next_call`
|
|
182
|
+
2. apply the minimal schema patch
|
|
183
|
+
3. rerun `app_read_fields`
|
|
184
|
+
4. rerun `app_flow_plan`
|
|
185
|
+
|
|
162
186
|
### `INVALID_FLOW_EDGE`
|
|
163
187
|
|
|
164
188
|
One or more transitions reference unknown nodes or create an invalid graph.
|
|
@@ -193,6 +217,11 @@ Do not keep guessing preset names or node shapes. First:
|
|
|
193
217
|
- `assignees.member_names`
|
|
194
218
|
- `permissions.editable_fields`
|
|
195
219
|
|
|
220
|
+
Do not copy internal keys from old plan outputs or logs, including:
|
|
221
|
+
|
|
222
|
+
- `role_entries`
|
|
223
|
+
- `editable_que_ids`
|
|
224
|
+
|
|
196
225
|
## Notes
|
|
197
226
|
|
|
198
227
|
- `mode=replace` is the only supported flow apply mode
|
|
@@ -4,10 +4,12 @@ Use this when the task is only about table, card, board, or gantt views.
|
|
|
4
4
|
|
|
5
5
|
## Minimal sequence
|
|
6
6
|
|
|
7
|
-
1. `
|
|
8
|
-
2. `
|
|
9
|
-
3. `
|
|
10
|
-
4. `
|
|
7
|
+
1. `builder_tool_contract(tool_name="app_views_plan")`
|
|
8
|
+
2. `app_read_fields`
|
|
9
|
+
3. `app_read_views_summary`
|
|
10
|
+
4. `app_views_plan`
|
|
11
|
+
5. `app_views_apply`
|
|
12
|
+
6. `app_read_views_summary` again whenever apply returns `failed` or `partial_success`
|
|
11
13
|
|
|
12
14
|
If you are unsure about keys or view types, call `builder_tool_contract(tool_name="app_views_apply")` before guessing.
|
|
13
15
|
|
|
@@ -62,6 +64,8 @@ Apply it:
|
|
|
62
64
|
}
|
|
63
65
|
```
|
|
64
66
|
|
|
67
|
+
After `app_views_plan` succeeds, prefer reusing its `suggested_next_call.arguments` directly. Do not rewrite aliases back into non-canonical keys such as `column_names`.
|
|
68
|
+
|
|
65
69
|
Board example:
|
|
66
70
|
|
|
67
71
|
```json
|
|
@@ -146,12 +150,14 @@ Do not repeat `app_views_apply` with guessed keys. First:
|
|
|
146
150
|
|
|
147
151
|
1. check `suggested_next_call`
|
|
148
152
|
2. reuse `canonical_arguments` if present
|
|
149
|
-
3.
|
|
150
|
-
4.
|
|
153
|
+
3. call `app_read_views_summary` to see whether any requested views landed anyway
|
|
154
|
+
4. if needed, call `builder_tool_contract`
|
|
155
|
+
5. retry only the minimal failed view patch
|
|
151
156
|
|
|
152
157
|
## Notes
|
|
153
158
|
|
|
154
159
|
- `fields` is accepted as an alias for `columns`, but skill examples should still use `columns`
|
|
155
160
|
- `column_names` should not appear in skill examples
|
|
161
|
+
- `app_read_views_summary` should be treated as canonical readback and now returns `columns`
|
|
156
162
|
- `filters` are ANDed together as one flat condition group
|
|
157
163
|
- `app_views_apply` publishes by default
|
|
@@ -679,6 +679,58 @@ class AiBuilderFacade:
|
|
|
679
679
|
normalized_nodes.append(normalized_node)
|
|
680
680
|
return normalized_nodes, issues
|
|
681
681
|
|
|
682
|
+
def _canonicalize_flow_nodes_for_public_output(self, nodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
683
|
+
public_nodes: list[dict[str, Any]] = []
|
|
684
|
+
for node in nodes:
|
|
685
|
+
if not isinstance(node, dict):
|
|
686
|
+
continue
|
|
687
|
+
payload = deepcopy(node)
|
|
688
|
+
assignees = payload.get("assignees") if isinstance(payload.get("assignees"), dict) else {}
|
|
689
|
+
permissions = payload.get("permissions") if isinstance(payload.get("permissions"), dict) else {}
|
|
690
|
+
public_assignees: dict[str, Any] = {}
|
|
691
|
+
role_ids = [
|
|
692
|
+
role_id
|
|
693
|
+
for role_id in (
|
|
694
|
+
_coerce_positive_int(entry.get("roleId"))
|
|
695
|
+
for entry in (assignees.get("role_entries") or [])
|
|
696
|
+
if isinstance(entry, dict)
|
|
697
|
+
)
|
|
698
|
+
if role_id is not None
|
|
699
|
+
]
|
|
700
|
+
member_uids = [
|
|
701
|
+
member_uid
|
|
702
|
+
for member_uid in (_coerce_positive_int(value) for value in (assignees.get("member_uids") or []))
|
|
703
|
+
if member_uid is not None
|
|
704
|
+
]
|
|
705
|
+
if role_ids:
|
|
706
|
+
public_assignees["role_ids"] = role_ids
|
|
707
|
+
if member_uids:
|
|
708
|
+
public_assignees["member_uids"] = member_uids
|
|
709
|
+
if bool(assignees.get("include_sub_departs")):
|
|
710
|
+
public_assignees["include_sub_departs"] = True
|
|
711
|
+
public_permissions: dict[str, Any] = {}
|
|
712
|
+
editable_fields = [str(name) for name in (permissions.get("editable_fields") or []) if str(name or "").strip()]
|
|
713
|
+
if editable_fields:
|
|
714
|
+
public_permissions["editable_fields"] = editable_fields
|
|
715
|
+
config_payload = payload.get("config") if isinstance(payload.get("config"), dict) else {}
|
|
716
|
+
if isinstance(config_payload, dict):
|
|
717
|
+
config_payload = deepcopy(config_payload)
|
|
718
|
+
config_payload.pop("conditionFormatMatrix", None)
|
|
719
|
+
if public_assignees:
|
|
720
|
+
payload["assignees"] = public_assignees
|
|
721
|
+
else:
|
|
722
|
+
payload.pop("assignees", None)
|
|
723
|
+
if public_permissions:
|
|
724
|
+
payload["permissions"] = public_permissions
|
|
725
|
+
else:
|
|
726
|
+
payload.pop("permissions", None)
|
|
727
|
+
if config_payload:
|
|
728
|
+
payload["config"] = config_payload
|
|
729
|
+
else:
|
|
730
|
+
payload.pop("config", None)
|
|
731
|
+
public_nodes.append(payload)
|
|
732
|
+
return public_nodes
|
|
733
|
+
|
|
682
734
|
def package_attach_app(
|
|
683
735
|
self,
|
|
684
736
|
*,
|
|
@@ -1287,7 +1339,8 @@ class AiBuilderFacade:
|
|
|
1287
1339
|
if fields_result.get("status") == "failed":
|
|
1288
1340
|
return fields_result
|
|
1289
1341
|
current_fields = fields_result.get("fields", [])
|
|
1290
|
-
|
|
1342
|
+
normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
|
|
1343
|
+
public_nodes = self._canonicalize_flow_nodes_for_public_output(normalized_nodes)
|
|
1291
1344
|
if resolution_issues:
|
|
1292
1345
|
first_issue = resolution_issues[0]
|
|
1293
1346
|
suggested_call = None
|
|
@@ -1304,17 +1357,17 @@ class AiBuilderFacade:
|
|
|
1304
1357
|
"app_key": request.app_key,
|
|
1305
1358
|
"mode": str(request.mode or "replace"),
|
|
1306
1359
|
"preset": request.preset.value if request.preset else None,
|
|
1307
|
-
"nodes":
|
|
1360
|
+
"nodes": public_nodes,
|
|
1308
1361
|
"transitions": transitions,
|
|
1309
1362
|
},
|
|
1310
1363
|
details={"issues": resolution_issues},
|
|
1311
1364
|
suggested_next_call=suggested_call,
|
|
1312
1365
|
)
|
|
1313
1366
|
status_field_present = _infer_status_field_id(current_fields) is not None
|
|
1314
|
-
node_types = {str(node.get("type") or "") for node in
|
|
1367
|
+
node_types = {str(node.get("type") or "") for node in normalized_nodes}
|
|
1315
1368
|
assignee_required_nodes = [
|
|
1316
1369
|
node.get("id")
|
|
1317
|
-
for node in
|
|
1370
|
+
for node in normalized_nodes
|
|
1318
1371
|
if str(node.get("type") or "") in {"approve", "fill", "copy"}
|
|
1319
1372
|
and not (
|
|
1320
1373
|
(node.get("assignees") or {}).get("role_entries")
|
|
@@ -1329,7 +1382,7 @@ class AiBuilderFacade:
|
|
|
1329
1382
|
"app_key": request.app_key,
|
|
1330
1383
|
"mode": str(request.mode or "replace"),
|
|
1331
1384
|
"preset": request.preset.value if request.preset else None,
|
|
1332
|
-
"nodes":
|
|
1385
|
+
"nodes": public_nodes,
|
|
1333
1386
|
"transitions": transitions,
|
|
1334
1387
|
},
|
|
1335
1388
|
details={"node_ids": assignee_required_nodes, "policy": "prefer role assignees; explicit members are also supported"},
|
|
@@ -1342,7 +1395,7 @@ class AiBuilderFacade:
|
|
|
1342
1395
|
normalized_args={
|
|
1343
1396
|
"app_key": request.app_key,
|
|
1344
1397
|
"mode": str(request.mode or "replace"),
|
|
1345
|
-
"nodes":
|
|
1398
|
+
"nodes": public_nodes,
|
|
1346
1399
|
"transitions": transitions,
|
|
1347
1400
|
},
|
|
1348
1401
|
details={"missing_dependencies": ["status field"]},
|
|
@@ -1359,12 +1412,12 @@ class AiBuilderFacade:
|
|
|
1359
1412
|
},
|
|
1360
1413
|
},
|
|
1361
1414
|
)
|
|
1362
|
-
workflow = _build_public_workflow_spec(nodes=
|
|
1415
|
+
workflow = _build_public_workflow_spec(nodes=normalized_nodes, transitions=transitions)
|
|
1363
1416
|
if workflow.get("status") == "failed":
|
|
1364
1417
|
workflow["normalized_args"] = {
|
|
1365
1418
|
"app_key": request.app_key,
|
|
1366
1419
|
"mode": str(request.mode or "replace"),
|
|
1367
|
-
"nodes":
|
|
1420
|
+
"nodes": public_nodes,
|
|
1368
1421
|
"transitions": transitions,
|
|
1369
1422
|
}
|
|
1370
1423
|
workflow["suggested_next_call"] = {
|
|
@@ -1373,7 +1426,7 @@ class AiBuilderFacade:
|
|
|
1373
1426
|
"profile": profile,
|
|
1374
1427
|
"app_key": request.app_key,
|
|
1375
1428
|
"mode": "replace",
|
|
1376
|
-
"nodes":
|
|
1429
|
+
"nodes": public_nodes,
|
|
1377
1430
|
"transitions": transitions,
|
|
1378
1431
|
},
|
|
1379
1432
|
}
|
|
@@ -1381,7 +1434,7 @@ class AiBuilderFacade:
|
|
|
1381
1434
|
normalized_args = {
|
|
1382
1435
|
"app_key": request.app_key,
|
|
1383
1436
|
"mode": str(request.mode or "replace"),
|
|
1384
|
-
"nodes":
|
|
1437
|
+
"nodes": public_nodes,
|
|
1385
1438
|
"transitions": transitions,
|
|
1386
1439
|
}
|
|
1387
1440
|
return {
|
|
@@ -2030,7 +2083,9 @@ class AiBuilderFacade:
|
|
|
2030
2083
|
)
|
|
2031
2084
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
2032
2085
|
current_fields = _parse_schema(schema)["fields"]
|
|
2033
|
-
|
|
2086
|
+
normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
|
|
2087
|
+
public_nodes = self._canonicalize_flow_nodes_for_public_output(normalized_nodes)
|
|
2088
|
+
normalized_args["nodes"] = public_nodes
|
|
2034
2089
|
if resolution_issues:
|
|
2035
2090
|
first_issue = resolution_issues[0]
|
|
2036
2091
|
suggested_call = None
|
|
@@ -2049,7 +2104,7 @@ class AiBuilderFacade:
|
|
|
2049
2104
|
)
|
|
2050
2105
|
assignee_required_nodes = [
|
|
2051
2106
|
node.get("id")
|
|
2052
|
-
for node in
|
|
2107
|
+
for node in normalized_nodes
|
|
2053
2108
|
if str(node.get("type") or "") in {"approve", "fill", "copy"}
|
|
2054
2109
|
and not (
|
|
2055
2110
|
(node.get("assignees") or {}).get("role_entries")
|
|
@@ -2064,13 +2119,13 @@ class AiBuilderFacade:
|
|
|
2064
2119
|
details={"node_ids": assignee_required_nodes, "policy": "prefer role assignees; explicit members are also supported"},
|
|
2065
2120
|
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, "keyword": ""}},
|
|
2066
2121
|
)
|
|
2067
|
-
workflow_spec = _build_public_workflow_spec(nodes=
|
|
2122
|
+
workflow_spec = _build_public_workflow_spec(nodes=normalized_nodes, transitions=transitions)
|
|
2068
2123
|
if workflow_spec.get("status") == "failed":
|
|
2069
2124
|
workflow_spec["normalized_args"] = normalized_args
|
|
2070
2125
|
workflow_spec.setdefault("request_id", None)
|
|
2071
2126
|
workflow_spec["suggested_next_call"] = {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
|
|
2072
2127
|
return workflow_spec
|
|
2073
|
-
desired_node_count = len([node for node in
|
|
2128
|
+
desired_node_count = len([node for node in normalized_nodes if node.get("type") != "end"])
|
|
2074
2129
|
current_workflow, _workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
2075
2130
|
current_node_count = len(_summarize_workflow_nodes(current_workflow))
|
|
2076
2131
|
if current_node_count == desired_node_count and desired_node_count > 0:
|
|
@@ -2124,7 +2179,7 @@ class AiBuilderFacade:
|
|
|
2124
2179
|
arguments.setdefault("profile", profile)
|
|
2125
2180
|
arguments.setdefault("app_key", app_key)
|
|
2126
2181
|
arguments.setdefault("mode", mode)
|
|
2127
|
-
arguments.setdefault("nodes",
|
|
2182
|
+
arguments.setdefault("nodes", public_nodes)
|
|
2128
2183
|
arguments.setdefault("transitions", transitions)
|
|
2129
2184
|
suggested_next_call["arguments"] = arguments
|
|
2130
2185
|
failed["suggested_next_call"] = suggested_next_call
|
|
@@ -2216,14 +2271,17 @@ class AiBuilderFacade:
|
|
|
2216
2271
|
if isinstance(field, dict) and str(field.get("name") or "")
|
|
2217
2272
|
}
|
|
2218
2273
|
removed: list[str] = []
|
|
2274
|
+
view_results: list[dict[str, Any]] = []
|
|
2219
2275
|
for name in remove_views:
|
|
2220
2276
|
key = existing_by_name.get(name)
|
|
2221
2277
|
if key:
|
|
2222
2278
|
self.views.view_delete(profile=profile, viewgraph_key=key)
|
|
2223
2279
|
removed.append(name)
|
|
2224
2280
|
existing_by_name.pop(name, None)
|
|
2281
|
+
view_results.append({"name": name, "type": None, "status": "removed"})
|
|
2225
2282
|
created: list[str] = []
|
|
2226
2283
|
updated: list[str] = []
|
|
2284
|
+
failed_views: list[dict[str, Any]] = []
|
|
2227
2285
|
existing_view_list = [
|
|
2228
2286
|
view
|
|
2229
2287
|
for view in (existing_views if isinstance(existing_views, list) else [])
|
|
@@ -2301,6 +2359,7 @@ class AiBuilderFacade:
|
|
|
2301
2359
|
)
|
|
2302
2360
|
self.views.view_update(profile=profile, viewgraph_key=existing_key, payload=payload)
|
|
2303
2361
|
updated.append(patch.name)
|
|
2362
|
+
view_results.append({"name": patch.name, "type": patch.type.value, "status": "updated"})
|
|
2304
2363
|
else:
|
|
2305
2364
|
template_key = _pick_view_template_key(existing_view_list, desired_type=patch.type.value)
|
|
2306
2365
|
if patch.type.value == "table" and template_key:
|
|
@@ -2326,9 +2385,13 @@ class AiBuilderFacade:
|
|
|
2326
2385
|
)
|
|
2327
2386
|
self.views.view_create(profile=profile, payload=payload)
|
|
2328
2387
|
created.append(patch.name)
|
|
2388
|
+
view_results.append({"name": patch.name, "type": patch.type.value, "status": "created"})
|
|
2329
2389
|
except (QingflowApiError, RuntimeError) as error:
|
|
2330
2390
|
api_error = _coerce_api_error(error)
|
|
2331
|
-
|
|
2391
|
+
should_retry_minimal = api_error.backend_code == 48104 or (
|
|
2392
|
+
patch.type.value == "table" and bool(patch.filters) and api_error.http_status is not None and api_error.http_status >= 500
|
|
2393
|
+
)
|
|
2394
|
+
if should_retry_minimal:
|
|
2332
2395
|
try:
|
|
2333
2396
|
if existing_key or created_key:
|
|
2334
2397
|
target_key = created_key or existing_key or ""
|
|
@@ -2342,8 +2405,14 @@ class AiBuilderFacade:
|
|
|
2342
2405
|
self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
|
|
2343
2406
|
if existing_key:
|
|
2344
2407
|
updated.append(patch.name)
|
|
2408
|
+
view_results.append(
|
|
2409
|
+
{"name": patch.name, "type": patch.type.value, "status": "updated", "fallback_applied": True}
|
|
2410
|
+
)
|
|
2345
2411
|
else:
|
|
2346
2412
|
created.append(patch.name)
|
|
2413
|
+
view_results.append(
|
|
2414
|
+
{"name": patch.name, "type": patch.type.value, "status": "created", "fallback_applied": True}
|
|
2415
|
+
)
|
|
2347
2416
|
continue
|
|
2348
2417
|
fallback_payload = _build_minimal_view_payload(
|
|
2349
2418
|
app_key=app_key,
|
|
@@ -2354,6 +2423,7 @@ class AiBuilderFacade:
|
|
|
2354
2423
|
)
|
|
2355
2424
|
self.views.view_create(profile=profile, payload=fallback_payload)
|
|
2356
2425
|
created.append(patch.name)
|
|
2426
|
+
view_results.append({"name": patch.name, "type": patch.type.value, "status": "created", "fallback_applied": True})
|
|
2357
2427
|
continue
|
|
2358
2428
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
2359
2429
|
api_error = _coerce_api_error(fallback_error)
|
|
@@ -2362,11 +2432,17 @@ class AiBuilderFacade:
|
|
|
2362
2432
|
self.views.view_delete(profile=profile, viewgraph_key=created_key)
|
|
2363
2433
|
except Exception:
|
|
2364
2434
|
pass
|
|
2365
|
-
|
|
2366
|
-
"
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2435
|
+
failure_entry = {
|
|
2436
|
+
"name": patch.name,
|
|
2437
|
+
"type": patch.type.value,
|
|
2438
|
+
"status": "failed",
|
|
2439
|
+
"error_code": "VIEW_APPLY_FAILED",
|
|
2440
|
+
"message": _public_error_message("VIEW_APPLY_FAILED", api_error),
|
|
2441
|
+
"request_id": api_error.request_id,
|
|
2442
|
+
"backend_code": api_error.backend_code,
|
|
2443
|
+
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
2444
|
+
"operation": "update" if existing_key or created_key else "create",
|
|
2445
|
+
"details": {
|
|
2370
2446
|
"app_key": app_key,
|
|
2371
2447
|
"view_name": patch.name,
|
|
2372
2448
|
"view_type": patch.type.value,
|
|
@@ -2377,12 +2453,16 @@ class AiBuilderFacade:
|
|
|
2377
2453
|
"end_field": patch.end_field,
|
|
2378
2454
|
"title_field": patch.title_field,
|
|
2379
2455
|
"operation": "update" if existing_key or created_key else "create",
|
|
2456
|
+
"transport_error": {
|
|
2457
|
+
"http_status": api_error.http_status,
|
|
2458
|
+
"backend_code": api_error.backend_code,
|
|
2459
|
+
"category": api_error.category,
|
|
2460
|
+
},
|
|
2380
2461
|
},
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
)
|
|
2462
|
+
}
|
|
2463
|
+
failed_views.append(failure_entry)
|
|
2464
|
+
view_results.append(failure_entry)
|
|
2465
|
+
continue
|
|
2386
2466
|
try:
|
|
2387
2467
|
verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
2388
2468
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -2401,6 +2481,63 @@ class AiBuilderFacade:
|
|
|
2401
2481
|
}
|
|
2402
2482
|
verified = (not verified_views_unavailable) and all(name in verified_names for name in created + updated) and all(name not in verified_names for name in removed)
|
|
2403
2483
|
noop = not created and not updated and not removed
|
|
2484
|
+
if failed_views:
|
|
2485
|
+
successful_changes = bool(created or updated or removed)
|
|
2486
|
+
verification_by_view = []
|
|
2487
|
+
for item in view_results:
|
|
2488
|
+
if item.get("status") in {"created", "updated"}:
|
|
2489
|
+
verification_by_view.append(
|
|
2490
|
+
{
|
|
2491
|
+
"name": item.get("name"),
|
|
2492
|
+
"type": item.get("type"),
|
|
2493
|
+
"status": item.get("status"),
|
|
2494
|
+
"present_in_readback": None if verified_views_unavailable else item.get("name") in verified_names,
|
|
2495
|
+
}
|
|
2496
|
+
)
|
|
2497
|
+
elif item.get("status") == "removed":
|
|
2498
|
+
verification_by_view.append(
|
|
2499
|
+
{
|
|
2500
|
+
"name": item.get("name"),
|
|
2501
|
+
"type": item.get("type"),
|
|
2502
|
+
"status": "removed",
|
|
2503
|
+
"present_in_readback": None if verified_views_unavailable else item.get("name") not in verified_names,
|
|
2504
|
+
}
|
|
2505
|
+
)
|
|
2506
|
+
else:
|
|
2507
|
+
verification_by_view.append(
|
|
2508
|
+
{
|
|
2509
|
+
"name": item.get("name"),
|
|
2510
|
+
"type": item.get("type"),
|
|
2511
|
+
"status": "failed",
|
|
2512
|
+
"present_in_readback": None,
|
|
2513
|
+
"error_code": item.get("error_code"),
|
|
2514
|
+
}
|
|
2515
|
+
)
|
|
2516
|
+
first_failure = failed_views[0]
|
|
2517
|
+
response = {
|
|
2518
|
+
"status": "partial_success" if successful_changes else "failed",
|
|
2519
|
+
"error_code": "VIEW_APPLY_PARTIAL" if successful_changes else "VIEW_APPLY_FAILED",
|
|
2520
|
+
"recoverable": True,
|
|
2521
|
+
"message": "applied some view patches; at least one view failed" if successful_changes else "one or more view patches failed",
|
|
2522
|
+
"normalized_args": normalized_args,
|
|
2523
|
+
"missing_fields": [],
|
|
2524
|
+
"allowed_values": {"view_types": [member.value for member in PublicViewType], "view.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
2525
|
+
"details": {"per_view_results": view_results},
|
|
2526
|
+
"request_id": first_failure.get("request_id"),
|
|
2527
|
+
"suggested_next_call": {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2528
|
+
"backend_code": first_failure.get("backend_code"),
|
|
2529
|
+
"http_status": first_failure.get("http_status"),
|
|
2530
|
+
"noop": noop,
|
|
2531
|
+
"verification": {
|
|
2532
|
+
"views_verified": verified,
|
|
2533
|
+
"views_read_unavailable": verified_views_unavailable,
|
|
2534
|
+
"by_view": verification_by_view,
|
|
2535
|
+
},
|
|
2536
|
+
"app_key": app_key,
|
|
2537
|
+
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
2538
|
+
"verified": verified,
|
|
2539
|
+
}
|
|
2540
|
+
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
2404
2541
|
response = {
|
|
2405
2542
|
"status": "success" if verified else "partial_success",
|
|
2406
2543
|
"error_code": None if not verified_views_unavailable else "VIEWS_READBACK_PENDING",
|
|
@@ -2413,9 +2550,9 @@ class AiBuilderFacade:
|
|
|
2413
2550
|
"request_id": None,
|
|
2414
2551
|
"suggested_next_call": None if not verified_views_unavailable else {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2415
2552
|
"noop": noop,
|
|
2416
|
-
"verification": {"views_verified": verified, "views_read_unavailable": verified_views_unavailable},
|
|
2553
|
+
"verification": {"views_verified": verified, "views_read_unavailable": verified_views_unavailable, "by_view": view_results},
|
|
2417
2554
|
"app_key": app_key,
|
|
2418
|
-
"views_diff": {"created": created, "updated": updated, "removed": removed},
|
|
2555
|
+
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
2419
2556
|
"verified": verified,
|
|
2420
2557
|
}
|
|
2421
2558
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
@@ -3487,6 +3624,11 @@ def _merge_flow_graph(
|
|
|
3487
3624
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
3488
3625
|
if not override_nodes and not override_transitions:
|
|
3489
3626
|
return deepcopy(base_nodes), deepcopy(base_transitions)
|
|
3627
|
+
override_nodes, override_transitions = _align_flow_preset_override_ids(
|
|
3628
|
+
base_nodes=base_nodes,
|
|
3629
|
+
override_nodes=override_nodes,
|
|
3630
|
+
override_transitions=override_transitions,
|
|
3631
|
+
)
|
|
3490
3632
|
merged_nodes: list[dict[str, Any]] = []
|
|
3491
3633
|
override_map = {
|
|
3492
3634
|
str(node.get("id") or ""): deepcopy(node)
|
|
@@ -3517,6 +3659,72 @@ def _merge_flow_graph(
|
|
|
3517
3659
|
return merged_nodes, merged_transitions
|
|
3518
3660
|
|
|
3519
3661
|
|
|
3662
|
+
def _align_flow_preset_override_ids(
|
|
3663
|
+
*,
|
|
3664
|
+
base_nodes: list[dict[str, Any]],
|
|
3665
|
+
override_nodes: list[dict[str, Any]],
|
|
3666
|
+
override_transitions: list[dict[str, Any]],
|
|
3667
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
3668
|
+
"""Map simple preset skeleton overrides back onto canonical preset node ids.
|
|
3669
|
+
|
|
3670
|
+
This keeps preset-based plans stable when the caller customizes the single
|
|
3671
|
+
approval/fill/copy step semantically but forgets to reuse ids such as
|
|
3672
|
+
`approve_1` or `fill_1`.
|
|
3673
|
+
"""
|
|
3674
|
+
patchable_types = {"approve", "fill", "copy"}
|
|
3675
|
+
base_by_type: dict[str, list[str]] = {}
|
|
3676
|
+
override_by_type: dict[str, list[str]] = {}
|
|
3677
|
+
for node in base_nodes:
|
|
3678
|
+
if not isinstance(node, dict):
|
|
3679
|
+
continue
|
|
3680
|
+
node_type = str(node.get("type") or "")
|
|
3681
|
+
node_id = str(node.get("id") or "")
|
|
3682
|
+
if node_type in patchable_types and node_id:
|
|
3683
|
+
base_by_type.setdefault(node_type, []).append(node_id)
|
|
3684
|
+
for node in override_nodes:
|
|
3685
|
+
if not isinstance(node, dict):
|
|
3686
|
+
continue
|
|
3687
|
+
node_type = str(node.get("type") or "")
|
|
3688
|
+
node_id = str(node.get("id") or "")
|
|
3689
|
+
if node_type in patchable_types and node_id:
|
|
3690
|
+
override_by_type.setdefault(node_type, []).append(node_id)
|
|
3691
|
+
replacement_map: dict[str, str] = {}
|
|
3692
|
+
for node_type in patchable_types:
|
|
3693
|
+
base_ids = base_by_type.get(node_type) or []
|
|
3694
|
+
override_ids = override_by_type.get(node_type) or []
|
|
3695
|
+
if len(base_ids) != 1 or len(override_ids) != 1:
|
|
3696
|
+
continue
|
|
3697
|
+
base_id = base_ids[0]
|
|
3698
|
+
override_id = override_ids[0]
|
|
3699
|
+
if override_id == base_id:
|
|
3700
|
+
continue
|
|
3701
|
+
replacement_map[override_id] = base_id
|
|
3702
|
+
if not replacement_map:
|
|
3703
|
+
return deepcopy(override_nodes), deepcopy(override_transitions)
|
|
3704
|
+
aligned_nodes: list[dict[str, Any]] = []
|
|
3705
|
+
for node in override_nodes:
|
|
3706
|
+
if not isinstance(node, dict):
|
|
3707
|
+
continue
|
|
3708
|
+
aligned = deepcopy(node)
|
|
3709
|
+
node_id = str(aligned.get("id") or "")
|
|
3710
|
+
if node_id in replacement_map:
|
|
3711
|
+
aligned["id"] = replacement_map[node_id]
|
|
3712
|
+
aligned_nodes.append(aligned)
|
|
3713
|
+
aligned_transitions: list[dict[str, Any]] = []
|
|
3714
|
+
for transition in override_transitions:
|
|
3715
|
+
if not isinstance(transition, dict):
|
|
3716
|
+
continue
|
|
3717
|
+
aligned = deepcopy(transition)
|
|
3718
|
+
source = str(aligned.get("from") or "")
|
|
3719
|
+
target = str(aligned.get("to") or "")
|
|
3720
|
+
if source in replacement_map:
|
|
3721
|
+
aligned["from"] = replacement_map[source]
|
|
3722
|
+
if target in replacement_map:
|
|
3723
|
+
aligned["to"] = replacement_map[target]
|
|
3724
|
+
aligned_transitions.append(aligned)
|
|
3725
|
+
return aligned_nodes, aligned_transitions
|
|
3726
|
+
|
|
3727
|
+
|
|
3520
3728
|
def _extract_directory_items(listed: JSONObject) -> list[dict[str, Any]]:
|
|
3521
3729
|
if isinstance(listed.get("items"), list):
|
|
3522
3730
|
return [item for item in listed["items"] if isinstance(item, dict)]
|
|
@@ -3694,13 +3902,20 @@ def _summarize_views(result: Any) -> list[dict[str, Any]]:
|
|
|
3694
3902
|
for view in result:
|
|
3695
3903
|
if not isinstance(view, dict):
|
|
3696
3904
|
continue
|
|
3905
|
+
name = view.get("viewgraphName") or view.get("viewName") or view.get("title")
|
|
3906
|
+
view_key = view.get("viewgraphKey") or view.get("viewKey")
|
|
3907
|
+
view_type = _normalize_view_type_name(view.get("viewgraphType") or view.get("type"))
|
|
3908
|
+
columns = view.get("columnNames") or view.get("columns") or []
|
|
3909
|
+
group_by = view.get("groupBy") or view.get("group_by")
|
|
3910
|
+
if not any((name, view_key, view_type, columns, group_by)):
|
|
3911
|
+
continue
|
|
3697
3912
|
items.append(
|
|
3698
3913
|
{
|
|
3699
|
-
"name":
|
|
3700
|
-
"view_key":
|
|
3701
|
-
"type":
|
|
3702
|
-
"
|
|
3703
|
-
"group_by":
|
|
3914
|
+
"name": name,
|
|
3915
|
+
"view_key": view_key,
|
|
3916
|
+
"type": view_type,
|
|
3917
|
+
"columns": columns,
|
|
3918
|
+
"group_by": group_by,
|
|
3704
3919
|
}
|
|
3705
3920
|
)
|
|
3706
3921
|
return items
|
|
@@ -4080,6 +4295,8 @@ def _pick_view_template_key(existing_views: list[dict[str, Any]], *, desired_typ
|
|
|
4080
4295
|
|
|
4081
4296
|
def _normalize_view_type_name(value: Any) -> str:
|
|
4082
4297
|
normalized = str(value or "").strip().lower()
|
|
4298
|
+
if not normalized:
|
|
4299
|
+
return ""
|
|
4083
4300
|
if "gantt" in normalized:
|
|
4084
4301
|
return "gantt"
|
|
4085
4302
|
if "board" in normalized:
|
|
@@ -1319,6 +1319,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
1319
1319
|
"node.type": [member.value for member in PublicFlowNodeType],
|
|
1320
1320
|
"node.condition.operator": [member.value for member in FlowConditionOperator],
|
|
1321
1321
|
},
|
|
1322
|
+
"dependency_hints": [
|
|
1323
|
+
"approval-style workflows require an explicit status field",
|
|
1324
|
+
"approve/fill/copy nodes require at least one assignee",
|
|
1325
|
+
],
|
|
1322
1326
|
"minimal_example": {
|
|
1323
1327
|
"profile": "default",
|
|
1324
1328
|
"app_key": "APP_KEY",
|
|
@@ -1396,6 +1400,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
1396
1400
|
"node.type": [member.value for member in PublicFlowNodeType],
|
|
1397
1401
|
"node.condition.operator": [member.value for member in FlowConditionOperator],
|
|
1398
1402
|
},
|
|
1403
|
+
"dependency_hints": [
|
|
1404
|
+
"approval-style workflows require an explicit status field",
|
|
1405
|
+
"approve/fill/copy nodes require at least one assignee",
|
|
1406
|
+
],
|
|
1399
1407
|
"minimal_example": {
|
|
1400
1408
|
"profile": "default",
|
|
1401
1409
|
"app_key": "APP_KEY",
|
|
@@ -1516,6 +1524,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
1516
1524
|
"view.type": [member.value for member in PublicViewType],
|
|
1517
1525
|
"view.filter.operator": [member.value for member in ViewFilterOperator],
|
|
1518
1526
|
},
|
|
1527
|
+
"execution_notes": [
|
|
1528
|
+
"apply may return partial_success when some views land and others fail",
|
|
1529
|
+
"read back app_read_views_summary after any failed or partial view apply",
|
|
1530
|
+
],
|
|
1519
1531
|
"minimal_example": {
|
|
1520
1532
|
"profile": "default",
|
|
1521
1533
|
"app_key": "APP_KEY",
|