@josephyan/qingflow-cli 1.1.4 → 1.1.6
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 +7 -3
- package/docs/local-agent-install.md +57 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/bin/qingflow.mjs +1 -34
- package/npm/lib/runtime.mjs +21 -101
- package/npm/scripts/postinstall.mjs +1 -10
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +58 -44
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
- package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
- package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
- package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
- package/skills/qingflow-cli/reference/builder/README.md +41 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
- package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
- package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
- package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
- package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
- package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
- package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
- package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
- package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
- package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
- package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
- package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
- package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
- package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
- package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
- package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
- package/skills/qingflow-mcp-setup/SKILL.md +115 -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 +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +282 -102
- package/src/qingflow_mcp/builder_facade/service.py +4192 -935
- package/src/qingflow_mcp/cli/commands/builder.py +316 -298
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- 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 +101 -27
- package/src/qingflow_mcp/cli/commands/task.py +28 -47
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +784 -16
- package/src/qingflow_mcp/cli/main.py +117 -33
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +26 -17
- package/src/qingflow_mcp/response_trim.py +81 -17
- package/src/qingflow_mcp/server.py +14 -12
- package/src/qingflow_mcp/server_app_builder.py +65 -21
- package/src/qingflow_mcp/server_app_user.py +22 -16
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/executor.py +245 -18
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +197 -35
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +244 -34
- package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
- package/src/qingflow_mcp/tools/file_tools.py +9 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +1141 -356
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +59 -45
- package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
- /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
- /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
本文与 **MCP / 服务端公开工具命名**对齐,并给出已安装的 `**qingflow` CLI** 等价命令。编写前已在当前环境对 `**task list` / `task get` / `task log`** 实跑;`**task report`** 在抽样任务上遇到过后端 `**40038` Object not exist**(见文末「实测排障」),属数据/权限/报表生命周期问题,**不否定**标准编排顺序。
|
|
4
4
|
|
|
5
|
-
**必须落盘**:`task` 的 `list`、`get`、`log`、`report` 以及 `**task action` 的响应**若体积大,均应 `> tmp/qingflow_*.json`,与主技能 [SKILL.md](
|
|
5
|
+
**必须落盘**:`task` 的 `list`、`get`、`log`、`report` 以及 `**task action` 的响应**若体积大,均应 `> tmp/qingflow_*.json`,与主技能 [SKILL.md](../../SKILL.md) 一致。
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -106,7 +106,7 @@ qingflow --json task action \
|
|
|
106
106
|
> tmp/qingflow_task_action.json
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
`**--action` 允许字面量**(实现校验,大小写不敏感):
|
|
109
|
+
`**--action` 允许字面量**(实现校验,大小写不敏感):
|
|
110
110
|
`approve`、`reject`、`rollback`、`transfer`、`urge`、`save_only`。
|
|
111
111
|
|
|
112
112
|
**约束摘要**(与实现一致):
|
|
@@ -126,7 +126,7 @@ qingflow --json task action \
|
|
|
126
126
|
## 4. 与「应用内 `record list`」的边界
|
|
127
127
|
|
|
128
128
|
- **任务中心待办 / 已办 / 抄送**:用 `**task list --task-box …`**,**不要**再用过时的 `record list --view-id system:todo` 等当作任务箱。
|
|
129
|
-
- **业务表读数**:仍在 `app get` 的 `**accessible_views`** 里选 `**custom:`* 等**,走 [QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md](
|
|
129
|
+
- **业务表读数**:仍在 `app get` 的 `**accessible_views`** 里选 `**custom:`* 等**,走 [QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md](../core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md)。
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
@@ -147,5 +147,5 @@ qingflow --json task action \
|
|
|
147
147
|
|
|
148
148
|
## 6. 交叉引用
|
|
149
149
|
|
|
150
|
-
- 主技能 [SKILL.md](
|
|
151
|
-
- [QINGFLOW_CLI_MEMBER_CHEATSHEET.md](
|
|
150
|
+
- 主技能 [SKILL.md](../../SKILL.md):**落盘规则**、`**task-box` / `flow-status`** 枚举、与 `record` 混用雷区。
|
|
151
|
+
- [QINGFLOW_CLI_MEMBER_CHEATSHEET.md](../core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md):成员侧最短路径。
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Qingflow CLI Task Ops
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This section is for task workflow operations only. Do not use it for record CRUD or final statistical analysis.
|
|
6
|
+
|
|
7
|
+
## Default Paths
|
|
8
|
+
|
|
9
|
+
Use exactly one of these default paths:
|
|
10
|
+
|
|
11
|
+
1. Find target todos
|
|
12
|
+
`qingflow task list`
|
|
13
|
+
|
|
14
|
+
2. Read one task context
|
|
15
|
+
`task list -> exact target -> task get`
|
|
16
|
+
|
|
17
|
+
3. Read associated approval context
|
|
18
|
+
`task get -> task report` or `task log`
|
|
19
|
+
|
|
20
|
+
4. Execute workflow action
|
|
21
|
+
`task list -> exact target -> task get -> task action`
|
|
22
|
+
|
|
23
|
+
5. Execute a user-specified action on an already-clear target
|
|
24
|
+
`task list -> exact target -> (optional task get) -> task action`
|
|
25
|
+
|
|
26
|
+
## Core Tools
|
|
27
|
+
|
|
28
|
+
- `qingflow task list`
|
|
29
|
+
- `qingflow task get`
|
|
30
|
+
- `qingflow task action`
|
|
31
|
+
- `qingflow task report`
|
|
32
|
+
- `qingflow task log`
|
|
33
|
+
|
|
34
|
+
## Supporting Tools
|
|
35
|
+
|
|
36
|
+
- `app_list`
|
|
37
|
+
- `app_search`
|
|
38
|
+
|
|
39
|
+
## Standard Operating Order
|
|
40
|
+
|
|
41
|
+
Use one of these two modes:
|
|
42
|
+
|
|
43
|
+
1. Recommendation mode
|
|
44
|
+
1. Discover the exact target with `task list`
|
|
45
|
+
2. Read node context with `task get`
|
|
46
|
+
3. Before giving any approval recommendation, read `task log`
|
|
47
|
+
4. If `task get` returns any `associated_reports`, read every visible report through `task report`
|
|
48
|
+
5. Give a recommendation only after reviewing node context, workflow log, and associated reports
|
|
49
|
+
6. Wait for explicit user confirmation before `task action`
|
|
50
|
+
|
|
51
|
+
2. User-directed execution mode
|
|
52
|
+
1. Discover the exact target with `task list`
|
|
53
|
+
2. If the target or action requirements are ambiguous, read `task get`; otherwise go straight to `task action`
|
|
54
|
+
3. Execute through `task action --task-id ...`
|
|
55
|
+
4. After actions, report the exact `task_id`, executed action, and any returned `app_key / record_id` plus warnings
|
|
56
|
+
|
|
57
|
+
## Task-Center Rules
|
|
58
|
+
|
|
59
|
+
- Use `task list` for flat browsing
|
|
60
|
+
- `task_box` must be one of:
|
|
61
|
+
- `todo`
|
|
62
|
+
- `initiated`
|
|
63
|
+
- `cc`
|
|
64
|
+
- `done`
|
|
65
|
+
- `flow_status` must be one of:
|
|
66
|
+
- `all`
|
|
67
|
+
- `in_progress`
|
|
68
|
+
- `approved`
|
|
69
|
+
- `rejected`
|
|
70
|
+
- `pending_fix`
|
|
71
|
+
- `urged`
|
|
72
|
+
- `overdue`
|
|
73
|
+
- `due_soon`
|
|
74
|
+
- `unread`
|
|
75
|
+
- `ended`
|
|
76
|
+
- `task list` is the only public task discovery path
|
|
77
|
+
- `task list --query` uses backend `searchKey` first; only when backend returns zero rows does CLI apply a local fallback match on normalized `app_name / workflow_node_name / app_key / record_id`
|
|
78
|
+
- Treat `task_id` from `task list.data.items[]` as the public action locator. Do not reconstruct action identity from `app_key + record_id + workflow_node_id`.
|
|
79
|
+
- Default box usage:
|
|
80
|
+
- `todo`: `task list -> task get -> task log / task report -> recommendation -> explicit user confirmation -> task action`
|
|
81
|
+
- `initiated`: `task list -> record get`
|
|
82
|
+
- `done`: `task list -> record get`
|
|
83
|
+
- `cc`: `task list -> record get`
|
|
84
|
+
- Treat `initiated`, `done`, and `cc` primarily as list-plus-record-detail flows, not task action flows
|
|
85
|
+
|
|
86
|
+
## Workflow Usage Actions
|
|
87
|
+
|
|
88
|
+
- `task get.capabilities.available_actions` is the source of truth for executable actions
|
|
89
|
+
- Current public actions are:
|
|
90
|
+
- `approve`
|
|
91
|
+
- `reject`
|
|
92
|
+
- `rollback`
|
|
93
|
+
- `transfer`
|
|
94
|
+
- `urge`
|
|
95
|
+
- `save_only`
|
|
96
|
+
- Before any approve/reject/rollback/transfer recommendation, always review `task log` when `task get.visibility.audit_record_visible=true`
|
|
97
|
+
- If `task get` returns visible `associated_reports`, review each one with `task report`; do not rely on report summary alone
|
|
98
|
+
- Do not give an approval recommendation based only on `task get`
|
|
99
|
+
- Do not execute `task action` until the user explicitly confirms the chosen action
|
|
100
|
+
- Exception: if the user has already explicitly authorized a concrete action on exact targets, you may execute directly after exact target resolution
|
|
101
|
+
- Avoid actions on ambiguous tasks or records
|
|
102
|
+
- Summarize the final action and the exact `task_id`
|
|
103
|
+
- `reject` requires `payload.audit_feedback`
|
|
104
|
+
- `save_only` requires non-empty `fields` and is only available when the backend exposes editable fields for the current node
|
|
105
|
+
- `task action` distinguishes action execution from workflow continuation. Read `verification.runtime_continuation_verified` before claiming the workflow actually moved on.
|
|
106
|
+
- If `task action` returns `partial_success` with `WORKFLOW_CONTINUATION_UNVERIFIED`, report the action as sent but the downstream continuation as unverified.
|
|
107
|
+
- If `task action` returns `TASK_CONTEXT_VISIBILITY_UNVERIFIED` after a `46001`-style context loss, do not claim the task was already processed unless the workflow log or record state proves it.
|
|
108
|
+
- If `task_action_execute` returns `TASK_RUNTIME_CONSUMED_AFTER_ACTION`, treat that as a normal post-success state: the current node runtime was consumed, the workflow likely continued, and `46001` does not by itself mean the action failed
|
|
109
|
+
|
|
110
|
+
## Feedback Escalation
|
|
111
|
+
|
|
112
|
+
- If task capabilities, associated report detail, workflow log visibility, or action support still cannot satisfy the user's goal after reasonable use of this skill, summarize the exact gap in plain language.
|
|
113
|
+
- Ask whether the user wants you to submit product feedback.
|
|
114
|
+
- Only after explicit user confirmation, call `feedback_submit`.
|
|
115
|
+
|
|
116
|
+
## Response Interpretation
|
|
117
|
+
|
|
118
|
+
- `task_list` returns normalized todo rows and is the only default discovery path
|
|
119
|
+
- `task_list` may return `TASK_LIST_QUERY_FALLBACK_APPLIED`; this means backend search missed the query and MCP recovered the result through local exact-field fallback
|
|
120
|
+
- `task_get` returns node context summary, not full historical report data
|
|
121
|
+
- `task_associated_report_detail_get` may return either:
|
|
122
|
+
- `result_type=view_list`
|
|
123
|
+
- `result_type=chart_data`
|
|
124
|
+
- `task_workflow_log_get` returns workflow log detail only when the node grants log visibility
|
|
125
|
+
- A successful approve/reject/rollback/transfer may still lose the current-node runtime immediately; treat `record_state_readable=false + backend 46001` as a post-action runtime loss unless continuation verification says otherwise
|
|
126
|
+
- Treat `request_route` as the source of truth for live route debugging
|
|
127
|
+
- If only part of the requested work is completed, explicitly disclose which parts are done and which are not
|
|
128
|
+
|
|
129
|
+
## Resources
|
|
130
|
+
|
|
131
|
+
- Workflow and task usage actions: [references/workflow-usage.md](./workflow-usage.md)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Environment Switching
|
|
2
|
+
|
|
3
|
+
Use this reference before any workflow usage action, comment, or task-center operation that might affect live work.
|
|
4
|
+
|
|
5
|
+
## Step 1: Resolve the active environment
|
|
6
|
+
|
|
7
|
+
Decide explicitly whether the task targets:
|
|
8
|
+
|
|
9
|
+
- `test`: demo, mock data, smoke usage validation, training scenarios
|
|
10
|
+
- `prod`: real operational tasks, comments, and workflow actions
|
|
11
|
+
|
|
12
|
+
If the user did not specify an environment, default to `prod`.
|
|
13
|
+
|
|
14
|
+
## Test Environment
|
|
15
|
+
|
|
16
|
+
Use test for:
|
|
17
|
+
|
|
18
|
+
- workflow walkthroughs
|
|
19
|
+
- user acceptance demos
|
|
20
|
+
- comment or transfer rehearsals
|
|
21
|
+
|
|
22
|
+
## Production Environment
|
|
23
|
+
|
|
24
|
+
Use production for:
|
|
25
|
+
|
|
26
|
+
- live task-center operations
|
|
27
|
+
- live comments on real business records
|
|
28
|
+
- approve / reject / rollback / transfer / urge on real work
|
|
29
|
+
|
|
30
|
+
Production guardrails:
|
|
31
|
+
|
|
32
|
+
- never assume a task id, record id, or workflow node id
|
|
33
|
+
- find the exact target first
|
|
34
|
+
- if the task can be answered read-only, do not act
|
|
35
|
+
|
|
36
|
+
## Reporting Rule
|
|
37
|
+
|
|
38
|
+
For task ops, always report:
|
|
39
|
+
|
|
40
|
+
- active environment
|
|
41
|
+
- target app or task box
|
|
42
|
+
- operation type: read, comment, approve, reject, rollback, transfer, urge, or mark_read
|
|
43
|
+
- affected task ids or record ids
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Workflow and Task Usage Actions
|
|
2
|
+
|
|
3
|
+
Use these when the user is operating inside an existing process, not redesigning it.
|
|
4
|
+
|
|
5
|
+
Examples:
|
|
6
|
+
|
|
7
|
+
- add a comment to a record
|
|
8
|
+
- approve or reject a workflow task
|
|
9
|
+
- transfer a task
|
|
10
|
+
- roll back a task
|
|
11
|
+
- list todo, initiated, done, or cc tasks
|
|
12
|
+
- inspect workload by worksheet or workflow node
|
|
13
|
+
- urge a pending task
|
|
14
|
+
|
|
15
|
+
Rules:
|
|
16
|
+
|
|
17
|
+
- if the user starts from inbox, todo, workload, cc, or bottleneck language, use `task_*` first
|
|
18
|
+
- use `task_summary` for headline counts
|
|
19
|
+
- use `task_list` for flat browsing
|
|
20
|
+
- use `task_facets` when worksheet or workflow-node buckets matter
|
|
21
|
+
- treat task counts as task-center counts, not record counts
|
|
22
|
+
- switch to `record_get` only after locating the exact business record behind a task
|
|
23
|
+
- identify the exact target first
|
|
24
|
+
- for approve or reject, identify the exact `workflow_node_id` first; prefer task-center results or current audit info, then use `task_approve` or `task_reject`
|
|
25
|
+
- avoid usage-side workflow actions on ambiguous records
|
|
26
|
+
- summarize the final action and target task ids or record ids
|
|
@@ -0,0 +1,124 @@
|
|
|
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())
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
对比两个工作流 spec 的差异,输出新增/删除/修改的节点和边。
|
|
4
|
+
用于更新模式下辅助 agent 判断是否遵循最小修改原则。
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
python3 diff_flow_spec.py <old_spec.json> <new_spec.json>
|
|
8
|
+
python3 diff_flow_spec.py <old_spec.json> <new_spec.json> --json # JSON 输出
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_json(path):
|
|
16
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
17
|
+
return json.load(f)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def extract_edges(spec):
|
|
21
|
+
"""从 spec 中提取边列表,兼容 edges 在不同层级的情况"""
|
|
22
|
+
e = spec.get('edges', [])
|
|
23
|
+
if isinstance(e, dict) and 'edges' in e:
|
|
24
|
+
return e['edges']
|
|
25
|
+
if isinstance(e, list):
|
|
26
|
+
return e
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def node_key(n):
|
|
31
|
+
"""节点的唯一标识"""
|
|
32
|
+
return n.get('id', '')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def edge_key(e):
|
|
36
|
+
"""边的唯一标识:from→to"""
|
|
37
|
+
return (e.get('from', ''), e.get('to', ''))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def node_identity(n):
|
|
41
|
+
"""节点的完整内容(不含 id,用于判断是否修改)"""
|
|
42
|
+
return {
|
|
43
|
+
'type': n.get('type', ''),
|
|
44
|
+
'name': n.get('name', ''),
|
|
45
|
+
'attrs': n.get('attrs', {}),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def edge_identity(e):
|
|
50
|
+
"""边的完整内容(不含 from/to,用于判断是否修改)"""
|
|
51
|
+
return {
|
|
52
|
+
'label': e.get('label', ''),
|
|
53
|
+
'condition': e.get('condition', {}),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def diff_nodes(old_nodes, new_nodes):
|
|
58
|
+
"""对比节点差异"""
|
|
59
|
+
old_by_id = {node_key(n): n for n in old_nodes}
|
|
60
|
+
new_by_id = {node_key(n): n for n in new_nodes}
|
|
61
|
+
|
|
62
|
+
old_ids = set(old_by_id.keys())
|
|
63
|
+
new_ids = set(new_by_id.keys())
|
|
64
|
+
|
|
65
|
+
deleted_ids = sorted(old_ids - new_ids)
|
|
66
|
+
added_ids = sorted(new_ids - old_ids)
|
|
67
|
+
common_ids = sorted(old_ids & new_ids)
|
|
68
|
+
|
|
69
|
+
modified = []
|
|
70
|
+
id_changes = []
|
|
71
|
+
for nid in common_ids:
|
|
72
|
+
old_ident = node_identity(old_by_id[nid])
|
|
73
|
+
new_ident = node_identity(new_by_id[nid])
|
|
74
|
+
if old_ident != new_ident:
|
|
75
|
+
modified.append({
|
|
76
|
+
'id': nid,
|
|
77
|
+
'old': {'type': old_ident['type'], 'name': old_ident['name']},
|
|
78
|
+
'new': {'type': new_ident['type'], 'name': new_ident['name']},
|
|
79
|
+
'attrs_changed': old_ident['attrs'] != new_ident['attrs'],
|
|
80
|
+
'type_changed': old_ident['type'] != new_ident['type'],
|
|
81
|
+
'name_changed': old_ident['name'] != new_ident['name'],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
# 检测可能的不必要 ID 变更(内容相同但 ID 不同)
|
|
85
|
+
old_by_identity = {}
|
|
86
|
+
for n in old_nodes:
|
|
87
|
+
ident = json.dumps(node_identity(n), sort_keys=True, ensure_ascii=False)
|
|
88
|
+
old_by_identity.setdefault(ident, []).append(n)
|
|
89
|
+
|
|
90
|
+
for n in new_nodes:
|
|
91
|
+
if node_key(n) in added_ids:
|
|
92
|
+
ident = json.dumps(node_identity(n), sort_keys=True, ensure_ascii=False)
|
|
93
|
+
if ident in old_by_identity and old_by_identity[ident]:
|
|
94
|
+
old_match = old_by_identity[ident][0]
|
|
95
|
+
if node_key(old_match) in deleted_ids:
|
|
96
|
+
id_changes.append({
|
|
97
|
+
'old_id': node_key(old_match),
|
|
98
|
+
'new_id': node_key(n),
|
|
99
|
+
'identity': node_identity(n),
|
|
100
|
+
'warning': '节点内容未变但 ID 已变更,可能导致后端不支持配置丢失',
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
'deleted': [{'id': nid, 'name': old_by_id[nid].get('name', ''), 'type': old_by_id[nid].get('type', '')} for nid in deleted_ids],
|
|
105
|
+
'added': [{'id': nid, 'name': new_by_id[nid].get('name', ''), 'type': new_by_id[nid].get('type', '')} for nid in added_ids],
|
|
106
|
+
'modified': modified,
|
|
107
|
+
'id_changes': id_changes,
|
|
108
|
+
'unchanged': len(common_ids) - len(modified),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def diff_edges(old_edges, new_edges):
|
|
113
|
+
"""对比边差异"""
|
|
114
|
+
old_by_key = {edge_key(e): e for e in old_edges}
|
|
115
|
+
new_by_key = {edge_key(e): e for e in new_edges}
|
|
116
|
+
|
|
117
|
+
old_keys = set(old_by_key.keys())
|
|
118
|
+
new_keys = set(new_by_key.keys())
|
|
119
|
+
|
|
120
|
+
deleted_keys = sorted(old_keys - new_keys)
|
|
121
|
+
added_keys = sorted(new_keys - old_keys)
|
|
122
|
+
common_keys = sorted(old_keys & new_keys)
|
|
123
|
+
|
|
124
|
+
modified = []
|
|
125
|
+
for key in common_keys:
|
|
126
|
+
old_ident = edge_identity(old_by_key[key])
|
|
127
|
+
new_ident = edge_identity(new_by_key[key])
|
|
128
|
+
if old_ident != new_ident:
|
|
129
|
+
modified.append({
|
|
130
|
+
'from': key[0],
|
|
131
|
+
'to': key[1],
|
|
132
|
+
'old_condition': old_ident['condition'],
|
|
133
|
+
'new_condition': new_ident['condition'],
|
|
134
|
+
'label_changed': old_ident['label'] != new_ident['label'],
|
|
135
|
+
'condition_changed': old_ident['condition'] != new_ident['condition'],
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
'deleted': [{'from': k[0], 'to': k[1]} for k in deleted_keys],
|
|
140
|
+
'added': [{'from': k[0], 'to': k[1]} for k in added_keys],
|
|
141
|
+
'modified': modified,
|
|
142
|
+
'unchanged': len(common_keys) - len(modified),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def print_human(result):
|
|
147
|
+
"""人类可读输出"""
|
|
148
|
+
nodes = result['nodes']
|
|
149
|
+
edges = result['edges']
|
|
150
|
+
|
|
151
|
+
print("=" * 60)
|
|
152
|
+
print("工作流 Spec 差异分析")
|
|
153
|
+
print("=" * 60)
|
|
154
|
+
|
|
155
|
+
# 节点汇总
|
|
156
|
+
print(f"\n📊 节点汇总:")
|
|
157
|
+
print(f" 删除: {len(nodes['deleted'])} | 新增: {len(nodes['added'])} | "
|
|
158
|
+
f"修改: {len(nodes['modified'])} | 未变: {nodes['unchanged']}")
|
|
159
|
+
|
|
160
|
+
if nodes['id_changes']:
|
|
161
|
+
print(f"\n⚠️ 检测到 {len(nodes['id_changes'])} 个可能的 ID 变更(内容相同但 ID 不同):")
|
|
162
|
+
for ic in nodes['id_changes']:
|
|
163
|
+
print(f" {ic['old_id']} → {ic['new_id']} ({ic['identity']['name']})")
|
|
164
|
+
print(f" ⚠ {ic['warning']}")
|
|
165
|
+
|
|
166
|
+
if nodes['deleted']:
|
|
167
|
+
print(f"\n🗑 删除的节点 ({len(nodes['deleted'])}):")
|
|
168
|
+
for d in nodes['deleted']:
|
|
169
|
+
print(f" - [{d['type']}] {d['id']} ({d['name']})")
|
|
170
|
+
|
|
171
|
+
if nodes['added']:
|
|
172
|
+
print(f"\n➕ 新增的节点 ({len(nodes['added'])}):")
|
|
173
|
+
for a in nodes['added']:
|
|
174
|
+
print(f" - [{a['type']}] {a['id']} ({a['name']})")
|
|
175
|
+
|
|
176
|
+
if nodes['modified']:
|
|
177
|
+
print(f"\n✏️ 修改的节点 ({len(nodes['modified'])}):")
|
|
178
|
+
for m in nodes['modified']:
|
|
179
|
+
changes = []
|
|
180
|
+
if m['type_changed']:
|
|
181
|
+
changes.append(f"type: {m['old']['type']} → {m['new']['type']}")
|
|
182
|
+
if m['name_changed']:
|
|
183
|
+
changes.append(f"name: {m['old']['name']} → {m['new']['name']}")
|
|
184
|
+
if m['attrs_changed']:
|
|
185
|
+
changes.append("attrs 已变更")
|
|
186
|
+
print(f" - {m['id']}: {', '.join(changes) if changes else '无实质变更'}")
|
|
187
|
+
|
|
188
|
+
# 边汇总
|
|
189
|
+
print(f"\n📊 边汇总:")
|
|
190
|
+
print(f" 删除: {len(edges['deleted'])} | 新增: {len(edges['added'])} | "
|
|
191
|
+
f"修改: {len(edges['modified'])} | 未变: {edges['unchanged']}")
|
|
192
|
+
|
|
193
|
+
if edges['deleted']:
|
|
194
|
+
print(f"\n🗑 删除的边 ({len(edges['deleted'])}):")
|
|
195
|
+
for d in edges['deleted']:
|
|
196
|
+
print(f" - {d['from']} → {d['to']}")
|
|
197
|
+
|
|
198
|
+
if edges['added']:
|
|
199
|
+
print(f"\n➕ 新增的边 ({len(edges['added'])}):")
|
|
200
|
+
for a in edges['added']:
|
|
201
|
+
print(f" - {a['from']} → {a['to']}")
|
|
202
|
+
|
|
203
|
+
if edges['modified']:
|
|
204
|
+
print(f"\n✏️ 修改的边 ({len(edges['modified'])}):")
|
|
205
|
+
for m in edges['modified']:
|
|
206
|
+
changes = []
|
|
207
|
+
if m['label_changed']:
|
|
208
|
+
changes.append("label 已变更")
|
|
209
|
+
if m['condition_changed']:
|
|
210
|
+
changes.append("condition 已变更")
|
|
211
|
+
if changes:
|
|
212
|
+
print(f" - {m['from']} → {m['to']}: {', '.join(changes)}")
|
|
213
|
+
else:
|
|
214
|
+
print(f" - {m['from']} → {m['to']}: 无实质变更")
|
|
215
|
+
|
|
216
|
+
# 最小修改原则评估
|
|
217
|
+
print(f"\n📋 最小修改原则评估:")
|
|
218
|
+
issues = 0
|
|
219
|
+
if nodes['id_changes']:
|
|
220
|
+
print(f" ❌ 存在 ID 变更({len(nodes['id_changes'])} 个节点),可能导致后端不支持配置丢失")
|
|
221
|
+
issues += 1
|
|
222
|
+
if nodes['deleted']:
|
|
223
|
+
print(f" ⚠️ 删除了 {len(nodes['deleted'])} 个节点,请确认是否为业务需要")
|
|
224
|
+
issues += 1
|
|
225
|
+
if not nodes['deleted'] and not nodes['id_changes'] and nodes['modified']:
|
|
226
|
+
print(f" ✅ 仅修改已有节点,未删除或变更 ID,符合最小修改原则")
|
|
227
|
+
if not nodes['deleted'] and not nodes['modified'] and not nodes['added'] and not nodes['id_changes']:
|
|
228
|
+
print(f" ✅ 无任何变更")
|
|
229
|
+
|
|
230
|
+
print(f"\n总结: 错误 {issues} 项")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def main():
|
|
234
|
+
if len(sys.argv) < 3:
|
|
235
|
+
print("用法: python3 diff_flow_spec.py <old_spec.json> <new_spec.json> [--json]")
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
|
|
238
|
+
old_file = sys.argv[1]
|
|
239
|
+
new_file = sys.argv[2]
|
|
240
|
+
output_json = '--json' in sys.argv
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
old_spec = load_json(old_file)
|
|
244
|
+
except Exception as e:
|
|
245
|
+
print(f"FATAL: 无法读取旧 spec 文件 {old_file}: {e}")
|
|
246
|
+
sys.exit(1)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
new_spec = load_json(new_file)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
print(f"FATAL: 无法读取新 spec 文件 {new_file}: {e}")
|
|
252
|
+
sys.exit(1)
|
|
253
|
+
|
|
254
|
+
old_nodes = old_spec.get('nodes', [])
|
|
255
|
+
new_nodes = new_spec.get('nodes', [])
|
|
256
|
+
old_edges = extract_edges(old_spec)
|
|
257
|
+
new_edges = extract_edges(new_spec)
|
|
258
|
+
|
|
259
|
+
result = {
|
|
260
|
+
'nodes': diff_nodes(old_nodes, new_nodes),
|
|
261
|
+
'edges': diff_edges(old_edges, new_edges),
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if output_json:
|
|
265
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
266
|
+
else:
|
|
267
|
+
print_human(result)
|
|
268
|
+
|
|
269
|
+
# 如果有 ID 变更,返回非零以便脚本判断
|
|
270
|
+
has_id_changes = len(result['nodes']['id_changes']) > 0
|
|
271
|
+
sys.exit(1 if has_id_changes else 0)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == '__main__':
|
|
275
|
+
main()
|