labull-framework 3.0.5__tar.gz → 3.0.8__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.
Files changed (72) hide show
  1. {labull_framework-3.0.5/labull_framework.egg-info → labull_framework-3.0.8}/PKG-INFO +1 -1
  2. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/devkit.py +1 -1
  3. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/service.py +59 -0
  4. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/types/workflow.py +31 -15
  5. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/views.py +28 -0
  6. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/urls.py +3 -0
  7. {labull_framework-3.0.5 → labull_framework-3.0.8/labull_framework.egg-info}/PKG-INFO +1 -1
  8. {labull_framework-3.0.5 → labull_framework-3.0.8}/pyproject.toml +1 -1
  9. {labull_framework-3.0.5 → labull_framework-3.0.8}/LICENSE +0 -0
  10. {labull_framework-3.0.5 → labull_framework-3.0.8}/MANIFEST.in +0 -0
  11. {labull_framework-3.0.5 → labull_framework-3.0.8}/README-pypi.md +0 -0
  12. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/__init__.py +0 -0
  13. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/access.py +0 -0
  14. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/admin.py +0 -0
  15. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/apps.py +0 -0
  16. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/auth/__init__.py +0 -0
  17. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/auth/backend.py +0 -0
  18. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/auth/bootstrap.py +0 -0
  19. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/auth/sso.py +0 -0
  20. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/auth/views.py +0 -0
  21. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/canvas/__init__.py +0 -0
  22. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/canvas/data.py +0 -0
  23. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/canvas/gitinfo.py +0 -0
  24. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/canvas/shape_ast.py +0 -0
  25. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/canvas/static/canvas.css +0 -0
  26. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/canvas/static/canvas.js +0 -0
  27. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/canvas/static/page.html +0 -0
  28. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/canvas/views.py +0 -0
  29. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/checks.py +0 -0
  30. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/env.py +0 -0
  31. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/flow.py +0 -0
  32. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/jsonio.py +0 -0
  33. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/__init__.py +0 -0
  34. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/blockcheck.py +0 -0
  35. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/blockpackage.py +0 -0
  36. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/blocktypes.py +0 -0
  37. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/deepcheck.py +0 -0
  38. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/dispatch.py +0 -0
  39. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/export.py +0 -0
  40. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/health.py +0 -0
  41. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/manifest.py +0 -0
  42. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/parallel.py +0 -0
  43. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/registry.py +0 -0
  44. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/requires.py +0 -0
  45. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/sdk.py +0 -0
  46. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/subproc.py +0 -0
  47. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/types/__init__.py +0 -0
  48. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/types/plain.py +0 -0
  49. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/magic_block/validate.py +0 -0
  50. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/management/__init__.py +0 -0
  51. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/management/commands/__init__.py +0 -0
  52. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/management/commands/labull_access.py +0 -0
  53. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/management/commands/labull_blocktype.py +0 -0
  54. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/management/commands/labull_contract.py +0 -0
  55. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/management/commands/labull_requires.py +0 -0
  56. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/migrations/0001_initial.py +0 -0
  57. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/migrations/0002_remove_magicblock_prev_files_and_more.py +0 -0
  58. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/migrations/0003_magicblock_tags.py +0 -0
  59. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/migrations/0004_magicblock_detail.py +0 -0
  60. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/migrations/0005_labullrole.py +0 -0
  61. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/migrations/0006_default_groups_permissions.py +0 -0
  62. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/migrations/0007_no_builtin_permissions.py +0 -0
  63. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/migrations/__init__.py +0 -0
  64. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/models.py +0 -0
  65. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/permissions.py +0 -0
  66. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/settings.py +0 -0
  67. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework/virtual.py +0 -0
  68. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework.egg-info/SOURCES.txt +0 -0
  69. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework.egg-info/dependency_links.txt +0 -0
  70. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework.egg-info/requires.txt +0 -0
  71. {labull_framework-3.0.5 → labull_framework-3.0.8}/labull_framework.egg-info/top_level.txt +0 -0
  72. {labull_framework-3.0.5 → labull_framework-3.0.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 3.0.5
3
+ Version: 3.0.8
4
4
  Summary: Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks
5
5
  Author: Labull Framework
6
6
  License-Expression: MIT
@@ -1583,7 +1583,7 @@ def dev_doc(blocks: int) -> str:
1583
1583
  9. **业务功能说明写在块包内的 `README.open.md` 里、不在清单里**(`brief` 只是清单里那句短话):写业务用途、谁在什么场景用、输入输出的含义与边界;`detail` 是遗留键,老块里还留着的话读回时会搬进这个文件。**它每一趟都要跟着改**:这一版块变成什么样,它就写成什么样 —— 老功能的说明不做保留地删掉(与第 5 节"改老功能就整段删掉旧的"是同一条规矩,只是落在文档上)。
1584
1584
  10. **依赖声明写在清单的 `requires` 里**(可选):项目模型 / pip 包 / settings 键 / `BLOCK_*` 变量 / 跨块地址这些用到就声明 —— 它**不授予任何能力**,只是让平台把"外部依赖"核对一遍、换项目或改配置后替你**报出来**;草稿不用手写,导出的 `requires.open.yaml` 会把建议写在注释里,读回时**只合并**那一处。
1585
1585
  11. **块完全独立、不许跨块**:块与块之间**不共享文件、不共享代码、不互相引用** —— 你要用的东西就在这个块自己的文件夹里。规矩适用于所有方向:不去读 / 引另一个块的任何文件,不把"公共文件"放在 `workspace/` 顶层或两个块之间指望两个块共用,也不假设自己的代码会被别的块用到。**一份代码要服务两个块就只有两个合法去处**:各块里**各写一份**(复制),或者放进**项目**里的公共模块(那是普通 Python,块后端能 import —— 见第 1 节 `requires` 怎么报备;⚠️ 加这个公共模块要动**项目**的代码,按规矩 13 先问用户)。前端更简单:包内相对路径引用的文件**必须在这个块文件夹里**,`../` 爬到别的块去的一律不算(宿主也不会替你内联它)。
1586
- 12. **所有弹窗都走 `MagicBlockDialogs`**:块里**不许自己画弹窗 / 浮层 / 遮罩**(`position: fixed` / `100vh` / 手写 `.modal` 那一类)。要弹就 `await magicBlock.dialog({{…}})`,由**应用根上挂的那一个 `<MagicBlockDialogs />`** 在真视口里渲染(块可以有好几屏高,自己画的浮层会被摆到看不见的地方);要自己写整篇就传 `{{ html }}`,里面用 `magicBlock.done({{…}})` 交回选择。应用那边没挂 `<MagicBlockDialogs />` 时调用会**当场报一句人话**(不是静默失败)。
1586
+ 12. **所有弹窗都走 `MagicBlockDialogs`**:块里**不许自己画弹窗 / 浮层 / 遮罩**(`position: fixed` / `100vh` / 手写 `.modal` 那一类)。要弹就 `await magicBlock.dialog({{…}})`,由**应用根上挂的那一个 `<MagicBlockDialogs />`** 在真视口里渲染(块可以有好几屏高,自己画的浮层会被摆到看不见的地方);要自己写整篇就传 `{{ html }}`,里面用 `magicBlock.done({{…}})` 交回选择。**弹窗多大不用你算**:`{{ html }}` 那一型的面板**按内容定高**(短的就小的、长了到 `90dvh` 封顶后整篇自滚),`size.height` 只是**上限**不是"必须占这么高";而且**露面之后尺寸固定**(内容再变多只在里面滚,不会"先小后大")⇒ 异步取的数据要在 `dialog()` 之前 await 好。**要看原图 / PDF 别塞进 `html`**(那条 512 KB 上限会把图缩掉):走 `dialog({{ html, assets: {{ 名字: dataURL }} }})` + 弹窗里 `await magicBlock.assets()`。应用那边没挂 `<MagicBlockDialogs />` 时调用会**当场报一句人话**(不是静默失败)。
1587
1587
  13. **一趟只动一个块,越界先问**:每次开发**只在当前 workspace 的一个块里**做。要动**第二个块**、或者要改**平台的前后端代码**(`backend/` / `frontend/`),**必须先向用户提问、得到同意才继续**;块里**不许把平台代码当自己的改**(它不进块包,还会影响别的块)。展开见 2.1。
1588
1588
 
1589
1589
  > 拿不准就把"我打算这么做"写下来先说一遍 —— 这里面的多数是"想清楚就不会犯"的。
@@ -68,6 +68,28 @@ class BlockNotFound(Exception):
68
68
  self.status = 404
69
69
 
70
70
 
71
+ def block_type_of(files: dict[str, str] | None) -> tuple[str, str]:
72
+ """这一包的**块类型**:`(清单里写的名字, 中文标签)`。
73
+
74
+ 🔴 回话里的 `type` 就是它:管理台按它决定"这一块要不要多显示一段"(流程块要画流程图,普通块不要)。
75
+ 🔴 **不是判据**:这里不判"这个类型合不合法"(那是 `manifest` 与 `blocktypes.check()` 的事),
76
+ 只把清单里那一格读出来。
77
+ ⚠️ 清单读不出来(缺 / 写坏)或类型不在注册表里 ⇒ `('', '')`:那是"不知道是哪一类",
78
+ 与"这是普通块"是两件事 —— 前端因此不会给一个坏清单画流程图。
79
+ """
80
+ from . import blocktypes # 函数内 import:类型注册表要 import 各类型模块(含子进程那一套)
81
+
82
+ try:
83
+ found, problems = manifest.read_manifest(files or {})
84
+ if problems or not found:
85
+ return '', ''
86
+ name = blocktypes.declared(found)
87
+ except Exception: # noqa: BLE001 —— 清单读不出来不该让整份清单请求炸掉
88
+ return '', ''
89
+ kind = blocktypes.get(name)
90
+ return name, (kind.label if kind is not None else '')
91
+
92
+
71
93
  class MagicBlockService:
72
94
  """块的存储与下发(无状态:状态在库里 + 注册表的缓存里)。"""
73
95
 
@@ -154,6 +176,10 @@ class MagicBlockService:
154
176
  🔴 这里算的 `code`(有效代码行数)**只回数字与一份按文件的明细,不回正文**:
155
177
  控制台要的是"这块有多大",而不是把每个块的代码都拉到浏览器里
156
178
  (20 个块一起拉就是几百 KB 的白流量)。
179
+ 🔴 `type` / `typeLabel` 是**块类型**(清单里的 `type:`):控制台按它决定"这一块要多显示什么"
180
+ —— 流程块点开要画流程图(`GET /magic-blocks/<id>/flow`),普通块不多显示任何东西。
181
+ ⚠️ 这里只读清单那一格(`block_type_of`),**不跑 `flow.py`**:跑一遍是子进程、每块 0.4 秒,
182
+ 挂在这条"打开抽屉就要一次"的口上不合适。
157
183
  🔴 算它必须读 `files` 那一列(清单上别的字段都不用),所以下面用 `.only(...)`
158
184
  把列收窄到"清单要的 + files"—— `health_issues` 那几列在大块上不小。
159
185
  """
@@ -179,6 +205,7 @@ class MagicBlockService:
179
205
  tags = self.normalize_tags(row.tags)
180
206
  if wanted and wanted not in [item.lower() for item in tags]:
181
207
  continue
208
+ type_name, type_label = block_type_of(row.files or {})
182
209
  out.append(
183
210
  {
184
211
  'id': row.id,
@@ -195,6 +222,11 @@ class MagicBlockService:
195
222
  'enabled': _enabled_of(row),
196
223
  'disabledReason': row.disabled_reason or '',
197
224
  'issues': health_issues(row),
225
+ # 🔴 **块类型**(清单里 `type:` 那一格):控制台按它决定"点开这一块要多显示什么"。
226
+ # 普通块(plain,缺省)不需要多余的展示;流程块要画 `flow.py` 里那条流程图。
227
+ # ⚠️ 只读清单,不执行 `flow.py`(见 docstring 最后那一条)。
228
+ 'type': type_name,
229
+ 'typeLabel': type_label,
198
230
  'updatedAt': iso(row.updated_at),
199
231
  'fileNames': sorted_files(row.files or {}),
200
232
  # 🔴 **代码行数**(控制台那一栏):`{total, files, byFile}`。
@@ -529,6 +561,33 @@ class MagicBlockService:
529
561
  raise SaveRejected('写完详情之后读不回来,请检查数据库连接')
530
562
  return saved
531
563
 
564
+ # ──────────────────────────── 流程声明(流程图那一条口) ────────────────────────────
565
+ def flow(self, block_id: str) -> dict:
566
+ """这一块的**流程声明快照**(`flow.py` 里那条 `Flow` 的归一化形状;管理台画流程图用)。
567
+
568
+ :returns: `{id, type, typeLabel, spec}` —— `spec` 是 `Flow.spec()` 那一份纯数据,
569
+ 不是流程块 / `flow.py` 跑不起来 / 注册了不止一条 ⇒ **`None`**(不是 500)。
570
+
571
+ 🔴 为什么是**单独一条口**、不塞进 `index()`:这份快照要在**子进程**里把 `flow.py` 跑一遍
572
+ (`blocktypes.spec_of`,结论按整包正文的摘要缓存)。挂在"打开抽屉就会请求"的清单上,
573
+ 块一多就是每块 0.4 秒 × N;而点开某一块时等一次是值得的。
574
+ 🔴 拿不到快照**不报错**:流程图那一栏会照实说"算不出来",原因在那几条体检结论里
575
+ (`health_issues` 早把 `flow.py` 的报错原文列出来了)—— 这里再造一份文案就是第二处真源。
576
+ """
577
+ row = self.get(block_id)
578
+ if row is None:
579
+ raise BlockNotFound(f'没有这个 magic block:{block_id}')
580
+ files = dict(row.files or {})
581
+ from . import blocktypes # 函数内 import:类型注册表会 import 各类型模块(含子进程那一套)
582
+
583
+ name, label = block_type_of(files)
584
+ return {
585
+ 'id': row.id,
586
+ 'type': name,
587
+ 'typeLabel': label,
588
+ 'spec': blocktypes.spec_of(files),
589
+ }
590
+
532
591
  # ──────────────────────────── 版本史 ────────────────────────────
533
592
  def versions(self, block_id: str) -> list[dict]:
534
593
  """这一块的版本列表(**新的在前**):`number / files 数 / 谁传的 / 什么时候 / 是不是当前**。
@@ -252,7 +252,11 @@ def cross(items: list[tuple[str, dict[str, Any]]]) -> list[tuple[str, str]]:
252
252
 
253
253
  :param items: `[(块标签, 归一化快照)]`,**全部副本**(至少两个才会被调)。
254
254
 
255
- 🔴 第一个是**权威**(按块标签排序,所以"谁是权威"与加载顺序无关)。
255
+ 🔴 **先按 `flow_type` 分组,每组各选一个权威**:块类型(`workflow`)回答的是"哪一类块",
256
+ 而"同一条流程的副本"是按 `flow_type` 认的。**不分组的话**,报销单 / 付款单 / 定价单 /
257
+ 调整单会被互相当成"权威副本" ⇒ 每升一次框架,那几个不相干的块就被整块停用
258
+ (3.0.5 的事故:`报销单流程` 被说成"与权威副本 `差旅申请单流程` 说法不一致")。
259
+ 🔴 组内第一个是**权威**(按块标签排序,所以"谁是权威"与加载顺序无关)。
256
260
  🔴 比的是流程骨架:状态码 / 动作码 / `to` / `kind` / `candidates` / 必填意见 / 解绑 /
257
261
  `confirm_permissions` / 中文名 / `ui` / `ui_by` / `moves_by` / `assignees` / `legacy` / `form` / `editable`,
258
262
  以及 `handler_kind`。**不比**注释、docstring、钩子对象,也不比**项目的**
@@ -260,23 +264,35 @@ def cross(items: list[tuple[str, dict[str, Any]]]) -> list[tuple[str, str]]:
260
264
  🔴 报错要逐条列出差异(哪个块、哪个字段、各自的值)并给两种改法:改一致,或者**统一成一份**
261
265
  (派生机制刻意不做:那一份差异一旦允许"显式声明",就又多了一处要说两遍的事实)。
262
266
  """
263
- if len(items) < 2:
264
- return []
265
- ordered = sorted(items, key=lambda item: str(item[0]))
266
- authority_label, authority = ordered[0]
267
+ # ── 分组:键是 `flow_type`(快照里那个 `type`),不是块类型 ──
268
+ # ⚠️ 取不到 `type` 的条目**不参与**:那种块的 `spec()` 本来就该在构造期就抛,
269
+ # 走到这里说明它的快照不完整 —— 猜一个组名去比只会制造假差异。
270
+ groups: dict[str, list[tuple[str, dict[str, Any]]]] = {}
271
+ for label, spec in items or ():
272
+ flow_type = str((spec or {}).get('type') or '').strip()
273
+ if not flow_type:
274
+ continue
275
+ groups.setdefault(flow_type, []).append((label, spec))
267
276
  out: list[tuple[str, str]] = []
268
- for label, spec in ordered[1:]:
269
- diffs = _diff(spec, authority)
270
- if not diffs:
277
+ for flow_type in sorted(groups):
278
+ group = groups[flow_type]
279
+ # 只有一份副本 ⇒ 没有"跨块"可比(单份流程自己的判据在构造期就判完了)。
280
+ if len(group) < 2:
271
281
  continue
272
- out.append(
273
- (
274
- label,
275
- f'块「{label}」里的流程 {spec.get("type")} 与权威副本「{authority_label}」说法不一致:'
276
- f'{";".join(diffs)}。改法:把两边改成同一个说法(同一套流程只有一种写法),'
277
- f'或者让其中一边删掉复制的那份流程声明。',
282
+ ordered = sorted(group, key=lambda item: str(item[0]))
283
+ authority_label, authority = ordered[0]
284
+ for label, spec in ordered[1:]:
285
+ diffs = _diff(spec, authority)
286
+ if not diffs:
287
+ continue
288
+ out.append(
289
+ (
290
+ label,
291
+ f'块「{label}」里的流程 {spec.get("type")} 与权威副本「{authority_label}」说法不一致:'
292
+ f'{";".join(diffs)}。改法:把两边改成同一个说法(同一套流程只有一种写法),'
293
+ f'或者让其中一边删掉复制的那份流程声明。',
294
+ )
278
295
  )
279
- )
280
296
  return out
281
297
 
282
298
 
@@ -27,6 +27,7 @@ from .service import (
27
27
  MagicBlockService,
28
28
  SaveRejected,
29
29
  api_dts,
30
+ block_type_of,
30
31
  health_issues,
31
32
  iso,
32
33
  sorted_files,
@@ -137,6 +138,7 @@ def _row_view(row: Any) -> dict:
137
138
  """
138
139
  service = MagicBlockService()
139
140
  routes, dts = service.api_manifest(row.id)
141
+ type_name, type_label = block_type_of(row.files or {})
140
142
  return {
141
143
  'id': row.id,
142
144
  'module': row.module,
@@ -147,6 +149,10 @@ def _row_view(row: Any) -> dict:
147
149
  'detail': row.detail or '',
148
150
  # 依赖声明(清单里那个可选的 `requires`):块详情里那一栏"依赖"读它。
149
151
  'requires': requires.requires_declared(row.files or {})[0],
152
+ # 块类型(清单里 `type:` 那一格)+ 中文标签:控制台按它决定"这一块要多显示什么"
153
+ # (流程块点开画流程图,普通块不多显示)。空串 = 清单读不出来 / 类型不在注册表里。
154
+ 'type': type_name,
155
+ 'typeLabel': type_label,
150
156
  'enabled': row.enabled is not False,
151
157
  'disabledReason': row.disabled_reason or '',
152
158
  'issues': health_issues(row),
@@ -275,6 +281,28 @@ def detail_field_view(request: HttpRequest, block_id: str) -> HttpResponse:
275
281
  return _json(_row_view(row))
276
282
 
277
283
 
284
+ @edit_only
285
+ def flow_view(request: HttpRequest, block_id: str) -> HttpResponse:
286
+ """`GET /magic-blocks/<id>/flow` —— 这一块的**流程声明快照**(管理台画流程图用)。
287
+
288
+ 回话:`{id, type, typeLabel, spec}`。`spec` 是 `flow.py` 里那条 `Flow` 的归一化形状
289
+ (状态 / 动作 / 谁能办 / 内容 key,见 `flow.Flow.spec()`),`None` = 不是流程块 / 算不出来。
290
+
291
+ 🔴 为什么单独一条口(用户要求"workflow 块点进去可以看到由 flow.py 构建的流程图"):
292
+ 快照要在**子进程**里真的把 `flow.py` 跑一遍(`blocktypes.spec_of`,按包正文摘要缓存)。
293
+ 塞进 `GET /magic-blocks/index` 的话,打开抽屉就要为每个块付一次子进程的钱。
294
+ 🔴 `spec` 拿不到时回 **200 + `null`**,不是 404 / 500:块本身没问题,只是"这条流程声明算不出来"
295
+ —— 原因已经在那几条体检结论里了(`issues` 带 `flow.py` 的报错原话),这里不另写一份话。
296
+ """
297
+ refused = _method(request, 'GET')
298
+ if refused is not None:
299
+ return refused
300
+ try:
301
+ return _json(MagicBlockService().flow(block_id))
302
+ except BlockNotFound as exc:
303
+ return problem(exc.message, 404)
304
+
305
+
278
306
  @edit_only
279
307
  def versions_view(request: HttpRequest, block_id: str, number: str | None = None) -> HttpResponse:
280
308
  """`/magic-blocks/<id>/versions[/<number>]` —— 版本史的读、切、删、下。
@@ -79,6 +79,9 @@ urlpatterns: list[Any] = [
79
79
  # 🔴 只改详情介绍那一条,同一个理由(详情是"给人读的一段话",不是代码)。
80
80
  # ⚠️ 与 `labull-blocks-detail`(`magic-blocks/<block_id>` 那一行本身)不是一回事。
81
81
  path('magic-blocks/<str:block_id>/detail', block_views.detail_field_view, name='labull-blocks-detail-field'),
82
+ # 🔴 流程声明那一条(管理台画流程图):`flow.py` 的归一化快照,**按需取、一次一块**
83
+ # (算一次要起子进程跑 `flow.py`,不能挂在"打开抽屉就要请求"的那条清单口上)。
84
+ path('magic-blocks/<str:block_id>/flow', block_views.flow_view, name='labull-blocks-flow'),
82
85
  # 🔴 版本史那一条排在 `<block_id>` **之前**(同一个理由:路径前缀更长的那条要先匹配)。
83
86
  # `GET/POST/DELETE …/versions[/<n>]` 五个动作都在同一个视图里分流(见 `versions_view`)。
84
87
  re_path(r'^magic-blocks/(?P<block_id>[^/]+)/versions(?:/(?P<number>\d+))?$',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 3.0.5
3
+ Version: 3.0.8
4
4
  Summary: Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks
5
5
  Author: Labull Framework
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "labull-framework"
7
- version = "3.0.5"
7
+ version = "3.0.8"
8
8
  description = "Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks"
9
9
  readme = "README-pypi.md"
10
10
  requires-python = ">=3.10"