labull-framework 0.1.8__tar.gz → 0.1.9__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 (48) hide show
  1. {labull_framework-0.1.8/labull_framework.egg-info → labull_framework-0.1.9}/PKG-INFO +1 -1
  2. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/devkit.py +1 -1
  3. {labull_framework-0.1.8 → labull_framework-0.1.9/labull_framework.egg-info}/PKG-INFO +1 -1
  4. {labull_framework-0.1.8 → labull_framework-0.1.9}/pyproject.toml +1 -1
  5. {labull_framework-0.1.8 → labull_framework-0.1.9}/LICENSE +0 -0
  6. {labull_framework-0.1.8 → labull_framework-0.1.9}/MANIFEST.in +0 -0
  7. {labull_framework-0.1.8 → labull_framework-0.1.9}/README-pypi.md +0 -0
  8. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/__init__.py +0 -0
  9. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/admin.py +0 -0
  10. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/apps.py +0 -0
  11. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/auth/__init__.py +0 -0
  12. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/auth/backend.py +0 -0
  13. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/auth/bootstrap.py +0 -0
  14. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/auth/sso.py +0 -0
  15. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/auth/views.py +0 -0
  16. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/canvas/__init__.py +0 -0
  17. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/canvas/data.py +0 -0
  18. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/canvas/gitinfo.py +0 -0
  19. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/canvas/static/canvas.css +0 -0
  20. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/canvas/static/canvas.js +0 -0
  21. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/canvas/static/page.html +0 -0
  22. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/canvas/views.py +0 -0
  23. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/env.py +0 -0
  24. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/jsonio.py +0 -0
  25. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/__init__.py +0 -0
  26. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/blockpackage.py +0 -0
  27. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/deepcheck.py +0 -0
  28. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/dispatch.py +0 -0
  29. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/export.py +0 -0
  30. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/health.py +0 -0
  31. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/registry.py +0 -0
  32. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/sdk.py +0 -0
  33. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/service.py +0 -0
  34. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/validate.py +0 -0
  35. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/magic_block/views.py +0 -0
  36. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/management/__init__.py +0 -0
  37. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/management/commands/__init__.py +0 -0
  38. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/management/commands/labull_contract.py +0 -0
  39. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/migrations/0001_initial.py +0 -0
  40. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/migrations/__init__.py +0 -0
  41. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/models.py +0 -0
  42. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/settings.py +0 -0
  43. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework/urls.py +0 -0
  44. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework.egg-info/SOURCES.txt +0 -0
  45. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework.egg-info/dependency_links.txt +0 -0
  46. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework.egg-info/requires.txt +0 -0
  47. {labull_framework-0.1.8 → labull_framework-0.1.9}/labull_framework.egg-info/top_level.txt +0 -0
  48. {labull_framework-0.1.8 → labull_framework-0.1.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 0.1.8
3
+ Version: 0.1.9
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
@@ -428,7 +428,7 @@ EXAMPLE_FILES: tuple[tuple[str, str], ...] = (
428
428
  #: 块自检脚本的正文:真源是仓库里的 `tools/smoke.mjs`(能直接 `node` 跑、也能改),
429
429
  #: 这里逐字抄一份铺进开发目录 —— 作者在**发布前**本地就能把块过一遍。
430
430
  #: ⚠️ 与 `EXAMPLE_FILES` 一样是**生成物**:改脚本请改 `tools/smoke.mjs`,然后重跑生成器。
431
- SMOKE_SCRIPT: str = '/* 块包自检(**本地就能跑**):把"发布前一定会后悔的那几条"提前抓出来。\n *\n * 用法(在开发目录里,或者指向它):\n * node smoke.mjs # 只查"有改动的那几个块"(跟 git 比),没有就查全部\n * node smoke.mjs --all # 查全部块\n * node smoke.mjs <块文件夹|块 uuid> # 只查一个块(可以给多个)\n * node smoke.mjs --workspace <开发目录>\n * LABULL_SMOKE_PYTHON=<python> node smoke.mjs # 指一个装了 Django 的解释器 ⇒ 多跑一层\n *\n * 它查两类东西:\n * ① **纯文本层(不需要任何依赖)**:清单四个键、文件名形状、前端引用链\n * (`<script src>` / `<link href>` 指向的文件在不在包里)、"包里的文件没人引用"、\n * 以及**后端文件之间不许互相引用**这条规矩。\n * ② **平台那一层(有 Python + Django 时才跑)**:真的按平台自己的判据加载一次\n * (`labull_framework.magic_block.validate`)—— 那份判据是**同一份代码**,\n * 不是在这里抄一遍(抄一遍的下场是"本地说没事、管理台说不合格")。\n *\n * 🔴 退了就报清楚:没装 Python / 没装 Django 时,第 ② 层会明说"这一层没跑",\n * **不假装自己全绿**(自检的意义就是提前发现,不是给个安心)。\n * 🔴 输出不用 emoji:中文 Windows 控制台是 cp936,emoji 会变成乱码让整行不可读。\n */\n\nimport { readdirSync, readFileSync, statSync } from \'node:fs\'\nimport { basename, extname, join, relative, resolve, sep } from \'node:path\'\nimport { existsSync } from \'node:fs\'\nimport { spawnSync } from \'node:child_process\'\n\nconst ENTRY = \'index.open.html\'\nconst MANIFEST = \'magic-block.open.yaml\'\nconst BACKEND = \'backend_open.py\'\n/** 平台随开发目录铺下来的生成物:它们**不进块包**,引用不到也不算错。 */\nconst PLATFORM_FILES = new Set([\n \'api.open.d.ts\', \'VERSION.lock\', \'README.lock.md\', \'GUIDE.lock.md\', \'backend.lock.pyi\',\n \'pages.lock.ts\', \'labull.css\', \'CONTROLS.lock.html\', \'requirements.lock.txt\',\n])\n/** 爬目录时跳过的目录(与后端 `export.SKIP_DIRS` 同一份口径)。 */\nconst SKIP_DIRS = new Set([\'.git\', \'.venv\', \'venv\', \'__pycache__\', \'node_modules\', \'dist\', \'build\', \'.idea\', \'.vscode\', \'example\'])\n/** 认哪些扩展名算"能进包的文本"。 */\nconst TEXT_EXT = new Set([\'.html\', \'.htm\', \'.js\', \'.mjs\', \'.cjs\', \'.ts\', \'.css\', \'.json\', \'.yaml\', \'.yml\', \'.md\', \'.txt\', \'.py\', \'.svg\'])\nconst MAX_DEPTH = 3\nconst MAX_FILE_BYTES = 512 * 1024\n\nconst argv = process.argv.slice(2)\nconst flags = { all: false, workspace: \'\', python: process.env.LABULL_SMOKE_PYTHON || \'\', targets: [] }\nfor (let i = 0; i < argv.length; i += 1) {\n if (argv[i] === \'--all\') flags.all = true\n else if (argv[i] === \'--workspace\') flags.workspace = argv[++i] || \'\'\n else if (argv[i] === \'--python\') flags.python = argv[++i] || \'\'\n else flags.targets.push(argv[i])\n}\n\nconst pass = []\nconst fail = []\nconst skip = []\nconst ok = (message) => { pass.push(message); console.log(` [OK] ${message}`) }\nconst ng = (message) => { fail.push(message); console.log(` [NG] ${message}`) }\nconst note = (message) => { skip.push(message); console.log(` [--] ${message}`) }\nconst info = (message) => console.log(message)\n\nfunction readText(path) {\n const raw = readFileSync(path)\n if (raw.length > MAX_FILE_BYTES) return { text: \'\', tooBig: true }\n return { text: raw.toString(\'utf8\').replace(/^\\uFEFF/, \'\'), tooBig: false }\n}\n\n// ──────────────────────────── 找开发目录与块 ────────────────────────────\n\nfunction findWorkspace(start) {\n let dir = resolve(start)\n for (let depth = 0; depth < 6; depth += 1) {\n const workspace = join(dir, \'workspace\')\n if (existsSync(workspace) && statSync(workspace).isDirectory()) return workspace\n if (basename(dir) === \'workspace\') return dir\n const parent = resolve(dir, \'..\')\n if (parent === dir) break\n dir = parent\n }\n return \'\'\n}\n\nfunction listBlocks(workspace) {\n const out = []\n for (const name of readdirSync(workspace).sort()) {\n const full = join(workspace, name)\n if (!statSync(full).isDirectory()) continue\n // 🔴 只认**块文件夹**(里面有清单):开发目录里还有 `example/` 这种平台铺下来的东西,\n // 把它也当成一个块去查,只会平白报一堆"缺这个缺那个"。\n if (!existsSync(join(full, MANIFEST)) && !existsSync(join(full, ENTRY))) continue\n out.push(full)\n }\n return out\n}\n\n/** 一个块文件夹 → `{相对路径: 正文}`(与平台 `export` 的扫描口径一致)。 */\nfunction readBlock(dir) {\n const files = {}\n const walk = (current, segments, depth) => {\n for (const name of readdirSync(current).sort()) {\n const full = join(current, name)\n const stat = statSync(full)\n const at = [...segments, name]\n if (stat.isDirectory()) {\n if (SKIP_DIRS.has(name) || depth >= MAX_DEPTH) continue\n walk(full, at, depth + 1)\n continue\n }\n if (!TEXT_EXT.has(extname(name).toLowerCase())) continue\n if (stat.size > MAX_FILE_BYTES) { files[at.join(\'/\')] = \'\'; continue }\n files[at.join(\'/\')] = readText(full).text\n }\n }\n walk(dir, [], 0)\n return files\n}\n\n// ──────────────────────────── ① 纯文本层 ────────────────────────────\n\nfunction parseManifest(text) {\n const found = {}\n const issues = []\n const stripComment = (line) => {\n let quote = \'\'\n for (let i = 0; i < line.length; i += 1) {\n const char = line[i]\n if (quote) { if (char === quote) quote = \'\' }\n else if (char === \'"\' || char === "\'") quote = char\n else if (char === \'#\') return line.slice(0, i)\n }\n return line\n }\n text.split(/\\r?\\n/).forEach((raw, index) => {\n const line = stripComment(raw).trim()\n if (!line) return\n const at = line.indexOf(\':\')\n if (at < 0) { issues.push(`第 ${index + 1} 行不是「键: 值」:${line}`); return }\n const key = line.slice(0, at).trim()\n const value = line.slice(at + 1).trim().replace(/^([\'"])(.*)\\1$/, \'$2\')\n if (![\'id\', \'module\', \'name\', \'brief\'].includes(key)) { issues.push(`第 ${index + 1} 行有不认识的键:${key}`); return }\n if (found[key] !== undefined) { issues.push(`第 ${index + 1} 行重复写了 ${key}`); return }\n found[key] = value\n })\n for (const key of [\'id\', \'module\', \'name\', \'brief\']) {\n if (!found[key]) issues.push(`清单缺 ${key}(或者是空的)`)\n }\n if (found.id && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(found.id)) {\n issues.push(`清单里的 id 不是 uuid:${found.id}`)\n }\n if (found.name && found.name.length > 40) issues.push(`name 超过 40 个字:${found.name.length}`)\n return { found, issues }\n}\n\n/** 前端引用链:入口里 `src=` / `href=` 指向的**包内**文件在不在。回 `{missing, referenced}`。 */\nfunction frontendRefs(files) {\n const referenced = new Map() // 引用者 → 被引用的相对路径\n const missing = []\n const pick = (ref) => {\n if (/^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i.test(ref)) return undefined // 外链 / data: → 不是包内文件\n const clean = ref.split(/[?#]/)[0].replace(/^\\.\\//, \'\').replace(/^\\/+/, \'\')\n if (!clean) return undefined\n if (files[clean] !== undefined) return clean\n const base = clean.split(\'/\').pop()\n return base ? Object.keys(files).find((name) => name.split(\'/\').pop() === base) : undefined\n }\n const scan = (owner, body) => {\n for (const match of body.matchAll(/<(?:script|link)\\b[^>]*?(?:src|href)\\s*=\\s*(["\'])(.*?)\\1[^>]*>/gi)) {\n const ref = match[2]\n const hit = pick(ref)\n if (hit) referenced.set(owner, [...(referenced.get(owner) || []), hit])\n else if (!/^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i.test(ref) && basename(ref) !== \'labull.css\') {\n missing.push(`${owner} 引用的 ${ref} 不在包里`)\n }\n }\n }\n for (const [name, body] of Object.entries(files)) {\n if (name.endsWith(\'.html\')) scan(name, body)\n }\n return { missing, referenced }\n}\n\n/** 每个 `.py` 里 import 的**包内**文件("文件之间不许互相引用"这条规矩的判据)。 */\nfunction siblingImports(files) {\n const known = new Map()\n for (const name of Object.keys(files)) {\n if (!name.endsWith(\'.py\')) continue\n const stem = name.slice(0, -3)\n known.set(stem, name)\n known.set(stem.split(\'/\').pop(), name)\n }\n const out = []\n for (const [name, body] of Object.entries(files)) {\n if (!name.endsWith(\'.py\')) continue\n for (const line of body.split(/\\r?\\n/)) {\n const trimmed = line.trim()\n if (trimmed.startsWith(\'#\')) continue\n const from = /^from\\s+([A-Za-z_][\\w.]*)\\s+import\\b/.exec(trimmed)\n const plain = /^import\\s+(.+)$/.exec(trimmed)\n const names = []\n if (from) names.push(from[1])\n else if (plain) names.push(...plain[1].split(\',\').map((item) => item.trim().split(/\\s+as\\s+/)[0]))\n for (const imported of names) {\n const hit = known.get(imported) || known.get(imported.split(\'.\')[0])\n if (hit && hit !== name) out.push(`${name} import 了包内的 ${imported}(就是 ${hit})`)\n }\n }\n }\n return out\n}\n\n/**\n * 从开发目录顶层那份 `backend.lock.pyi` 里读"**你现在配了哪些键**"。\n *\n * 🔴 为什么要从清单里读,而不是自己去扫环境:块作者本地通常**没有** Django(那份清单是平台\n * 导出时算的)。清单里那一块就是"导出这一刻真的存在的 `BLOCK_` 键名",是这里唯一的真源。\n * 格式约定:说明那行是「你现在配了哪些键」,紧跟其后的一串 `# KEY` 就是键名。\n */\nfunction readConfigKeys(workspace) {\n const path = join(workspace, \'..\', \'backend.lock.pyi\')\n if (!existsSync(path)) return null\n const lines = readText(path).text.split(/\\r?\\n/)\n const out = []\n let inside = false\n for (const line of lines) {\n if (line.includes(\'你现在配了哪些键\')) { inside = true; continue }\n if (!inside) continue\n const hit = /^#\\s{3}([A-Za-z_][A-Za-z0-9_]*)\\s*$/.exec(line)\n if (hit) { out.push(hit[1]); continue }\n if (line.trim() && !line.startsWith(\'#\')) break\n if (line.startsWith(\'# (\')) break // "(一个都没有:…)"\n }\n return out\n}\n\n/** 在开发目录附近找 `.env`(`.env` / `.env.local` / `.env.dev`),返回找到的第一个的键名集合。 */\nfunction readEnvKeys(workspace) {\n const names = new Set()\n let dir = resolve(workspace)\n for (let depth = 0; depth < 4; depth += 1) {\n for (const file of [\'.env\', \'.env.local\', \'.env.dev\']) {\n const full = join(dir, file)\n if (!existsSync(full)) continue\n for (const raw of readText(full).text.split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line || line.startsWith(\'#\')) continue\n const at = line.indexOf(\'=\')\n if (at <= 0) continue\n names.add(line.slice(0, at).trim())\n }\n return { file: full, names }\n }\n const parent = resolve(dir, \'..\')\n if (parent === dir) break\n dir = parent\n }\n return null\n}\n\n/** 一份 `.py` 里用到的配置键:`ctx.config[\'KEY\']` / `ctx.config.get(\'KEY\')` / `"KEY" in ctx.config`。 */\nfunction configUsage(files) {\n const out = []\n const patterns = [\n /config\\[\\s*[\'"]([A-Za-z_][A-Za-z0-9_]*)[\'"]\\s*\\]/g,\n /config\\.get\\(\\s*[\'"]([A-Za-z_][A-Za-z0-9_]*)[\'"]/g,\n /[\'"]([A-Za-z_][A-Za-z0-9_]*)[\'"]\\s+in\\s+config/g,\n ]\n for (const [name, body] of Object.entries(files)) {\n if (!name.endsWith(\'.py\')) continue\n for (const pattern of patterns) {\n for (const match of body.matchAll(pattern)) {\n if (!out.some((item) => item.file === name && item.key === match[1])) {\n out.push({ file: name, key: match[1] })\n }\n }\n }\n }\n return out\n}\n\n/**\n * 查"块用到的配置键对不对"——\n * ① 代码里用到的键必须在清单里(清单空着就跳过,不然没配过的人会被满屏报错吓到);\n * ② 找到了 `.env` 就再对一遍:写了却没配的键是**必错项**(运行期取不到),配了却没人用的只提示。\n */\nfunction checkConfig(label, files, workspace) {\n const known = readConfigKeys(workspace)\n const env = readEnvKeys(workspace)\n const usage = configUsage(files)\n let bad = 0\n for (const { file, key } of usage) {\n if (Array.isArray(known) && known.length > 0 && !known.includes(key)) {\n bad += 1\n ng(`${label}:${file} 里用了 ctx.config[\'${key}\'],但它不在清单里 —— `\n + `导出那一刻环境里配的是:${known.join(\' / \')}(写错的键运行期只会取到 None,不报错)`)\n }\n if (env && !env.names.has(`BLOCK_${key}`)) {\n bad += 1\n ng(`${label}:${file} 里用了 ctx.config[\'${key}\'],但 ${basename(env.file)} 里没有 BLOCK_${key}(运行期取不到)`)\n }\n }\n if (!env) {\n note(`${label}:附近的 .env 没找到 ⇒ "这个键配了没有"那一半没查(代码用到的键仍与清单对过)`)\n } else {\n const used = new Set(usage.map((item) => item.key))\n const unused = [...env.names]\n .filter((name) => name.startsWith(\'BLOCK_\') && !used.has(name.slice(\'BLOCK_\'.length)))\n .map((name) => name.slice(\'BLOCK_\'.length))\n if (unused.length) note(`${label}:${basename(env.file)} 里这几个 BLOCK_ 键没有被用到:${unused.join(\' / \')}`)\n }\n if (usage.length && bad === 0) {\n ok(`${label}:代码里用到的配置键都对得上(${usage.map((item) => item.key).join(\' / \')})`)\n }\n}\n\nfunction checkBlock(dir) {\n const label = basename(dir)\n info(`\\n── ${label} ──`)\n const files = readBlock(dir)\n const names = Object.keys(files)\n\n if (!files[ENTRY]) ng(`${label}:缺入口 ${ENTRY}`)\n else ok(`${label}:有入口 ${ENTRY}`)\n\n if (files[MANIFEST] === undefined) {\n ng(`${label}:缺清单 ${MANIFEST}`)\n } else {\n const { found, issues } = parseManifest(files[MANIFEST])\n if (issues.length) ng(`${label}:清单有问题 —— ${issues.join(\';\')}`)\n else ok(`${label}:清单四个键齐(${found.module} / ${found.name})`)\n }\n\n for (const name of names) {\n if (!/^(?![A-Za-z]:)(?!\\/)[A-Za-z0-9._\\-/]+$/.test(name) || name.split(\'/\').includes(\'..\')) {\n ng(`${label}:文件名不合法 ${name}`)\n }\n }\n\n const { missing, referenced } = frontendRefs(files)\n for (const item of missing) ng(`${label}:${item}`)\n if (!missing.length && files[ENTRY]) ok(`${label}:前端的引用都指得到(入口里的 script/link 都在包里)`)\n\n // "包里的文件没人引用":只提示(入口文件与清单、后端本来就不该被引用)。\n const used = new Set([ENTRY, MANIFEST, BACKEND, ...[...referenced.values()].flat()])\n const orphans = names.filter((name) => !used.has(name) && !PLATFORM_FILES.has(name)\n && !name.endsWith(\'.py\') && name !== \'README.md\')\n if (orphans.length) note(`${label}:这几个文件没有任何引用(写进包里也没人用):${orphans.join(\' / \')}`)\n\n const cross = siblingImports(files)\n if (cross.length) {\n for (const item of cross) {\n ng(`${label}:${item} —— **块里文件之间不能互相引用**(平台把每个 .py 各自独立加载):`\n + \'要么把那段逻辑复制过去,要么放进项目里的公共模块再 import\')\n }\n } else if (names.some((name) => name.endsWith(\'.py\'))) {\n ok(`${label}:后端那些 .py 之间没有互相引用(${names.filter((n) => n.endsWith(\'.py\')).join(\' / \')})`)\n }\n\n checkConfig(label, files, workspace)\n return files\n}\n\n// ──────────────────────────── ② 平台那一层 ────────────────────────────\n\nfunction findPython() {\n if (flags.python) return flags.python\n const candidates = [process.env.LABULL_PYTHON, \'python\', \'python3\', \'py\'].filter(Boolean)\n const probe = \'import django, labull_framework; print("ok")\'\n for (const candidate of candidates) {\n const done = spawnSync(candidate, [\'-c\', probe], {\n encoding: \'utf-8\',\n env: { ...process.env, PYTHONIOENCODING: \'utf-8\' },\n })\n if (done.status === 0 && String(done.stdout).includes(\'ok\')) return candidate\n }\n return \'\'\n}\n\nfunction platformCheck(workspace, python, payload) {\n const script = [\n \'import json, os, sys\',\n \'from labull_framework.magic_block import validate\',\n \'payload = json.loads(os.environ["LABULL_SMOKE_PAYLOAD"])\',\n \'out = []\',\n \'for item in payload:\',\n \' issues = validate.check_block(item["files"])\',\n \' out.append({"name": item["name"], "issues": [list(pair) for pair in issues]})\',\n \'print("LABULL_SMOKE" + json.dumps(out, ensure_ascii=False))\',\n ].join(\'\\n\')\n const done = spawnSync(python, [\'-c\', script], {\n encoding: \'utf-8\',\n env: { ...process.env, PYTHONIOENCODING: \'utf-8\', LABULL_SMOKE_PAYLOAD: JSON.stringify(payload) },\n })\n const text = `${done.stdout || \'\'}${done.stderr || \'\'}`\n const at = text.indexOf(\'LABULL_SMOKE\')\n if (done.status !== 0 || at < 0) {\n note(`平台那一层没跑起来(退出码 ${done.status}):${text.trim().split(\'\\n\').slice(-1)[0] || \'没有输出\'}`)\n return\n }\n const results = JSON.parse(text.slice(at + \'LABULL_SMOKE\'.length))\n for (const item of results) {\n if (!item.issues.length) { ok(`${item.name}:过平台那一层判据(清单/结构/试加载/引用深检)`); continue }\n for (const [stage, message] of item.issues) ng(`${item.name}:[${stage}] ${message}`)\n }\n}\n\n// ──────────────────────────── 主流程 ────────────────────────────\n\nconst workspace = flags.workspace\n ? (findWorkspace(flags.workspace) || resolve(flags.workspace))\n : findWorkspace(process.cwd())\nif (!workspace) {\n console.log(\'找不到开发目录(里面应该有 workspace/ 那一层)。用法:node smoke.mjs [--workspace <开发目录>] [块…]\')\n process.exit(2)\n}\nlet blocks = listBlocks(workspace)\nif (flags.targets.length) {\n blocks = blocks.filter((dir) => flags.targets.some((target) => basename(dir).includes(target)))\n if (!blocks.length) { console.log(`按 ${flags.targets.join(\' / \')} 没找到块(在 ${workspace} 里)`); process.exit(2) }\n}\ninfo(`开发目录:${workspace}`)\ninfo(`要查的块:${blocks.length} 个${flags.targets.length ? `(按 ${flags.targets.join(\' / \')} 筛的)` : flags.all ? \'(全部)\' : \'\'}`)\n\nconst payload = []\nfor (const dir of blocks) {\n const files = checkBlock(dir)\n payload.push({ name: basename(dir), files })\n}\n\nconst python = findPython()\nif (!python) {\n note(\'没找到装了 Django 的 Python ⇒ **平台那一层没跑**(试加载 / 引用深检 / 路由那几条没验)。\'\n + \'设 LABULL_SMOKE_PYTHON=<项目解释器> 或 --python <python> 再跑一次。\')\n} else {\n info(`\\n── 平台判据(用 ${python})──`)\n platformCheck(workspace, python, payload)\n}\n\ninfo(`\\n合计:过 ${pass.length} 条、不过 ${fail.length} 条${skip.length ? `、跳过 ${skip.length} 条` : \'\'}`)\nif (fail.length) {\n info(\'不过的:\')\n for (const item of fail) info(` - ${item}`)\n process.exit(1)\n}\ninfo(\'都过了。\')\n'
431
+ SMOKE_SCRIPT: str = '/* 块包自检(**本地就能跑**):把"发布前一定会后悔的那几条"提前抓出来。\n *\n * 用法(在开发目录里,或者指向它):\n * node smoke.mjs # 只查"有改动的那几个块"(跟 git 比),没有就查全部\n * node smoke.mjs --all # 查全部块\n * node smoke.mjs <块文件夹|块 uuid> # 只查一个块(可以给多个)\n * node smoke.mjs --workspace <开发目录>\n * LABULL_SMOKE_PYTHON=<python> node smoke.mjs # 指一个装了 Django 的解释器 ⇒ 多跑一层\n *\n * 它查两类东西:\n * ① **纯文本层(不需要任何依赖)**:清单四个键、文件名形状、前端引用链\n * (`<script src>` / `<link href>` 指向的文件在不在包里)、"包里的文件没人引用"、\n * 以及**后端文件之间不许互相引用**这条规矩。\n * ② **平台那一层(有 Python + Django 时才跑)**:真的按平台自己的判据加载一次\n * (`labull_framework.magic_block.validate`)—— 那份判据是**同一份代码**,\n * 不是在这里抄一遍(抄一遍的下场是"本地说没事、管理台说不合格")。\n *\n * 🔴 退了就报清楚:没装 Python / 没装 Django 时,第 ② 层会明说"这一层没跑",\n * **不假装自己全绿**(自检的意义就是提前发现,不是给个安心)。\n * 🔴 输出不用 emoji:中文 Windows 控制台是 cp936,emoji 会变成乱码让整行不可读。\n */\n\nimport { readdirSync, readFileSync, statSync } from \'node:fs\'\nimport { basename, extname, join, relative, resolve, sep } from \'node:path\'\nimport { existsSync } from \'node:fs\'\nimport { spawnSync } from \'node:child_process\'\n\nconst ENTRY = \'index.open.html\'\nconst MANIFEST = \'magic-block.open.yaml\'\nconst BACKEND = \'backend_open.py\'\n/** 平台随开发目录铺下来的生成物:它们**不进块包**,引用不到也不算错。 */\nconst PLATFORM_FILES = new Set([\n \'api.open.d.ts\', \'VERSION.lock\', \'README.lock.md\', \'GUIDE.lock.md\', \'backend.lock.pyi\',\n \'pages.lock.ts\', \'labull.css\', \'CONTROLS.lock.html\', \'requirements.lock.txt\',\n])\n/** 爬目录时跳过的目录(与后端 `export.SKIP_DIRS` 同一份口径)。 */\nconst SKIP_DIRS = new Set([\'.git\', \'.venv\', \'venv\', \'__pycache__\', \'node_modules\', \'dist\', \'build\', \'.idea\', \'.vscode\', \'example\'])\n/** 认哪些扩展名算"能进包的文本"。 */\nconst TEXT_EXT = new Set([\'.html\', \'.htm\', \'.js\', \'.mjs\', \'.cjs\', \'.ts\', \'.css\', \'.json\', \'.yaml\', \'.yml\', \'.md\', \'.txt\', \'.py\', \'.svg\'])\nconst MAX_DEPTH = 3\nconst MAX_FILE_BYTES = 512 * 1024\n\nconst argv = process.argv.slice(2)\nconst flags = { all: false, workspace: \'\', python: process.env.LABULL_SMOKE_PYTHON || \'\', targets: [] }\nfor (let i = 0; i < argv.length; i += 1) {\n if (argv[i] === \'--all\') flags.all = true\n else if (argv[i] === \'--workspace\') flags.workspace = argv[++i] || \'\'\n else if (argv[i] === \'--python\') flags.python = argv[++i] || \'\'\n else flags.targets.push(argv[i])\n}\n\nconst pass = []\nconst fail = []\nconst skip = []\nconst ok = (message) => { pass.push(message); console.log(` [OK] ${message}`) }\nconst ng = (message) => { fail.push(message); console.log(` [NG] ${message}`) }\nconst note = (message) => { skip.push(message); console.log(` [--] ${message}`) }\nconst info = (message) => console.log(message)\n\nfunction readText(path) {\n const raw = readFileSync(path)\n if (raw.length > MAX_FILE_BYTES) return { text: \'\', tooBig: true }\n return { text: raw.toString(\'utf8\').replace(/^\\uFEFF/, \'\'), tooBig: false }\n}\n\n// ──────────────────────────── 找开发目录与块 ────────────────────────────\n\nfunction findWorkspace(start) {\n let dir = resolve(start)\n for (let depth = 0; depth < 6; depth += 1) {\n const workspace = join(dir, \'workspace\')\n if (existsSync(workspace) && statSync(workspace).isDirectory()) return workspace\n if (basename(dir) === \'workspace\') return dir\n const parent = resolve(dir, \'..\')\n if (parent === dir) break\n dir = parent\n }\n return \'\'\n}\n\nfunction listBlocks(workspace) {\n const out = []\n for (const name of readdirSync(workspace).sort()) {\n const full = join(workspace, name)\n if (!statSync(full).isDirectory()) continue\n // 🔴 只认**块文件夹**(里面有清单):开发目录里还有 `example/` 这种平台铺下来的东西,\n // 把它也当成一个块去查,只会平白报一堆"缺这个缺那个"。\n if (!existsSync(join(full, MANIFEST)) && !existsSync(join(full, ENTRY))) continue\n out.push(full)\n }\n return out\n}\n\n/** 一个块文件夹 → `{相对路径: 正文}`(与平台 `export` 的扫描口径一致)。 */\nfunction readBlock(dir) {\n const files = {}\n const walk = (current, segments, depth) => {\n for (const name of readdirSync(current).sort()) {\n const full = join(current, name)\n const stat = statSync(full)\n const at = [...segments, name]\n if (stat.isDirectory()) {\n if (SKIP_DIRS.has(name) || depth >= MAX_DEPTH) continue\n walk(full, at, depth + 1)\n continue\n }\n if (!TEXT_EXT.has(extname(name).toLowerCase())) continue\n if (stat.size > MAX_FILE_BYTES) { files[at.join(\'/\')] = \'\'; continue }\n files[at.join(\'/\')] = readText(full).text\n }\n }\n walk(dir, [], 0)\n return files\n}\n\n// ──────────────────────────── ① 纯文本层 ────────────────────────────\n\nfunction parseManifest(text) {\n const found = {}\n const issues = []\n const stripComment = (line) => {\n let quote = \'\'\n for (let i = 0; i < line.length; i += 1) {\n const char = line[i]\n if (quote) { if (char === quote) quote = \'\' }\n else if (char === \'"\' || char === "\'") quote = char\n else if (char === \'#\') return line.slice(0, i)\n }\n return line\n }\n text.split(/\\r?\\n/).forEach((raw, index) => {\n const line = stripComment(raw).trim()\n if (!line) return\n const at = line.indexOf(\':\')\n if (at < 0) { issues.push(`第 ${index + 1} 行不是「键: 值」:${line}`); return }\n const key = line.slice(0, at).trim()\n const value = line.slice(at + 1).trim().replace(/^([\'"])(.*)\\1$/, \'$2\')\n if (![\'id\', \'module\', \'name\', \'brief\'].includes(key)) { issues.push(`第 ${index + 1} 行有不认识的键:${key}`); return }\n if (found[key] !== undefined) { issues.push(`第 ${index + 1} 行重复写了 ${key}`); return }\n found[key] = value\n })\n for (const key of [\'id\', \'module\', \'name\', \'brief\']) {\n if (!found[key]) issues.push(`清单缺 ${key}(或者是空的)`)\n }\n if (found.id && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(found.id)) {\n issues.push(`清单里的 id 不是 uuid:${found.id}`)\n }\n if (found.name && found.name.length > 40) issues.push(`name 超过 40 个字:${found.name.length}`)\n return { found, issues }\n}\n\n/** 前端引用链:入口里 `src=` / `href=` 指向的**包内**文件在不在。回 `{missing, referenced}`。 */\nfunction frontendRefs(files) {\n const referenced = new Map() // 引用者 → 被引用的相对路径\n const missing = []\n const pick = (ref) => {\n if (/^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i.test(ref)) return undefined // 外链 / data: → 不是包内文件\n const clean = ref.split(/[?#]/)[0].replace(/^\\.\\//, \'\').replace(/^\\/+/, \'\')\n if (!clean) return undefined\n if (files[clean] !== undefined) return clean\n const base = clean.split(\'/\').pop()\n return base ? Object.keys(files).find((name) => name.split(\'/\').pop() === base) : undefined\n }\n const scan = (owner, body) => {\n for (const match of body.matchAll(/<(?:script|link)\\b[^>]*?(?:src|href)\\s*=\\s*(["\'])(.*?)\\1[^>]*>/gi)) {\n const ref = match[2]\n const hit = pick(ref)\n if (hit) referenced.set(owner, [...(referenced.get(owner) || []), hit])\n else if (!/^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i.test(ref) && basename(ref) !== \'labull.css\') {\n missing.push(`${owner} 引用的 ${ref} 不在包里`)\n }\n }\n }\n for (const [name, body] of Object.entries(files)) {\n if (name.endsWith(\'.html\')) scan(name, body)\n }\n return { missing, referenced }\n}\n\n/** 每个 `.py` 里 import 的**包内**文件("文件之间不许互相引用"这条规矩的判据)。 */\nfunction siblingImports(files) {\n const known = new Map()\n for (const name of Object.keys(files)) {\n if (!name.endsWith(\'.py\')) continue\n const stem = name.slice(0, -3)\n known.set(stem, name)\n known.set(stem.split(\'/\').pop(), name)\n }\n const out = []\n for (const [name, body] of Object.entries(files)) {\n if (!name.endsWith(\'.py\')) continue\n for (const line of body.split(/\\r?\\n/)) {\n const trimmed = line.trim()\n if (trimmed.startsWith(\'#\')) continue\n const from = /^from\\s+([A-Za-z_][\\w.]*)\\s+import\\b/.exec(trimmed)\n const plain = /^import\\s+(.+)$/.exec(trimmed)\n const names = []\n if (from) names.push(from[1])\n else if (plain) names.push(...plain[1].split(\',\').map((item) => item.trim().split(/\\s+as\\s+/)[0]))\n for (const imported of names) {\n const hit = known.get(imported) || known.get(imported.split(\'.\')[0])\n if (hit && hit !== name) out.push(`${name} import 了包内的 ${imported}(就是 ${hit})`)\n }\n }\n }\n return out\n}\n\n/**\n * 从开发目录顶层那份 `backend.lock.pyi` 里读"**你现在配了哪些键**"。\n *\n * 🔴 为什么要从清单里读,而不是自己去扫环境:块作者本地通常**没有** Django(那份清单是平台\n * 导出时算的)。清单里那一块就是"导出这一刻真的存在的 `BLOCK_` 键名",是这里唯一的真源。\n * 格式约定:说明那行是「你现在配了哪些键」,紧跟其后的一串 `# KEY` 就是键名。\n */\nfunction readConfigKeys(workspace) {\n const path = join(workspace, \'..\', \'backend.lock.pyi\')\n if (!existsSync(path)) return null\n const lines = readText(path).text.split(/\\r?\\n/)\n const out = []\n let inside = false\n for (const line of lines) {\n if (line.includes(\'你现在配了哪些键\')) { inside = true; continue }\n if (!inside) continue\n const hit = /^#\\s{3}([A-Za-z_][A-Za-z0-9_]*)\\s*$/.exec(line)\n if (hit) { out.push(hit[1]); continue }\n if (line.trim() && !line.startsWith(\'#\')) break\n if (line.startsWith(\'# (\')) break // "(一个都没有:…)"\n }\n return out\n}\n\n/** 在开发目录附近找 `.env`(`.env` / `.env.local` / `.env.dev`),返回找到的第一个的键名集合。 */\nfunction readEnvKeys(workspace) {\n const names = new Set()\n let dir = resolve(workspace)\n for (let depth = 0; depth < 4; depth += 1) {\n for (const file of [\'.env\', \'.env.local\', \'.env.dev\']) {\n const full = join(dir, file)\n if (!existsSync(full)) continue\n for (const raw of readText(full).text.split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line || line.startsWith(\'#\')) continue\n const at = line.indexOf(\'=\')\n if (at <= 0) continue\n names.add(line.slice(0, at).trim())\n }\n return { file: full, names }\n }\n const parent = resolve(dir, \'..\')\n if (parent === dir) break\n dir = parent\n }\n return null\n}\n\n/** 一份 `.py` 里用到的配置键:`ctx.config[\'KEY\']` / `ctx.config.get(\'KEY\')` / `"KEY" in ctx.config`。 */\nfunction configUsage(files) {\n const out = []\n const patterns = [\n /config\\[\\s*[\'"]([A-Za-z_][A-Za-z0-9_]*)[\'"]\\s*\\]/g,\n /config\\.get\\(\\s*[\'"]([A-Za-z_][A-Za-z0-9_]*)[\'"]/g,\n /[\'"]([A-Za-z_][A-Za-z0-9_]*)[\'"]\\s+in\\s+config/g,\n ]\n for (const [name, body] of Object.entries(files)) {\n if (!name.endsWith(\'.py\')) continue\n for (const pattern of patterns) {\n for (const match of body.matchAll(pattern)) {\n if (!out.some((item) => item.file === name && item.key === match[1])) {\n out.push({ file: name, key: match[1] })\n }\n }\n }\n }\n return out\n}\n\n/**\n * 查"块用到的配置键对不对"——\n * ① 代码里用到的键必须在清单里(清单空着就跳过,不然没配过的人会被满屏报错吓到);\n * ② 找到了 `.env` 就再对一遍:写了却没配的键是**必错项**(运行期取不到),配了却没人用的只提示。\n */\nfunction checkConfig(label, files, workspace) {\n const known = readConfigKeys(workspace)\n const env = readEnvKeys(workspace)\n const usage = configUsage(files)\n let bad = 0\n for (const { file, key } of usage) {\n if (Array.isArray(known) && known.length > 0 && !known.includes(key)) {\n bad += 1\n ng(`${label}:${file} 里用了 ctx.config[\'${key}\'],但它不在清单里 —— `\n + `导出那一刻环境里配的是:${known.join(\' / \')}(写错的键运行期只会取到 None,不报错)`)\n }\n if (env && !env.names.has(`BLOCK_${key}`)) {\n bad += 1\n ng(`${label}:${file} 里用了 ctx.config[\'${key}\'],但 ${basename(env.file)} 里没有 BLOCK_${key}(运行期取不到)`)\n }\n }\n if (!env) {\n note(`${label}:附近的 .env 没找到 ⇒ "这个键配了没有"那一半没查(代码用到的键仍与清单对过)`)\n } else {\n const used = new Set(usage.map((item) => item.key))\n const unused = [...env.names]\n .filter((name) => name.startsWith(\'BLOCK_\') && !used.has(name.slice(\'BLOCK_\'.length)))\n .map((name) => name.slice(\'BLOCK_\'.length))\n if (unused.length) note(`${label}:${basename(env.file)} 里这几个 BLOCK_ 键没有被用到:${unused.join(\' / \')}`)\n }\n if (usage.length && bad === 0) {\n ok(`${label}:代码里用到的配置键都对得上(${usage.map((item) => item.key).join(\' / \')})`)\n }\n}\n\n/**\n * 块里**越界 import**的那些(判据来自 `AGENTS.lock.md`,不是这里另发明一套):\n * · Django 自带的模型(`auth_permission` / `Group` / `ContentType` …)—— 清单里根本没有它们,\n * 而它们**不是业务权限目录**(这是真出过的一类错);\n * · 平台自己的库直接 import(该走 `ctx`)。\n * 只认"从这几处 import **模型**"的写法;`django.contrib.auth.models.AbstractUser` 这类\n * 造模型用的基类也会被点到 —— 那种要写在**项目**里,不该写在块里。\n */\nconst FORBIDDEN_IMPORTS = [\n { pattern: /from\\s+django\\.contrib\\.(auth|admin|contenttypes|sessions)[\\w.]*\\s+import\\s+(.+)/, what: \'Django 自带的模型\' },\n { pattern: /import\\s+django\\.contrib\\.(auth|admin|contenttypes|sessions)\\b/, what: \'Django 自带的模型\' },\n { pattern: /from\\s+labull_framework\\.models\\s+import\\s+/, what: \'平台自己的模型(要块/用户走 ctx)\' },\n]\n\nfunction forbiddenImports(files) {\n const out = []\n for (const [name, body] of Object.entries(files)) {\n if (!name.endsWith(\'.py\')) continue\n body.split(/\\r?\\n/).forEach((raw, index) => {\n const line = raw.trim()\n if (!line || line.startsWith(\'#\')) return\n for (const { pattern, what } of FORBIDDEN_IMPORTS) {\n if (pattern.test(line)) out.push({ file: name, line: index + 1, text: line, what })\n }\n })\n }\n return out\n}\n\nfunction checkBlock(dir) {\n const label = basename(dir)\n info(`\\n── ${label} ──`)\n const files = readBlock(dir)\n const names = Object.keys(files)\n\n if (!files[ENTRY]) ng(`${label}:缺入口 ${ENTRY}`)\n else ok(`${label}:有入口 ${ENTRY}`)\n\n if (files[MANIFEST] === undefined) {\n ng(`${label}:缺清单 ${MANIFEST}`)\n } else {\n const { found, issues } = parseManifest(files[MANIFEST])\n if (issues.length) ng(`${label}:清单有问题 —— ${issues.join(\';\')}`)\n else ok(`${label}:清单四个键齐(${found.module} / ${found.name})`)\n }\n\n for (const name of names) {\n if (!/^(?![A-Za-z]:)(?!\\/)[A-Za-z0-9._\\-/]+$/.test(name) || name.split(\'/\').includes(\'..\')) {\n ng(`${label}:文件名不合法 ${name}`)\n }\n }\n\n const { missing, referenced } = frontendRefs(files)\n for (const item of missing) ng(`${label}:${item}`)\n if (!missing.length && files[ENTRY]) ok(`${label}:前端的引用都指得到(入口里的 script/link 都在包里)`)\n\n // "包里的文件没人引用":只提示(入口文件与清单、后端本来就不该被引用)。\n const used = new Set([ENTRY, MANIFEST, BACKEND, ...[...referenced.values()].flat()])\n const orphans = names.filter((name) => !used.has(name) && !PLATFORM_FILES.has(name)\n && !name.endsWith(\'.py\') && name !== \'README.md\')\n if (orphans.length) note(`${label}:这几个文件没有任何引用(写进包里也没人用):${orphans.join(\' / \')}`)\n\n const cross = siblingImports(files)\n if (cross.length) {\n for (const item of cross) {\n ng(`${label}:${item} —— **块里文件之间不能互相引用**(平台把每个 .py 各自独立加载):`\n + \'要么把那段逻辑复制过去,要么放进项目里的公共模块再 import\')\n }\n } else if (names.some((name) => name.endsWith(\'.py\'))) {\n ok(`${label}:后端那些 .py 之间没有互相引用(${names.filter((n) => n.endsWith(\'.py\')).join(\' / \')})`)\n }\n\n checkConfig(label, files, workspace)\n\n const forbidden = forbiddenImports(files)\n for (const item of forbidden) {\n ng(`${label}:${item.file} 第 ${item.line} 行 import 了${item.what} —— `\n + `${item.text.trim()}(见同目录 AGENTS.lock.md 第 1 条:那不是业务口径)`)\n }\n if (!forbidden.length && names.some((name) => name.endsWith(\'.py\'))) {\n ok(`${label}:没有 import Django 自带的模型 / 平台内部模型(只用清单里的东西)`)\n }\n return files\n}\n\n// ──────────────────────────── ② 平台那一层 ────────────────────────────\n\nfunction findPython() {\n if (flags.python) return flags.python\n const candidates = [process.env.LABULL_PYTHON, \'python\', \'python3\', \'py\'].filter(Boolean)\n const probe = \'import django, labull_framework; print("ok")\'\n for (const candidate of candidates) {\n const done = spawnSync(candidate, [\'-c\', probe], {\n encoding: \'utf-8\',\n env: { ...process.env, PYTHONIOENCODING: \'utf-8\' },\n })\n if (done.status === 0 && String(done.stdout).includes(\'ok\')) return candidate\n }\n return \'\'\n}\n\nfunction platformCheck(workspace, python, payload) {\n const script = [\n \'import json, os, sys\',\n \'from labull_framework.magic_block import validate\',\n \'payload = json.loads(os.environ["LABULL_SMOKE_PAYLOAD"])\',\n \'out = []\',\n \'for item in payload:\',\n \' issues = validate.check_block(item["files"])\',\n \' out.append({"name": item["name"], "issues": [list(pair) for pair in issues]})\',\n \'print("LABULL_SMOKE" + json.dumps(out, ensure_ascii=False))\',\n ].join(\'\\n\')\n const done = spawnSync(python, [\'-c\', script], {\n encoding: \'utf-8\',\n env: { ...process.env, PYTHONIOENCODING: \'utf-8\', LABULL_SMOKE_PAYLOAD: JSON.stringify(payload) },\n })\n const text = `${done.stdout || \'\'}${done.stderr || \'\'}`\n const at = text.indexOf(\'LABULL_SMOKE\')\n if (done.status !== 0 || at < 0) {\n note(`平台那一层没跑起来(退出码 ${done.status}):${text.trim().split(\'\\n\').slice(-1)[0] || \'没有输出\'}`)\n return\n }\n const results = JSON.parse(text.slice(at + \'LABULL_SMOKE\'.length))\n for (const item of results) {\n if (!item.issues.length) { ok(`${item.name}:过平台那一层判据(清单/结构/试加载/引用深检)`); continue }\n for (const [stage, message] of item.issues) ng(`${item.name}:[${stage}] ${message}`)\n }\n}\n\n// ──────────────────────────── 主流程 ────────────────────────────\n\nconst workspace = flags.workspace\n ? (findWorkspace(flags.workspace) || resolve(flags.workspace))\n : findWorkspace(process.cwd())\nif (!workspace) {\n console.log(\'找不到开发目录(里面应该有 workspace/ 那一层)。用法:node smoke.mjs [--workspace <开发目录>] [块…]\')\n process.exit(2)\n}\nlet blocks = listBlocks(workspace)\nif (flags.targets.length) {\n blocks = blocks.filter((dir) => flags.targets.some((target) => basename(dir).includes(target)))\n if (!blocks.length) { console.log(`按 ${flags.targets.join(\' / \')} 没找到块(在 ${workspace} 里)`); process.exit(2) }\n}\ninfo(`开发目录:${workspace}`)\ninfo(`要查的块:${blocks.length} 个${flags.targets.length ? `(按 ${flags.targets.join(\' / \')} 筛的)` : flags.all ? \'(全部)\' : \'\'}`)\n\nconst payload = []\nfor (const dir of blocks) {\n const files = checkBlock(dir)\n payload.push({ name: basename(dir), files })\n}\n\nconst python = findPython()\nif (!python) {\n note(\'没找到装了 Django 的 Python ⇒ **平台那一层没跑**(试加载 / 引用深检 / 路由那几条没验)。\'\n + \'设 LABULL_SMOKE_PYTHON=<项目解释器> 或 --python <python> 再跑一次。\')\n} else {\n info(`\\n── 平台判据(用 ${python})──`)\n platformCheck(workspace, python, payload)\n}\n\ninfo(`\\n合计:过 ${pass.length} 条、不过 ${fail.length} 条${skip.length ? `、跳过 ${skip.length} 条` : \'\'}`)\nif (fail.length) {\n info(\'不过的:\')\n for (const item of fail) info(` - ${item}`)\n process.exit(1)\n}\ninfo(\'都过了。\')\n'
432
432
 
433
433
 
434
434
  def example_files(parts: dict[str, str] | None = None) -> dict[str, str]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 0.1.8
3
+ Version: 0.1.9
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 = "0.1.8"
7
+ version = "0.1.9"
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"