camunda-python 0.1.0__tar.gz
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.
- camunda_python-0.1.0/LICENSE +200 -0
- camunda_python-0.1.0/PKG-INFO +377 -0
- camunda_python-0.1.0/README.md +348 -0
- camunda_python-0.1.0/camunda/__init__.py +10 -0
- camunda_python-0.1.0/camunda/api/__init__.py +14 -0
- camunda_python-0.1.0/camunda/api/app.py +80 -0
- camunda_python-0.1.0/camunda/api/deps.py +17 -0
- camunda_python-0.1.0/camunda/api/errors.py +84 -0
- camunda_python-0.1.0/camunda/api/pagination.py +74 -0
- camunda_python-0.1.0/camunda/api/routers/__init__.py +5 -0
- camunda_python-0.1.0/camunda/api/routers/decision.py +57 -0
- camunda_python-0.1.0/camunda/api/routers/deployment.py +139 -0
- camunda_python-0.1.0/camunda/api/routers/history.py +128 -0
- camunda_python-0.1.0/camunda/api/routers/process_definition.py +49 -0
- camunda_python-0.1.0/camunda/api/routers/process_instance.py +106 -0
- camunda_python-0.1.0/camunda/api/routers/task.py +92 -0
- camunda_python-0.1.0/camunda/api/schemas.py +200 -0
- camunda_python-0.1.0/camunda/common/__init__.py +19 -0
- camunda_python-0.1.0/camunda/common/clock.py +30 -0
- camunda_python-0.1.0/camunda/common/exceptions.py +34 -0
- camunda_python-0.1.0/camunda/common/idgen.py +23 -0
- camunda_python-0.1.0/camunda/common/timers.py +106 -0
- camunda_python-0.1.0/camunda/dmn/__init__.py +5 -0
- camunda_python-0.1.0/camunda/dmn/engine.py +219 -0
- camunda_python-0.1.0/camunda/dmn/feel.py +392 -0
- camunda_python-0.1.0/camunda/engine/__init__.py +9 -0
- camunda_python-0.1.0/camunda/engine/behavior.py +51 -0
- camunda_python-0.1.0/camunda/engine/expression.py +126 -0
- camunda_python-0.1.0/camunda/engine/process_engine.py +3237 -0
- camunda_python-0.1.0/camunda/job/__init__.py +9 -0
- camunda_python-0.1.0/camunda/job/executor.py +136 -0
- camunda_python-0.1.0/camunda/model/__init__.py +48 -0
- camunda_python-0.1.0/camunda/model/bpmn.py +326 -0
- camunda_python-0.1.0/camunda/model/dmn.py +101 -0
- camunda_python-0.1.0/camunda/model/execution.py +121 -0
- camunda_python-0.1.0/camunda/model/job.py +88 -0
- camunda_python-0.1.0/camunda/model/task.py +33 -0
- camunda_python-0.1.0/camunda/model/variable.py +35 -0
- camunda_python-0.1.0/camunda/parser/__init__.py +5 -0
- camunda_python-0.1.0/camunda/parser/bpmn_parser.py +646 -0
- camunda_python-0.1.0/camunda/parser/dmn_parser.py +225 -0
- camunda_python-0.1.0/camunda/persistence/__init__.py +21 -0
- camunda_python-0.1.0/camunda/persistence/entities.py +202 -0
- camunda_python-0.1.0/camunda/persistence/store.py +721 -0
- camunda_python-0.1.0/camunda_python.egg-info/PKG-INFO +377 -0
- camunda_python-0.1.0/camunda_python.egg-info/SOURCES.txt +49 -0
- camunda_python-0.1.0/camunda_python.egg-info/dependency_links.txt +1 -0
- camunda_python-0.1.0/camunda_python.egg-info/requires.txt +13 -0
- camunda_python-0.1.0/camunda_python.egg-info/top_level.txt +1 -0
- camunda_python-0.1.0/pyproject.toml +49 -0
- camunda_python-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for describing the origin of the Work and
|
|
141
|
+
reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file name and extension be included on the same "printed" page as
|
|
186
|
+
the copyright notice for easier identification within third-party archives.
|
|
187
|
+
|
|
188
|
+
Copyright 2026 yingwang (movingheart000@gmail.com)
|
|
189
|
+
|
|
190
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
191
|
+
you may not use this file except in compliance with the License.
|
|
192
|
+
You may obtain a copy of the License at
|
|
193
|
+
|
|
194
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
195
|
+
|
|
196
|
+
Unless required by applicable law or agreed to in writing, software
|
|
197
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
198
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
199
|
+
See the License for the specific language governing permissions and
|
|
200
|
+
limitations under the License.
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: camunda-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python3 reimplementation of Camunda 7 BPMN engine (semantic-aligned, Apache-2.0 independent implementation)
|
|
5
|
+
Author: yingwang
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: bpmn,camunda,workflow,process-engine,workflow-engine
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: lxml>=5.0
|
|
18
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
19
|
+
Requires-Dist: croniter>=2.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
23
|
+
Provides-Extra: api
|
|
24
|
+
Requires-Dist: fastapi>=0.115; extra == "api"
|
|
25
|
+
Requires-Dist: uvicorn>=0.30; extra == "api"
|
|
26
|
+
Requires-Dist: python-multipart>=0.0.9; extra == "api"
|
|
27
|
+
Requires-Dist: httpx>=0.27; extra == "api"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# camunda-python
|
|
31
|
+
|
|
32
|
+
用 Python 3 语义对齐重写 Camunda 7 BPMN 引擎(Apache-2.0 独立实现,不搬运 Java 源码)。
|
|
33
|
+
|
|
34
|
+
**第一次来?** 先看 [docs/USER_GUIDE.md](docs/USER_GUIDE.md)(从「5 分钟跑通 hello world」到「生产部署」)。
|
|
35
|
+
**想知道项目覆盖了哪些特性 / 跑测试 / 怎么用每个 API?** 看本文档。
|
|
36
|
+
**想了解设计决策 / 与 Camunda 7 的差异?** 看 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
|
|
37
|
+
|
|
38
|
+
## 状态
|
|
39
|
+
|
|
40
|
+
- **M0~M8 已交付**(M4 全家族 + M5 DMN + M6 REST + M7 多 JobExecutor 抢锁 + M8 REST 列表分页):工程骨架 + BPMN 模型层 + lxml 解析器 + 内存版引擎核心流转 +
|
|
41
|
+
SQLAlchemy 2.0 持久化(ACT_RE/RU/HI 三套表)+ 崩溃恢复 + 历史写入 +
|
|
42
|
+
作业执行器(Timer Start / Timer Catch / asyncBefore / asyncAfter / 失败重试 / 轮询)+
|
|
43
|
+
timer 边界事件(中断式,宿主 userTask / asyncBefore 节点)+ **内嵌子流程**
|
|
44
|
+
(embedded SubProcess:容器递归解析 / 展开与收束复活 / 边界 timer 中断整段 scope)+
|
|
45
|
+
**事件子流程**(error end 冒泡捕获 / timer start 订阅触发)+ **非中断式边界事件**
|
|
46
|
+
(cancelActivity=false:宿主不取消、spawn 并发线;root 停驻等待并发子树收束)+
|
|
47
|
+
**多实例**(MultiInstanceLoopCharacteristics:userTask / serviceTask / subProcess 三宿主 +
|
|
48
|
+
completionCondition 提前终止 + loopCounter/元素变量 + 持久化恢复)+ **消息/信号事件**
|
|
49
|
+
(correlate_message 1:1 关联 + throw_signal 跨实例广播 + catch/边界/事件子流程 start/中间
|
|
50
|
+
与结束抛出全形态 + 非中断订阅常驻 + 恢复重推导)+ **DMN 决策引擎**
|
|
51
|
+
(决策表解析 + FEEL 子集求值 + 六种 hitPolicy 收敛 + businessRuleTask 集成)+ **REST 兼容层**
|
|
52
|
+
(FastAPI 对齐 Camunda engine-rest:部署 / 流程定义 / 流程实例 / 任务 / 历史 / 决策六类端点 +
|
|
53
|
+
9 个列表端点 firstResult + maxResults 分页)+ **多 JobExecutor 抢锁**
|
|
54
|
+
(DB CAS lease:同一作业不会被多节点重复执行 + lease 过期可被接管 + 续约)
|
|
55
|
+
- 参考基线:Camunda 7.23.0(社区版最后开源稳定版)
|
|
56
|
+
- 架构与里程碑见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
57
|
+
|
|
58
|
+
## 快速开始
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cd /Users/yingwang/CodeSpace/camunda
|
|
62
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
63
|
+
pip install -e ".[dev]" # 跑测试
|
|
64
|
+
pip install -e ".[api]" # 用 REST(M6,含 fastapi/uvicorn)
|
|
65
|
+
|
|
66
|
+
# 跑贷款审批示例(排他网关 + 用户任务 + 服务任务)
|
|
67
|
+
python examples/run_demo.py
|
|
68
|
+
|
|
69
|
+
# 跑定时器/作业示例(真实时钟 ~15 秒:自动定时启动 + async 拆分 + 冷却等待 + 崩溃恢复)
|
|
70
|
+
python examples/run_timer_demo.py
|
|
71
|
+
|
|
72
|
+
# 跑边界事件/asyncAfter 示例(真实时钟 ~6 秒:审批超时自动降级 + asyncAfter 审计日志)
|
|
73
|
+
python examples/run_boundary_demo.py
|
|
74
|
+
|
|
75
|
+
# 跑内嵌子流程示例(真实时钟 ~5 秒:订单履约子流程 + 边界超时整段中断退款)
|
|
76
|
+
python examples/run_subprocess_demo.py
|
|
77
|
+
|
|
78
|
+
# 跑非中断式边界示例(真实时钟 ~6 秒:工单催办宿主保留 + 并发复核收束)
|
|
79
|
+
python examples/run_ni_demo.py
|
|
80
|
+
|
|
81
|
+
# 跑多实例三宿主示例(纯同步驱动:serviceTask 同步推送 / subProcess 顺序逐店 /
|
|
82
|
+
# subProcess 并行 + completionCondition 抢跑终止)
|
|
83
|
+
python examples/run_mi_demo.py
|
|
84
|
+
|
|
85
|
+
# 跑消息/信号事件示例(纯同步驱动:跨实例消息接力 / 信号广播通知 / 崩溃恢复)
|
|
86
|
+
python examples/run_msg_sig_demo.py
|
|
87
|
+
|
|
88
|
+
# 跑 DMN 决策示例(纯同步驱动:直接求值 / 决策联动 COLLECT+SUM / businessRuleTask 集成)
|
|
89
|
+
python examples/run_dmn_demo.py
|
|
90
|
+
|
|
91
|
+
# 跑 REST API 示例(真实起 uvicorn + 真实 HTTP:部署/启动/任务/历史/决策/异常)
|
|
92
|
+
python examples/run_api_demo.py
|
|
93
|
+
|
|
94
|
+
# 跑多 JobExecutor 抢锁示例(同步驱动 + 真实 sleep:双 owner 共享 store,
|
|
95
|
+
# 同一 due job 只被其中一个执行 + lease 过期可被另一 owner 接管)
|
|
96
|
+
python examples/run_lock_demo.py
|
|
97
|
+
|
|
98
|
+
# 跑测试(268 个单测:M1 流转 + M2 持久化 + M3 作业 + M4-1 边界/asyncAfter +
|
|
99
|
+
# M4-2a 子流程 + M4-2b 事件子流程/非中断式边界 + M4-2c 多实例 +
|
|
100
|
+
# M4-2d 消息/信号事件 + 持久化恢复 + M5 DMN 解析/FEEL/引擎/businessRuleTask +
|
|
101
|
+
# M6 REST 端点与异常映射 + M7 store CAS lease 抢锁原语 + 多 executor 防双执行 +
|
|
102
|
+
# M8 REST 列表分页(9 个端点 + clamp/边界))
|
|
103
|
+
pytest
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 目录结构
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
camunda/
|
|
110
|
+
├── model/ # 纯数据模型:BpmnModel / Process / FlowNode / Execution / Task / Variable
|
|
111
|
+
├── parser/ # lxml 自研 BPMN 2.0 解析器
|
|
112
|
+
├── engine/ # 引擎门面 + 节点行为(M1 内存版)
|
|
113
|
+
├── job/ # 作业执行器(M3)
|
|
114
|
+
├── persistence/ # SQLAlchemy 持久层(M2)
|
|
115
|
+
├── api/ # REST 兼容层(M6)
|
|
116
|
+
├── dmn/ # DMN 决策引擎(M5)
|
|
117
|
+
└── common/ # 异常层次、ID 生成器
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 引擎用法
|
|
121
|
+
|
|
122
|
+
**内存版(M1)**——无 DB 依赖:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from camunda.parser import parse_bpmn_xml
|
|
126
|
+
from camunda.engine import ProcessEngine
|
|
127
|
+
|
|
128
|
+
engine = ProcessEngine()
|
|
129
|
+
with open("examples/loan-approval.bpmn") as f:
|
|
130
|
+
model = parse_bpmn_xml(f.read(), source_name="loan-approval.bpmn")
|
|
131
|
+
engine.deploy(model) # 部署 -> ["loan-approval"]
|
|
132
|
+
|
|
133
|
+
pi = engine.start_process_instance_by_key(
|
|
134
|
+
"loan-approval", {"applicant": "张三", "amount": 20000})
|
|
135
|
+
tasks = engine.create_task_query(process_instance_id=pi.id) # [人工审批]
|
|
136
|
+
engine.complete_task(tasks[0].id, {"approved": True}) # -> COMPLETED
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**持久化版(M2)**——每个命令边界全量同步 ACT 表;进程崩溃后 `from_database` 恢复:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from camunda.persistence.store import Store
|
|
143
|
+
|
|
144
|
+
# 裸文件路径即可,Store 自动归一化为 sqlite:/// 绝对路径
|
|
145
|
+
engine = ProcessEngine(store=Store("/tmp/camunda.db"))
|
|
146
|
+
engine.deploy(model)
|
|
147
|
+
pi = engine.start_process_instance_by_key("loan-approval", {"amount": 20000})
|
|
148
|
+
# …… 进程在此崩溃 ……
|
|
149
|
+
|
|
150
|
+
# 重启:恢复部署定义 + 运行中实例(execution 树/task/变量/join 等待登记)
|
|
151
|
+
engine2 = ProcessEngine.from_database("/tmp/camunda.db")
|
|
152
|
+
pi2 = engine2.get_process_instance(pi.id)
|
|
153
|
+
engine2.register_delegate("checkCredit", lambda v: v.update(credit_ok=True)) # delegate 不落库
|
|
154
|
+
engine2.complete_task(engine2.create_task_query(process_instance_id=pi.id)[0].id, {"approved": False})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**作业执行器版(M3)**——定时启动/定时等待/async 拆分 + 失败重试,`JobExecutor` 后台轮询
|
|
158
|
+
(也可手动 `execute_due_jobs()` / 单步 `tick()` 拨钟测试):
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from camunda.engine import ProcessEngine
|
|
162
|
+
from camunda.job import JobExecutor
|
|
163
|
+
|
|
164
|
+
engine = ProcessEngine()
|
|
165
|
+
engine.deploy(model) # 流程含 timer start / timer catch / camunda:asyncBefore 节点
|
|
166
|
+
engine.create_job_query() # [timer-start job(定义级)] 等
|
|
167
|
+
|
|
168
|
+
ex = JobExecutor(engine, poll_interval=0.5) # 后台轮询到期作业(用法见 examples/run_timer_demo.py)
|
|
169
|
+
ex.start() # 也可 ex.tick() 单步 / ex.shutdown() 停表
|
|
170
|
+
# 到期自动:timer-start 启动流程、timer-catch 继续流转、async 执行节点行为;
|
|
171
|
+
# 失败自动 retries-1 并顺延 duedate,耗尽即死信;引擎时间可注入 clock 便于测试
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**边界事件 / asyncAfter(M4-1)**——宿主等待期内 timer 到期中断,或行为完成后异步离开:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# BPMN:<bpmn:userTask id="approve"/> + 平级 <bpmn:boundaryEvent id="timeout"
|
|
178
|
+
# attachedToRef="approve"><bpmn:timerEventDefinition><bpmn:timeDuration
|
|
179
|
+
# xsi:type="bpmn:tFormalExpression">PT2S</bpmn:timeDuration>...
|
|
180
|
+
pi = engine.start_process_instance_by_key("approval-timeout") # 停在审批任务
|
|
181
|
+
# 2 秒无人 complete -> JobExecutor 触发 timer-boundary:取消审批任务,token 沿
|
|
182
|
+
# 边界事件出边走超时路径;期间 complete_task 则正常走主路、边界作业自动撤销
|
|
183
|
+
# asyncAfter:<bpmn:serviceTask camunda:asyncAfter="true" .../> delegate 同步执行,
|
|
184
|
+
# 「离开推进」拆成 async-after 作业(XOR 离开时重新求值条件);asyncBefore+asyncAfter 可链式
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**内嵌子流程(M4-2a)**——token 进入 subProcess 展开为 SCOPE,内部走完自动收束复活:
|
|
188
|
+
```python
|
|
189
|
+
# BPMN:<bpmn:subProcess id="fulfill"> 内含独立 startEvent/task/endEvent 与连线
|
|
190
|
+
pi = engine.start_process_instance_by_key("order-fulfillment") # 停在子流程内部质检任务
|
|
191
|
+
# - 容器递归解析:跨容器连线引用部署即报错;嵌套子流程逐层收束
|
|
192
|
+
# - 边界 timer 挂 subProcess:等待窗口 = 整段内部执行;到期中断整段 scope(内部
|
|
193
|
+
# 任务归档/actinst 结算/作业清理),token 沿边界路径走;正常走完则边界撤销
|
|
194
|
+
# - 变量沿用实例级(文档化差异);asyncBefore 可用于 subProcess(展开前异步窗口)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**非中断式边界事件(M4-2b4,cancelActivity=false)**——到期不打断宿主,spawn 并发线:
|
|
198
|
+
```python
|
|
199
|
+
# BPMN:<bpmn:userTask id="handle"/> + <bpmn:boundaryEvent id="esc"
|
|
200
|
+
# attachedToRef="handle" cancelActivity="false"> <timer PT2S ...>
|
|
201
|
+
pi = engine.start_process_instance_by_key("ticket-support") # 停处理任务
|
|
202
|
+
# 2 秒无人处理 -> JobExecutor 触发 NI 边界:处理任务保留不取消(cancelActivity=false),
|
|
203
|
+
# 并发线从边界出边推进(如自动催办)后收束;宿主随后完成 -> 实例收束完成。
|
|
204
|
+
# 若主线先到 end 而并发线(如复核任务)未收束:实例不提前结束,root 转 SCOPE 停驻,
|
|
205
|
+
# 等并发线全部收束后实例才完成;subProcess 宿主 + NI 明确报错(文档化差异)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**多实例(M4-2c)**——MultiInstanceLoopCharacteristics,三种宿主 userTask/serviceTask/
|
|
209
|
+
subProcess(并行/顺序):
|
|
210
|
+
```python
|
|
211
|
+
# BPMN:<bpmn:userTask id="review"> <bpmn:multiInstanceLoopCharacteristics
|
|
212
|
+
# camunda:collection="${reviewers}" camunda:elementVariable="reviewer"/>
|
|
213
|
+
pi = engine.start_process_instance_by_key("mi-flow", {"reviewers": ["a", "b", "c"]})
|
|
214
|
+
# 并行:N 实例任务并存(execution 树 = SCOPE 容器 + N child);顺序:同一时刻仅 1 实例。
|
|
215
|
+
# serviceTask 宿主 = 同步 delegate 无等待窗口(start 返回即收束);
|
|
216
|
+
# subProcess 宿主 = 实例各自进内部流转,内部走完由收束链驱动实例完成、容器收束离开。
|
|
217
|
+
# completionCondition(如 ${nrOfCompletedInstances >= 2}):满足即终止剩余实例(任务归档/
|
|
218
|
+
# actinst 结算/整树清理);空集合零实例直接通过;loopCounter/elementVariable 行为期注入、
|
|
219
|
+
# 容器收尾清理;宿主组合 asyncBefore/边界事件运行时明确报错(文档化差异)。
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**消息 / 信号事件(M4-2d)**——correlate_message 1:1 精准关联,throw_signal 跨实例广播:
|
|
223
|
+
```python
|
|
224
|
+
# BPMN:<bpmn:intermediateCatchEvent><bpmn:messageEventDefinition messageRef="M1"/>
|
|
225
|
+
# 配套 <bpmn:message id="M1" name="orderReady"/> 声明(signal 同型)
|
|
226
|
+
pi = engine.start_process_instance_by_key("order-relay") # 停在消息 catch 停等
|
|
227
|
+
engine.correlate_message("orderReady", variables={"paid": True}) # 1:1 投递(未限定实例取注册序最早)
|
|
228
|
+
# 边界事件:<bpmn:boundaryEvent attachedToRef="ut" cancelActivity="false">
|
|
229
|
+
# <bpmn:signalEventDefinition signalRef="S1"/>
|
|
230
|
+
hits = engine.throw_signal("maintenance") # 跨实例广播全部命中订阅,返回命中数
|
|
231
|
+
# 非中断式边界/事件子流程 start:订阅常驻可重复触发;中断式:触发即消费、宿主被接管
|
|
232
|
+
# 实例内 throw:<bpmn:intermediateThrowEvent>/<bpmn:endEvent> + message/signal 定义,
|
|
233
|
+
# 触发同实例 catch(未命中静默丢弃);订阅为纯内存派生态,崩溃恢复自动重推导
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**DMN 决策表(M5)**——独立决策引擎 + BPMN businessRuleTask 集成:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from camunda.dmn.engine import DmnEngine
|
|
240
|
+
from camunda.parser.dmn_parser import parse_dmn_file
|
|
241
|
+
|
|
242
|
+
dmn = DmnEngine()
|
|
243
|
+
dmn.deploy(parse_dmn_file("examples/loan-grading.dmn")) # -> ["loan-grading", "rate-discount"]
|
|
244
|
+
dmn.evaluate_decision("loan-grading", {"amount": 9000, "credit_score": 750}) # -> "B"
|
|
245
|
+
|
|
246
|
+
# BPMN 集成:<bpmn:businessRuleTask id="grade" camunda:decisionRef="loan-grading"
|
|
247
|
+
# camunda:resultVariable="grade"/>
|
|
248
|
+
engine.deploy_dmn(parse_dmn_file("examples/loan-grading.dmn"))
|
|
249
|
+
engine.deploy(parse_bpmn_file("examples/loan-grading-flow.bpmn"))
|
|
250
|
+
pi = engine.start_process_instance_by_key("loan-process", {"amount": 20000, "credit_score": 500})
|
|
251
|
+
pi.variables["grade"] # "C" -> 决策结果写入 resultVariable,可驱动排他网关选路
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
- **FEEL 子集**(unaryTests 输入单元格):比较 `= != < <= > >=`、区间 `[a..b]` / `(a..b)` /
|
|
255
|
+
`]a..b[`(DMN 双标记法)、逗号列表 = OR、`not(...)`、`null` 判缺变量、空文本 / `-` = 通配;
|
|
256
|
+
expression(输出单元格 / inputExpression)支持字面量、变量引用(未定义 -> null)、
|
|
257
|
+
算术 `+ - * /` 与括号、字符串 `+` 拼接。不支持函数调用 / between / in / 日期时间 / 路径表达式
|
|
258
|
+
(运行时明确报错)。
|
|
259
|
+
- **hitPolicy**:`UNIQUE`(多行命中运行时报错)/ `FIRST` / `ANY`(各行输出不一致报错)/
|
|
260
|
+
`PRIORITY`(按 output 的 outputValues 优先级序取最高,仅单输出列)/ `RULE ORDER`(行结果列表)/
|
|
261
|
+
`COLLECT`(列表,可叠 `SUM` / `MIN` / `MAX` / `COUNT` 聚合)。无命中不报错,返回空结果。
|
|
262
|
+
- **已知限制**:DMN 部署不落库(对齐 delegate 注册先例)——崩溃恢复后须重新 `deploy_dmn`,
|
|
263
|
+
否则 businessRuleTask 求值报未部署。
|
|
264
|
+
|
|
265
|
+
**REST API(M6)**——FastAPI 对齐 Camunda engine-rest,总前缀 `/engine-rest`:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
pip install -e ".[api]"
|
|
269
|
+
uvicorn camunda.api.app:create_app --factory --port 8080
|
|
270
|
+
# 交互式文档:http://127.0.0.1:8080/docs
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
from camunda.api import create_app
|
|
275
|
+
app = create_app() # 内存引擎(demo/测试)
|
|
276
|
+
app = create_app(engine=engine) # 复用既有引擎(含 Store / JobExecutor)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# 部署(multipart 对齐 Camunda;另有 JSON 便捷通道 POST /deployment/create/xml)
|
|
281
|
+
curl -F "data=@examples/loan-approval.bpmn" \
|
|
282
|
+
http://localhost:8080/engine-rest/deployment/create
|
|
283
|
+
|
|
284
|
+
# 启动实例(变量兼容包装形态与裸值)
|
|
285
|
+
curl -XPOST http://localhost:8080/engine-rest/process-instance \
|
|
286
|
+
-H 'Content-Type: application/json' \
|
|
287
|
+
-d '{"definitionKey":"loan-approval","variables":{"amount":{"value":20000,"type":"Long"}}}'
|
|
288
|
+
|
|
289
|
+
# 任务:列表 -> 认领 -> 完成
|
|
290
|
+
curl http://localhost:8080/engine-rest/task
|
|
291
|
+
curl -XPOST http://localhost:8080/engine-rest/task/{id}/claim \
|
|
292
|
+
-H 'Content-Type: application/json' -d '{"userId":"lisi"}'
|
|
293
|
+
curl -XPOST http://localhost:8080/engine-rest/task/{id}/complete \
|
|
294
|
+
-H 'Content-Type: application/json' -d '{"variables":{"approved":true}}'
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
| 端点 | 方法 | 说明 |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| `/deployment/create` | POST | multipart 部署(字段名 `data`,BPMN/DMN 自动分派) |
|
|
300
|
+
| `/deployment/create/xml` | POST | JSON 便捷部署(本项目扩展) |
|
|
301
|
+
| `/deployment` | GET | 部署列表 |
|
|
302
|
+
| `/process-definition[ /key/{key}[/xml] ]` | GET | 流程定义列表 / 单个 / XML |
|
|
303
|
+
| `/process-instance` | POST/GET | 启动 / 列表(按 key、businessKey、active 过滤) |
|
|
304
|
+
| `/process-instance/{id}` | GET/DELETE | 查询 / 删除(历史保留,HI 置 DELETED) |
|
|
305
|
+
| `/process-instance/{id}/variables[ /{name} ]` | GET/PUT | 变量读写 |
|
|
306
|
+
| `/task` | GET | 任务列表(按 assignee / candidateUser / unassigned 过滤) |
|
|
307
|
+
| `/task/{id}` | GET | 任务详情 |
|
|
308
|
+
| `/task/{id}/claim` `unclaim` `assignee` `complete` | POST | 认领 / 取消 / 指派 / 完成 |
|
|
309
|
+
| `/history/process-instance` `task` `activity-instance` `variable-instance` | GET | 四类历史查询 |
|
|
310
|
+
| `/decision-definition[ /key/{key} ]` | GET | 决策定义列表 / 单个 |
|
|
311
|
+
| `/decision-definition/key/{key}/evaluate` | POST | 决策表求值 |
|
|
312
|
+
|
|
313
|
+
- **变量形态**:入参兼容包装形态 `{"amount": {"value": 20000}}` 与裸值 `{"amount": 20000}`;
|
|
314
|
+
出参默认包装形态(对齐 Camunda),带 `?bare=true` 退化为裸值 map。
|
|
315
|
+
|
|
316
|
+
**REST 列表分页(M8)**——9 个列表端点统一支持 `firstResult` + `maxResults`(对齐 Camunda 7 REST):
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# 翻页:先 0..2,再 2..4,最后 4..200
|
|
320
|
+
curl 'http://localhost:8080/engine-rest/process-instance?firstResult=0&maxResults=2'
|
|
321
|
+
curl 'http://localhost:8080/engine-rest/process-instance?firstResult=2&maxResults=2'
|
|
322
|
+
curl 'http://localhost:8080/engine-rest/process-instance?firstResult=4&maxResults=200'
|
|
323
|
+
# 末页判定:返回数 < maxResults 即末页(响应仍为裸数组,不包 count/total)
|
|
324
|
+
# 非法 firstResult(<0)/ maxResults(<1)由 FastAPI 422 拒绝
|
|
325
|
+
# maxResults > 1000 自动 clamp 到 1000(防脚本误用拉空内存)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
支持的端点:`/process-instance`、`/task`、`/process-definition`、`/decision-definition`、
|
|
329
|
+
`/deployment`、`/history/process-instance`、`/history/task`、`/history/activity-instance`、
|
|
330
|
+
`/history/variable-instance`(共 9 个,统一默认 `maxResults=200`)。
|
|
331
|
+
- **错误响应体**:统一 `{"type": "<异常类名>", "message": "<异常消息>"}`;
|
|
332
|
+
状态码 —— 未找到 404、部署/参数非法 400、实例状态冲突 409、其余引擎异常 500。
|
|
333
|
+
|
|
334
|
+
**多 JobExecutor 抢锁(M7)**—— DB CAS lease 模式,多节点 / 多进程安全并发:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from camunda.engine.process_engine import ProcessEngine
|
|
338
|
+
from camunda.job.executor import JobExecutor
|
|
339
|
+
from camunda.persistence.store import Store
|
|
340
|
+
|
|
341
|
+
engine = ProcessEngine(store=Store("sqlite:///shared.db"))
|
|
342
|
+
engine.deploy(model)
|
|
343
|
+
|
|
344
|
+
exec_a = JobExecutor(engine, name="exec-a", lease_seconds=300)
|
|
345
|
+
exec_b = JobExecutor(engine, name="exec-b", lease_seconds=300)
|
|
346
|
+
exec_a.start() # 后台轮询
|
|
347
|
+
exec_b.start() # 同一个 engine + store,但 lock_owner 不同
|
|
348
|
+
# engine.execute_due_jobs(lock_owner=...) 由 JobExecutor.tick() 自动注入
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
- **抢锁语义**:JobExecutor 启动时按 `name-pid-hostname-uuid8` 生成唯一
|
|
352
|
+
`lock_owner`,调 `engine.execute_due_jobs(lock_owner=me, lease_seconds=300)`
|
|
353
|
+
—— 引擎走 `_execute_due_jobs_db`,从 `store.acquire_due_jobs` 拿一批
|
|
354
|
+
due job:每条 `UPDATE ACT_RU_JOB SET LOCK_OWNER=me, LOCK_EXP_TIME=now+lease
|
|
355
|
+
WHERE id=:id AND (LOCK_OWNER_ IS NULL OR LOCK_EXP_TIME_ < :now)`,affected_rows > 0
|
|
356
|
+
即抢到。
|
|
357
|
+
- **崩溃恢复**:JobExecutor.shutdown 不主动释放 lease(让其自然过期),
|
|
358
|
+
便于模拟崩溃场景;其他 JobExecutor 等 lease 过期后 `LOCK_EXP_TIME_ < :now`
|
|
359
|
+
命中,可重新抢到。
|
|
360
|
+
- **续约**:长作业执行期间调 `store.extend_lock(job_id, owner, lease_seconds, now)`
|
|
361
|
+
把 lease 延后;CAS 失败 = 锁已被别人接管,当前执行应中止提交。
|
|
362
|
+
- **监控**:`store.list_locks(lock_owner=None)` 查看当前持锁情况(调试 / 运维)。
|
|
363
|
+
- **兼容性**:engine 无 Store 时 JobExecutor 自动走单进程内存路径(向后兼容旧用法)。
|
|
364
|
+
|
|
365
|
+
## 里程碑
|
|
366
|
+
|
|
367
|
+
| 版本 | 内容 | 状态 |
|
|
368
|
+
|---|---|---|
|
|
369
|
+
| M0 | 骨架 + 模型 + 解析器 | ✅ |
|
|
370
|
+
| M1 | 内存版流转(网关/任务/服务任务) | ✅ |
|
|
371
|
+
| M2 | SQLAlchemy 持久化 + 崩溃恢复(ACT_RE/RU/HI) | ✅ |
|
|
372
|
+
| M3 | 作业执行器(Timer / async continuation) | ✅ |
|
|
373
|
+
| M4 | BPMN 全元素补齐 | 🔄 M4-1 边界事件 + asyncAfter、M4-2a 内嵌子流程、M4-2b 事件子流程 + 非中断式边界、M4-2c 多实例三宿主、M4-2d 消息/信号事件已交付 |
|
|
374
|
+
| M5 | DMN + FEEL 子集 | ✅ 决策表解析 + FEEL 子集求值 + 六种 hitPolicy 收敛 + businessRuleTask 集成已交付 |
|
|
375
|
+
| M6 | REST API(FastAPI) | ✅ 六类端点(部署/定义/实例/任务/历史/决策)+ 变量双形态 + 异常映射已交付 |
|
|
376
|
+
| M7 | 多 JobExecutor 抢锁 | ✅ DB CAS lease:跨 JobExecutor / 跨进程唯一执行同一作业 + lease 过期可被接管 + 续约已交付 |
|
|
377
|
+
| M8 | REST 列表分页 | ✅ 9 个列表端点统一 firstResult + maxResults(对齐 Camunda 7 REST),maxResults 默认 200 / 硬上限 1000 |
|