labull-framework 1.0.4__tar.gz → 1.1.1__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 (66) hide show
  1. {labull_framework-1.0.4/labull_framework.egg-info → labull_framework-1.1.1}/PKG-INFO +1 -1
  2. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/env.py +238 -235
  3. labull_framework-1.1.1/labull_framework/flow.py +958 -0
  4. labull_framework-1.1.1/labull_framework/magic_block/blockcheck.py +538 -0
  5. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/blockpackage.py +28 -1
  6. labull_framework-1.1.1/labull_framework/magic_block/blocktypes.py +402 -0
  7. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/deepcheck.py +1191 -1615
  8. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/devkit.py +235 -102
  9. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/export.py +1380 -1331
  10. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/health.py +329 -325
  11. labull_framework-1.1.1/labull_framework/magic_block/manifest.py +814 -0
  12. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/parallel.py +1 -1
  13. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/registry.py +2 -2
  14. labull_framework-1.1.1/labull_framework/magic_block/requires.py +829 -0
  15. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/sdk.py +376 -352
  16. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/service.py +11 -8
  17. labull_framework-1.1.1/labull_framework/magic_block/types/__init__.py +2 -0
  18. labull_framework-1.1.1/labull_framework/magic_block/types/plain.py +20 -0
  19. labull_framework-1.1.1/labull_framework/magic_block/types/workflow.py +394 -0
  20. labull_framework-1.1.1/labull_framework/magic_block/validate.py +238 -0
  21. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/views.py +663 -663
  22. labull_framework-1.1.1/labull_framework/management/commands/labull_blocktype.py +203 -0
  23. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/management/commands/labull_requires.py +7 -7
  24. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/migrations/0004_magicblock_detail.py +1 -1
  25. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/models.py +253 -253
  26. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/settings.py +1 -1
  27. {labull_framework-1.0.4 → labull_framework-1.1.1/labull_framework.egg-info}/PKG-INFO +1 -1
  28. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework.egg-info/SOURCES.txt +9 -0
  29. {labull_framework-1.0.4 → labull_framework-1.1.1}/pyproject.toml +4 -1
  30. labull_framework-1.0.4/labull_framework/magic_block/validate.py +0 -2303
  31. {labull_framework-1.0.4 → labull_framework-1.1.1}/LICENSE +0 -0
  32. {labull_framework-1.0.4 → labull_framework-1.1.1}/MANIFEST.in +0 -0
  33. {labull_framework-1.0.4 → labull_framework-1.1.1}/README-pypi.md +0 -0
  34. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/__init__.py +0 -0
  35. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/admin.py +0 -0
  36. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/apps.py +0 -0
  37. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/auth/__init__.py +0 -0
  38. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/auth/backend.py +0 -0
  39. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/auth/bootstrap.py +0 -0
  40. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/auth/sso.py +0 -0
  41. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/auth/views.py +0 -0
  42. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/canvas/__init__.py +0 -0
  43. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/canvas/data.py +0 -0
  44. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/canvas/gitinfo.py +0 -0
  45. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/canvas/shape_ast.py +0 -0
  46. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/canvas/static/canvas.css +0 -0
  47. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/canvas/static/canvas.js +0 -0
  48. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/canvas/static/page.html +0 -0
  49. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/canvas/views.py +0 -0
  50. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/jsonio.py +0 -0
  51. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/__init__.py +0 -0
  52. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/dispatch.py +0 -0
  53. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/magic_block/subproc.py +0 -0
  54. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/management/__init__.py +0 -0
  55. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/management/commands/__init__.py +0 -0
  56. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/management/commands/labull_contract.py +0 -0
  57. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/migrations/0001_initial.py +0 -0
  58. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/migrations/0002_remove_magicblock_prev_files_and_more.py +0 -0
  59. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/migrations/0003_magicblock_tags.py +0 -0
  60. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/migrations/__init__.py +0 -0
  61. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/urls.py +0 -0
  62. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework/virtual.py +0 -0
  63. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework.egg-info/dependency_links.txt +0 -0
  64. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework.egg-info/requires.txt +0 -0
  65. {labull_framework-1.0.4 → labull_framework-1.1.1}/labull_framework.egg-info/top_level.txt +0 -0
  66. {labull_framework-1.0.4 → labull_framework-1.1.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 1.0.4
3
+ Version: 1.1.1
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
@@ -1,235 +1,238 @@
1
- """环境变量的唯一入口:读值、加载 `.env`、给块的配置白名单,以及**有哪几个变量**这份清单。
2
-
3
- 🔴 `.env` 解析器只有这一处(Django 自己不读 `.env`,标准库也没有这回事)。
4
- 两处各解析一遍的后果是"平台读得到、你的代码读不到" —— 最难查的一类不一致。
5
- 🔴 `ENV_ITEMS` 是"平台读哪些环境变量"的**唯一真源**:开发包里的 `backend.lock.pyi`
6
- 照它铺一份给块作者看,登录那一组的必填清单与 `.env` 例子也从它取(见 `auth/sso.py`)。
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import os
12
- import re
13
- from dataclasses import dataclass
14
- from pathlib import Path
15
- from typing import Mapping
16
-
17
- from .magic_block.sdk import BLOCK_CONFIG_PREFIX, BLOCK_DEEPCHECK, BLOCK_PROBE, BLOCK_REQUIRES
18
-
19
- #: 给块的配置前缀:`BLOCK_OCR_KEY=abc` ⇒ 块后端里 `ctx.config['OCR_KEY'] == 'abc'`。
20
- #: 🔴 这是**白名单**(不是黑名单):想让块读一项,就给那个变量加这个前缀,不用改库的代码。
21
- #: ⚠️ 字面量的真源在 `sdk`(上面那行 import)—— 这里**不许**再写一遍 `'BLOCK_'`。
22
-
23
- #: `.env` 里一行的形状。允许 `export` 前缀、行首缩进、`=` 两边空白;
24
- #: 🔴 **不剥行内注释**(值里真带 `#` 的密钥不该被截断)、**不做变量展开**(那会让"谁先谁后"变成隐式规则)。
25
- _ENV_LINE = re.compile(r'^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$')
26
-
27
-
28
- def load_env_file(path: str | Path) -> None:
29
- """把一份 `.env` 并进 `os.environ`(在 `settings.py` 的**第一行**调一次)。
30
-
31
- 🔴 必须排在读 `os.environ` 之前:顺序反了,`.env` 里给的值读不到,
32
- 而报出来的症状是"我配了,它还是不生效"。
33
- 🔴 `.env` **覆盖**进程环境:它是项目的配置真源,而进程里可能留着同名的外部变量。
34
- 🔴 用 `utf-8-sig` 读:编辑器存"UTF-8 带 BOM"时那个 BOM 会粘在第一个键名前面,
35
- 而 `^\\s*` 不匹配它 ⇒ 第一行**静默消失**(症状是"我明明配了它")。
36
- ⚠️ 文件不在 = 什么都不做(不抛、不报):缺配置该由**用到它的那一刻**报出来。
37
- """
38
- source = Path(path)
39
- if not source.is_file():
40
- return
41
- values: dict[str, str] = {}
42
- for line in source.read_text(encoding='utf-8-sig').splitlines():
43
- match = _ENV_LINE.match(line)
44
- if not match:
45
- continue # 空行与 `#` 注释都走这一条
46
- raw = match.group(2).strip()
47
- quoted = len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in ('"', "'")
48
- values[match.group(1)] = raw[1:-1] if quoted else raw
49
- os.environ.update(values)
50
-
51
-
52
- def read_env(name: str) -> str:
53
- """读一个环境变量(空串当没写 —— `.env` 里 `KEY=` 是常态),去掉首尾空白。"""
54
- return str(os.environ.get(name, '') or '').strip()
55
-
56
-
57
- def block_config(env: Mapping[str, str] | None = None) -> dict[str, str]:
58
- """给块的配置:带 `BLOCK_` 前缀的那几个,**剥掉前缀当键**。
59
-
60
- 🔴 没配的键在 `ctx.config` 上**不存在**(别拿 `None` 当默认值使,先判 `'X' in ctx.config`);
61
- 配成空串的**照给**(`BLOCK_X=` ⇒ `ctx.config['X'] == ''`)。
62
- 🔴 剥完是空串的**不算键**(`BLOCK_=` 不可能是名字)。
63
- 🔴 别的变量一个都不给块 —— 数据库连接串、SSO 密钥都不在这里。
64
- """
65
- source = os.environ if env is None else env
66
- result: dict[str, str] = {}
67
- for name, value in source.items():
68
- if not name.startswith(BLOCK_CONFIG_PREFIX):
69
- continue
70
- key = name[len(BLOCK_CONFIG_PREFIX):]
71
- if key:
72
- result[key] = value
73
- return result
74
-
75
-
76
- # ──────────────────────────── "有哪几个环境变量"这份清单 ────────────────────────────
77
-
78
- #: 登录那一组的组名。🔴 `auth/sso.py` 按它取必填清单(**不抄第二份名单**)。
79
- LOGIN_GROUP = '登录(Authing OIDC)'
80
-
81
-
82
- @dataclass(frozen=True)
83
- class EnvItem:
84
- """一个环境变量在**开发包**里被看到的样子。
85
-
86
- 🔴 `hint` 是"能直接粘进 `.env` 的一行":缺必填时那句报错就照它说(见 `auth/sso.py`)——
87
- 只说"缺了"的报错,用户还得自己去翻文档才知道那一行该怎么写。
88
- 🔴 **这里永远不写值**:这份清单会铺进开发包(跟着块作者的编辑器走),而环境里有
89
- 数据库连接串与登录密钥。块的配置面是白名单,同一个道理。
90
- """
91
-
92
- #: 变量名。⚠️ 前缀那一条写的是 `BLOCK_*` —— 它不是一个真名字,是一族名字的前缀。
93
- name: str
94
- #: 这个变量管什么(一句人话)。
95
- brief: str
96
- #: 能直接粘进 `.env` 的一行(没有例子就是空串)。
97
- hint: str = ''
98
- #: 必填吗(缺了就跑不起来的那几项)。
99
- required: bool = False
100
- #: 分组(开发包里按它分段;"哪几项是登录用的"也从这里取)。
101
- group: str = '平台'
102
-
103
-
104
- #: 平台读的**全部**环境变量 —— 唯一真源。
105
- #:
106
- #: 🔴 它是一份**清单**,不是"读值的入口":各处仍然各读各的(`read_env('DATABASE_URL')` 之类)。
107
- #: 两者对不上的风险由 `tools/sdk-check-py.mjs` 兜住:它扫一遍库里所有 `read_env('<名字>')`
108
- #: 字面量,漏在这份清单外的当场报红。
109
- ENV_ITEMS: tuple[EnvItem, ...] = (
110
- # ── 登录(Authing OIDC):这四项不配就没人能登录 ──
111
- EnvItem(
112
- name='SSO_ISSUER',
113
- brief='Authing 的 OIDC 发证地址(OIDC discovery 就在它下面)',
114
- hint='SSO_ISSUER=https://<你的域名>.authing.cn/oidc',
115
- required=True,
116
- group=LOGIN_GROUP,
117
- ),
118
- EnvItem(
119
- name='SSO_CLIENT_ID',
120
- brief='Authing 应用的 App ID',
121
- hint='SSO_CLIENT_ID=<Authing 应用的 App ID>',
122
- required=True,
123
- group=LOGIN_GROUP,
124
- ),
125
- EnvItem(
126
- name='SSO_CLIENT_SECRET',
127
- brief='Authing 应用的 App Secret(**只在服务端**用,别带进前端)',
128
- hint='SSO_CLIENT_SECRET=<Authing 应用的 App Secret>',
129
- required=True,
130
- group=LOGIN_GROUP,
131
- ),
132
- EnvItem(
133
- name='SSO_REDIRECT_URI',
134
- brief='登录回调地址(必须与 Authing 应用里登记的那个逐字一致)',
135
- hint='SSO_REDIRECT_URI=http://localhost:8000/auth/callback',
136
- required=True,
137
- group=LOGIN_GROUP,
138
- ),
139
- EnvItem(
140
- name='SSO_WEB_ORIGIN',
141
- brief='登录成功后把人送回哪个前端地址(不写就用 auth/sso.py 里的默认值)',
142
- hint='SSO_WEB_ORIGIN=http://localhost:5173',
143
- group=LOGIN_GROUP,
144
- ),
145
- # ── 跨源与数据库 ──
146
- EnvItem(
147
- name='CORS_ORIGIN',
148
- brief='允许跨源访问的前端地址(逗号分隔);它同时是 CSRF 信任名单的来源',
149
- hint='CORS_ORIGIN=http://localhost:5173',
150
- group='跨源与数据库',
151
- ),
152
- EnvItem(
153
- name='DATABASE_URL',
154
- brief='项目数据库连接串(postgres://… 形式);不写就用项目自己 settings 里的 DATABASES',
155
- hint='DATABASE_URL=postgres://user:password@127.0.0.1:5432/app',
156
- group='跨源与数据库',
157
- ),
158
- # ── 平台开关 ──
159
- EnvItem(
160
- name='BOOTSTRAP_ADMIN_WORK_IDS',
161
- brief='管理员名单:启动时把这些人设成 is_staff,名单之外的一律清掉(逗号分隔,上限 200;没配就是没有管理员)',
162
- hint='BOOTSTRAP_ADMIN_WORK_IDS=1001',
163
- group='平台开关',
164
- ),
165
- EnvItem(
166
- name='LABULL_SCHEMA_CANVAS',
167
- brief='数据模型那一页的开关:on 常开 / auto 只在 DEBUG 开 / 其余(默认)不开',
168
- hint='LABULL_SCHEMA_CANVAS=on',
169
- group='平台开关',
170
- ),
171
- EnvItem(
172
- name='LABULL_CONTRACT_PATH',
173
- brief='模型形状那份 JSON 的落点(**兜底**用:画布的基线默认是从提交里的源码算的;默认 .labull/contract.json)',
174
- hint='LABULL_CONTRACT_PATH=.labull/contract.json',
175
- group='平台开关',
176
- ),
177
- EnvItem(
178
- name='LABULL_BASELINE_REV',
179
- brief="画布拿哪一版当\"上一版\":留空=HEAD~1(上一次提交改了什么)/ head=只看没提交的改动 / off=不做差异 / 也可以给一个提交号或分支",
180
- hint='LABULL_BASELINE_REV=HEAD~1',
181
- group='平台开关',
182
- ),
183
- EnvItem(
184
- name='LABULL_BASELINE_IGNORE_APPS',
185
- brief='画布比对时**不参与**的 app(逗号/空格分隔,例如 labull_framework)—— 这些 app 的表照样画,只是不带"上一版 vs 现在"的差异色',
186
- hint='LABULL_BASELINE_IGNORE_APPS=labull_framework',
187
- group='平台开关',
188
- ),
189
- # 🔴 下面两条是**体检**那一侧的应急开关(名字的真源在 `magic_block/sdk.py`,不在这里手抄):
190
- # 默认都开 —— 关掉等于"字段改名这类问题回到运行期才现形",只该在排查误报时临时用一下。
191
- EnvItem(
192
- name=BLOCK_DEEPCHECK,
193
- brief='块的「引用深检」总开关:off 退回只判形状(字段 / 模型改了名就不再提前报出来)',
194
- hint=f'{BLOCK_DEEPCHECK}=off',
195
- group='平台开关',
196
- ),
197
- EnvItem(
198
- name=BLOCK_PROBE,
199
- brief='体检里「把 handler 真跑一遍」那一半的开关:off 只留静态判据(不执行任何块代码)',
200
- hint=f'{BLOCK_PROBE}=off',
201
- group='平台开关',
202
- ),
203
- # 🔴 依赖声明(`requires`)那一层的开关(名字的真源在 `magic_block/sdk.py`):
204
- # 默认 `error`(1.0.1 起)—— "先警告、后强制"里的**后强制**已经落地了。
205
- # ⚠️ 代价如实说:库里那些**还没声明**的存量块会集体判不合格(全部停用),
206
- # 所以升级前先用 `manage.py labull_requires --write` 把扫得到的声明批量补进库
207
- # (那条命令就是为这个默认值而存在的:它是这条路上唯一的恢复口)。
208
- EnvItem(
209
- name=BLOCK_REQUIRES,
210
- brief='块「依赖声明」(requires)那一层的开关:off 整段不跑 / warn 只提示不拦(要显式写)/ error(默认)变成硬判据',
211
- hint=f'{BLOCK_REQUIRES}=warn',
212
- group='平台开关',
213
- ),
214
- # 🔴 体检那条链一次开几个口子(名字的真源在 `magic_block/parallel.py` 的 `ENV_WORKERS`)。
215
- # ⚠️ 它是**性能旋钮、不是判据**:写错一个数(0 / 64 / 一句乱码)会被夹到 1..8,不该让体检报错。
216
- # 它在这里的意义是"使用者的清单里能看见这个开关"(`backend.lock.pyi` 就是按 ENV_ITEMS 铺的)。
217
- EnvItem(
218
- name='LABULL_BLOCK_WORKERS',
219
- brief='启动体检 / 全量校验一次开几个子进程(默认 4,夹在 1..8):机器小就往下调,写 1 退回串行',
220
- hint='LABULL_BLOCK_WORKERS=2',
221
- group='平台开关',
222
- ),
223
- # ── 块的配置面 ──
224
- EnvItem(
225
- name=f'{BLOCK_CONFIG_PREFIX}*',
226
- brief='块的配置面:**只有**带这个前缀的变量才进 ctx.config(前缀剥掉当键,别的变量一个都不给块)',
227
- hint=f'{BLOCK_CONFIG_PREFIX}OCR_KEY=xxx',
228
- group='块的配置',
229
- ),
230
- )
231
-
232
-
233
- def required_items(group: str) -> tuple[EnvItem, ...]:
234
- """某一组里必填的那几项(顺序 = `ENV_ITEMS` 里的顺序,所以报错顺序稳定)。"""
235
- return tuple(item for item in ENV_ITEMS if item.group == group and item.required)
1
+ """环境变量的唯一入口:读值、加载 `.env`、给块的配置白名单,以及**有哪几个变量**这份清单。
2
+
3
+ 🔴 `.env` 解析器只有这一处(Django 自己不读 `.env`,标准库也没有这回事)。
4
+ 两处各解析一遍的后果是"平台读得到、你的代码读不到" —— 最难查的一类不一致。
5
+ 🔴 `ENV_ITEMS` 是"平台读哪些环境变量"的**唯一真源**:开发包里的 `backend.lock.pyi`
6
+ 照它铺一份给块作者看,登录那一组的必填清单与 `.env` 例子也从它取(见 `auth/sso.py`)。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import re
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Mapping
16
+
17
+ from .magic_block.sdk import BLOCK_CONFIG_PREFIX, BLOCK_DEEPCHECK, BLOCK_REQUIRES, BLOCK_TYPE
18
+
19
+ #: 给块的配置前缀:`BLOCK_OCR_KEY=abc` ⇒ 块后端里 `ctx.config['OCR_KEY'] == 'abc'`。
20
+ #: 🔴 这是**白名单**(不是黑名单):想让块读一项,就给那个变量加这个前缀,不用改库的代码。
21
+ #: ⚠️ 字面量的真源在 `sdk`(上面那行 import)—— 这里**不许**再写一遍 `'BLOCK_'`。
22
+
23
+ #: `.env` 里一行的形状。允许 `export` 前缀、行首缩进、`=` 两边空白;
24
+ #: 🔴 **不剥行内注释**(值里真带 `#` 的密钥不该被截断)、**不做变量展开**(那会让"谁先谁后"变成隐式规则)。
25
+ _ENV_LINE = re.compile(r'^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$')
26
+
27
+
28
+ def load_env_file(path: str | Path) -> None:
29
+ """把一份 `.env` 并进 `os.environ`(在 `settings.py` 的**第一行**调一次)。
30
+
31
+ 🔴 必须排在读 `os.environ` 之前:顺序反了,`.env` 里给的值读不到,
32
+ 而报出来的症状是"我配了,它还是不生效"。
33
+ 🔴 `.env` **覆盖**进程环境:它是项目的配置真源,而进程里可能留着同名的外部变量。
34
+ 🔴 用 `utf-8-sig` 读:编辑器存"UTF-8 带 BOM"时那个 BOM 会粘在第一个键名前面,
35
+ 而 `^\\s*` 不匹配它 ⇒ 第一行**静默消失**(症状是"我明明配了它")。
36
+ ⚠️ 文件不在 = 什么都不做(不抛、不报):缺配置该由**用到它的那一刻**报出来。
37
+ """
38
+ source = Path(path)
39
+ if not source.is_file():
40
+ return
41
+ values: dict[str, str] = {}
42
+ for line in source.read_text(encoding='utf-8-sig').splitlines():
43
+ match = _ENV_LINE.match(line)
44
+ if not match:
45
+ continue # 空行与 `#` 注释都走这一条
46
+ raw = match.group(2).strip()
47
+ quoted = len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in ('"', "'")
48
+ values[match.group(1)] = raw[1:-1] if quoted else raw
49
+ os.environ.update(values)
50
+
51
+
52
+ def read_env(name: str) -> str:
53
+ """读一个环境变量(空串当没写 —— `.env` 里 `KEY=` 是常态),去掉首尾空白。"""
54
+ return str(os.environ.get(name, '') or '').strip()
55
+
56
+
57
+ def block_config(env: Mapping[str, str] | None = None) -> dict[str, str]:
58
+ """给块的配置:带 `BLOCK_` 前缀的那几个,**剥掉前缀当键**。
59
+
60
+ 🔴 没配的键在 `ctx.config` 上**不存在**(别拿 `None` 当默认值使,先判 `'X' in ctx.config`);
61
+ 配成空串的**照给**(`BLOCK_X=` ⇒ `ctx.config['X'] == ''`)。
62
+ 🔴 剥完是空串的**不算键**(`BLOCK_=` 不可能是名字)。
63
+ 🔴 别的变量一个都不给块 —— 数据库连接串、SSO 密钥都不在这里。
64
+ """
65
+ source = os.environ if env is None else env
66
+ result: dict[str, str] = {}
67
+ for name, value in source.items():
68
+ if not name.startswith(BLOCK_CONFIG_PREFIX):
69
+ continue
70
+ key = name[len(BLOCK_CONFIG_PREFIX):]
71
+ if key:
72
+ result[key] = value
73
+ return result
74
+
75
+
76
+ # ──────────────────────────── "有哪几个环境变量"这份清单 ────────────────────────────
77
+
78
+ #: 登录那一组的组名。🔴 `auth/sso.py` 按它取必填清单(**不抄第二份名单**)。
79
+ LOGIN_GROUP = '登录(Authing OIDC)'
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class EnvItem:
84
+ """一个环境变量在**开发包**里被看到的样子。
85
+
86
+ 🔴 `hint` 是"能直接粘进 `.env` 的一行":缺必填时那句报错就照它说(见 `auth/sso.py`)——
87
+ 只说"缺了"的报错,用户还得自己去翻文档才知道那一行该怎么写。
88
+ 🔴 **这里永远不写值**:这份清单会铺进开发包(跟着块作者的编辑器走),而环境里有
89
+ 数据库连接串与登录密钥。块的配置面是白名单,同一个道理。
90
+ """
91
+
92
+ #: 变量名。⚠️ 前缀那一条写的是 `BLOCK_*` —— 它不是一个真名字,是一族名字的前缀。
93
+ name: str
94
+ #: 这个变量管什么(一句人话)。
95
+ brief: str
96
+ #: 能直接粘进 `.env` 的一行(没有例子就是空串)。
97
+ hint: str = ''
98
+ #: 必填吗(缺了就跑不起来的那几项)。
99
+ required: bool = False
100
+ #: 分组(开发包里按它分段;"哪几项是登录用的"也从这里取)。
101
+ group: str = '平台'
102
+
103
+
104
+ #: 平台读的**全部**环境变量 —— 唯一真源。
105
+ #:
106
+ #: 🔴 它是一份**清单**,不是"读值的入口":各处仍然各读各的(`read_env('DATABASE_URL')` 之类)。
107
+ #: 两者对不上的风险由 `tools/sdk-check-py.mjs` 兜住:它扫一遍库里所有 `read_env('<名字>')`
108
+ #: 字面量,漏在这份清单外的当场报红。
109
+ ENV_ITEMS: tuple[EnvItem, ...] = (
110
+ # ── 登录(Authing OIDC):这四项不配就没人能登录 ──
111
+ EnvItem(
112
+ name='SSO_ISSUER',
113
+ brief='Authing 的 OIDC 发证地址(OIDC discovery 就在它下面)',
114
+ hint='SSO_ISSUER=https://<你的域名>.authing.cn/oidc',
115
+ required=True,
116
+ group=LOGIN_GROUP,
117
+ ),
118
+ EnvItem(
119
+ name='SSO_CLIENT_ID',
120
+ brief='Authing 应用的 App ID',
121
+ hint='SSO_CLIENT_ID=<Authing 应用的 App ID>',
122
+ required=True,
123
+ group=LOGIN_GROUP,
124
+ ),
125
+ EnvItem(
126
+ name='SSO_CLIENT_SECRET',
127
+ brief='Authing 应用的 App Secret(**只在服务端**用,别带进前端)',
128
+ hint='SSO_CLIENT_SECRET=<Authing 应用的 App Secret>',
129
+ required=True,
130
+ group=LOGIN_GROUP,
131
+ ),
132
+ EnvItem(
133
+ name='SSO_REDIRECT_URI',
134
+ brief='登录回调地址(必须与 Authing 应用里登记的那个逐字一致)',
135
+ hint='SSO_REDIRECT_URI=http://localhost:8000/auth/callback',
136
+ required=True,
137
+ group=LOGIN_GROUP,
138
+ ),
139
+ EnvItem(
140
+ name='SSO_WEB_ORIGIN',
141
+ brief='登录成功后把人送回哪个前端地址(不写就用 auth/sso.py 里的默认值)',
142
+ hint='SSO_WEB_ORIGIN=http://localhost:5173',
143
+ group=LOGIN_GROUP,
144
+ ),
145
+ # ── 跨源与数据库 ──
146
+ EnvItem(
147
+ name='CORS_ORIGIN',
148
+ brief='允许跨源访问的前端地址(逗号分隔);它同时是 CSRF 信任名单的来源',
149
+ hint='CORS_ORIGIN=http://localhost:5173',
150
+ group='跨源与数据库',
151
+ ),
152
+ EnvItem(
153
+ name='DATABASE_URL',
154
+ brief='项目数据库连接串(postgres://… 形式);不写就用项目自己 settings 里的 DATABASES',
155
+ hint='DATABASE_URL=postgres://user:password@127.0.0.1:5432/app',
156
+ group='跨源与数据库',
157
+ ),
158
+ # ── 平台开关 ──
159
+ EnvItem(
160
+ name='BOOTSTRAP_ADMIN_WORK_IDS',
161
+ brief='管理员名单:启动时把这些人设成 is_staff,名单之外的一律清掉(逗号分隔,上限 200;没配就是没有管理员)',
162
+ hint='BOOTSTRAP_ADMIN_WORK_IDS=1001',
163
+ group='平台开关',
164
+ ),
165
+ EnvItem(
166
+ name='LABULL_SCHEMA_CANVAS',
167
+ brief='数据模型那一页的开关:on 常开 / auto 只在 DEBUG 开 / 其余(默认)不开',
168
+ hint='LABULL_SCHEMA_CANVAS=on',
169
+ group='平台开关',
170
+ ),
171
+ EnvItem(
172
+ name='LABULL_CONTRACT_PATH',
173
+ brief='模型形状那份 JSON 的落点(**兜底**用:画布的基线默认是从提交里的源码算的;默认 .labull/contract.json)',
174
+ hint='LABULL_CONTRACT_PATH=.labull/contract.json',
175
+ group='平台开关',
176
+ ),
177
+ EnvItem(
178
+ name='LABULL_BASELINE_REV',
179
+ brief="画布拿哪一版当\"上一版\":留空=HEAD~1(上一次提交改了什么)/ head=只看没提交的改动 / off=不做差异 / 也可以给一个提交号或分支",
180
+ hint='LABULL_BASELINE_REV=HEAD~1',
181
+ group='平台开关',
182
+ ),
183
+ EnvItem(
184
+ name='LABULL_BASELINE_IGNORE_APPS',
185
+ brief='画布比对时**不参与**的 app(逗号/空格分隔,例如 labull_framework)—— 这些 app 的表照样画,只是不带"上一版 vs 现在"的差异色',
186
+ hint='LABULL_BASELINE_IGNORE_APPS=labull_framework',
187
+ group='平台开关',
188
+ ),
189
+ # 🔴 下面两条是**体检**那一侧的应急开关(名字的真源在 `magic_block/sdk.py`,不在这里手抄):
190
+ # 默认都开 —— 关掉等于"字段改名这类问题回到运行期才现形",只该在排查误报时临时用一下。
191
+ EnvItem(
192
+ name=BLOCK_DEEPCHECK,
193
+ brief='块的「引用深检」总开关:off 退回只判形状(字段 / 模型改了名就不再提前报出来)',
194
+ hint=f'{BLOCK_DEEPCHECK}=off',
195
+ group='平台开关',
196
+ ),
197
+ # 🔴 依赖声明(`requires`)那一层的开关(名字的真源在 `magic_block/sdk.py`):
198
+ # 默认 `error`(1.0.1 起)—— "先警告、后强制"里的**后强制**已经落地了。
199
+ # ⚠️ 代价如实说:库里那些**还没声明**的存量块会集体判不合格(全部停用),
200
+ # 所以升级前先用 `manage.py labull_requires --write` 把扫得到的声明批量补进库
201
+ # (那条命令就是为这个默认值而存在的:它是这条路上唯一的恢复口)。
202
+ EnvItem(
203
+ name=BLOCK_REQUIRES,
204
+ brief='块「依赖声明」(requires)那一层的开关:off 整段不跑 / warn 只提示不拦(要显式写)/ error(默认)变成硬判据',
205
+ hint=f'{BLOCK_REQUIRES}=warn',
206
+ group='平台开关',
207
+ ),
208
+ # 🔴 块「类型」那一层的开关(名字的真源在 `magic_block/sdk.py`):
209
+ # 默认 `error`(与 `requires` 同一个方向)—— 声明了 `type: workflow` 却写错流程,那是事实。
210
+ # ⚠️ `off` 只关"这一种类型多出来的判据",通用那几段**一直跑**(V7 盯着这一条)。
211
+ EnvItem(
212
+ name=BLOCK_TYPE,
213
+ brief='块「类型」(清单里的 type)那一层的开关:off 只关该类型多出来的判据(通用校验照跑)/ warn 只提示不拦 / error(默认)变成硬判据',
214
+ hint=f'{BLOCK_TYPE}=warn',
215
+ group='平台开关',
216
+ ),
217
+ # 🔴 体检那条链一次开几个口子(名字的真源在 `magic_block/parallel.py` 的 `ENV_WORKERS`)。
218
+ # ⚠️ 它是**性能旋钮、不是判据**:写错一个数(0 / 64 / 一句乱码)会被夹到 1..8,不该让体检报错。
219
+ # 它在这里的意义是"使用者的清单里能看见这个开关"(`backend.lock.pyi` 就是按 ENV_ITEMS 铺的)。
220
+ EnvItem(
221
+ name='LABULL_BLOCK_WORKERS',
222
+ brief='启动体检 / 全量校验一次开几个子进程(默认 4,夹在 1..8):机器小就往下调,写 1 退回串行',
223
+ hint='LABULL_BLOCK_WORKERS=2',
224
+ group='平台开关',
225
+ ),
226
+ # ── 块的配置面 ──
227
+ EnvItem(
228
+ name=f'{BLOCK_CONFIG_PREFIX}*',
229
+ brief='块的配置面:**只有**带这个前缀的变量才进 ctx.config(前缀剥掉当键,别的变量一个都不给块)',
230
+ hint=f'{BLOCK_CONFIG_PREFIX}OCR_KEY=xxx',
231
+ group='块的配置',
232
+ ),
233
+ )
234
+
235
+
236
+ def required_items(group: str) -> tuple[EnvItem, ...]:
237
+ """某一组里必填的那几项(顺序 = `ENV_ITEMS` 里的顺序,所以报错顺序稳定)。"""
238
+ return tuple(item for item in ENV_ITEMS if item.group == group and item.required)