From c08ab6a27843ee30235e36e0734bc1c40a91a731 Mon Sep 17 00:00:00 2001 From: pbls-bot Date: Tue, 15 Sep 2026 17:39:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(pbl=5Fappcodes):=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=20-=20pbls=20=E9=A1=B9=E7=9B=AE=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E4=BB=A3=E7=A0=81=E5=85=A5=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 12 ++++ init/data.json | 53 ++++++++++++++++ pbl_appcodes/__init__.py | 19 ++++++ pbl_appcodes/api.py | 70 +++++++++++++++++++++ pbl_appcodes/init.py | 21 +++++++ pyproject.toml | 13 ++++ scripts/load_path.py | 41 +++++++++++++ skill/SKILL.md | 20 ++++++ wwwroot/api/pbl_appcodes_inject.dspy | 4 ++ wwwroot/api/pbl_appcodes_list.dspy | 4 ++ wwwroot/index.ui | 91 ++++++++++++++++++++++++++++ 11 files changed, 348 insertions(+) create mode 100644 .gitignore create mode 100644 init/data.json create mode 100644 pbl_appcodes/__init__.py create mode 100644 pbl_appcodes/api.py create mode 100644 pbl_appcodes/init.py create mode 100644 pyproject.toml create mode 100644 scripts/load_path.py create mode 100644 skill/SKILL.md create mode 100644 wwwroot/api/pbl_appcodes_inject.dspy create mode 100644 wwwroot/api/pbl_appcodes_list.dspy create mode 100644 wwwroot/index.ui diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6579d96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +.coverage +.DS_Store +*.log +node_modules/ +dist/ +build/ +.venv/ +venv/ diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..9c9cf12 --- /dev/null +++ b/init/data.json @@ -0,0 +1,53 @@ +{ + "appcodes": { + "pbl_quality_state": [ + "draft", + "needs_work", + "pbl_ready", + "playtest_ready", + "publish_ready" + ], + "pbl_approval_status": [ + "none", + "pending", + "approved", + "rejected", + "withdrawn" + ], + "pbl_visibility": [ + "private", + "class", + "school", + "public_readonly" + ], + "pbl_evidence_type": [ + "artifact", + "process_event", + "reflection", + "feedback" + ], + "pbl_agent_type": [ + "designer", + "critic" + ], + "pbl_tool_state": [ + "enabled", + "disabled_contract_only" + ], + "pbl_artifact_type": [ + "document", + "presentation", + "model", + "code", + "media" + ], + "pbl_event_type": [ + "mission.completed", + "task.completed", + "pbl.unlockMission", + "entity.state.changed", + "session.member.joined", + "artifact.submitted" + ] + } +} diff --git a/pbl_appcodes/__init__.py b/pbl_appcodes/__init__.py new file mode 100644 index 0000000..82aa16b --- /dev/null +++ b/pbl_appcodes/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pbl_appcodes —— PBL 枚举编码注入(6+2 组,幂等,零表) + +注册三处同步之 ②:必须导出 init.py 里的全部契约函数,漏一行 .dspy 调用即 NameError。 +""" +from pbl_appcodes.init import load_pbl_appcodes +from pbl_appcodes.api import ( + pbl_appcodes_inject, + pbl_appcodes_list, + +) + +__all__ = [ + 'load_pbl_appcodes', + 'pbl_appcodes_inject', + 'pbl_appcodes_list', + +] diff --git a/pbl_appcodes/api.py b/pbl_appcodes/api.py new file mode 100644 index 0000000..132eb5f --- /dev/null +++ b/pbl_appcodes/api.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pbl_appcodes.api —— 枚举 6+2 组幂等注入(DDL 零 ENUM、零 INSERT,编码集中于此)。 + +事实源:modules/pbl_appcodes/init/data.json(由 apps/pbls/scripts/gen_artifacts.py 同源生成)。 +幂等:按 (tenant_id, appcode, codename) 已存在即跳过;重复执行不产生重复行。 +G6 范围纪律:本模块只写 appcodes 表,不写 pbl_kdb_item / research 任何表。 +""" +import json +import os + +from pbl_common.api import PblError, dbname, now_str, sql_exec, sql_rows, tenant_id + +SEED_RELPATH = os.path.join('init', 'data.json') + + +def _seed_path(): + here = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # modules/pbl_appcodes + return os.path.join(here, SEED_RELPATH) + + +def load_seed(): + p = _seed_path() + if not os.path.exists(p): + raise PblError('PBL_APPCODES_SEED_MISSING', '种子文件缺失:%s' % p) + with open(p, encoding='utf-8') as f: + return json.load(f)['appcodes'] + + +async def pbl_appcodes_inject(**kw): + """幂等注入 6+2 组枚举。返回 {groups, inserted, skipped}。""" + groups = load_seed() + only = kw.get('appcode') + if only: + groups = {only: groups.get(only) or []} + if not groups[only]: + raise PblError('PBL_APPCODE_UNKNOWN', '未知枚举组 %s' % only) + tid = await tenant_id(required=False) + inserted = skipped = 0 + for code, names in groups.items(): + for i, name in enumerate(names): + exist = await sql_rows( + 'SELECT `id` FROM `appcodes` WHERE `appcode` = ${a}$ AND `codename` = ${c}$' + ' AND `tenant_id` = ${t}$ LIMIT 1', + {'a': code, 'c': name, 't': tid}, 'pbl') + if exist: + skipped += 1 + continue + await sql_exec( + 'INSERT INTO `appcodes` (`tenant_id`,`appcode`,`codename`,`codename_cn`,' + '`order_no`,`is_enabled`,`created_at`) ' + 'VALUES (${t}$,${a}$,${c}$,${n}$,${o}$,1,${ts}$)', + {'t': tid, 'a': code, 'c': name, 'n': name, 'o': (i + 1) * 10, + 'ts': now_str()}, 'pbl') + inserted += 1 + return {'ok': True, 'groups': len(groups), 'inserted': inserted, 'skipped': skipped, + 'keys': sorted(groups)} + + +async def pbl_appcodes_list(**kw): + """读取枚举组(供前端下拉/校验白名单,只读)。""" + tid = await tenant_id(required=False) + sql = ('SELECT `appcode`,`codename`,`order_no` FROM `appcodes` WHERE `tenant_id` = ${t}$' + + (' AND `appcode` = ${a}$' if kw.get('appcode') else '') + + ' ORDER BY `appcode`,`order_no`') + rows = await sql_rows(sql, {'t': tid, 'a': kw.get('appcode')}, 'pbl') + out = {} + for r in rows: + out.setdefault(r['appcode'], []).append(r['codename']) + return {'ok': True, 'groups': out, 'counts': {k: len(v) for k, v in out.items()}} diff --git a/pbl_appcodes/init.py b/pbl_appcodes/init.py new file mode 100644 index 0000000..47d7e8b --- /dev/null +++ b/pbl_appcodes/init.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""`load_pbl_appcodes()` —— pbl_appcodes 模块唯一挂载入口。 + +注册三处同步之 ③:env.<契约名> = <契约名>(① 定义在 api.py,② 导出在 __init__.py)。 +""" +from ahserver.serverenv import ServerEnv + +from pbl_appcodes.api import ( + pbl_appcodes_inject, + pbl_appcodes_list, + +) + + +def load_pbl_appcodes(): + env = ServerEnv() + env.pbl_appcodes_inject = pbl_appcodes_inject + env.pbl_appcodes_list = pbl_appcodes_list + + return 'pbl_appcodes' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ffbfe2c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "pbl_appcodes" +version = "0.1.0" +description = "PBL 枚举编码注入(6+2 组,幂等,零表)" +requires-python = ">=3.9" +dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"] + +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["pbl_appcodes"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..d2ca493 --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pbl_appcodes RBAC 路径注册(硬门禁 6.6 / QC #11)。 + +约定: +- 路径 = 模块自动路由 `/pbl_appcodes/api/<契约>.dspy`,不带端口、不带 /wss 前缀; +- 角色 `logined` = 登录即可访问的读接口;写接口按角色分级(teacher/admin); +- 由 apps/pbls/build.sh 第 8 步调用 `register()`;rbac CLI 不在位时打印清单(不静默跳过)。 +""" +import os +import subprocess +import sys + +MODULE = 'pbl_appcodes' + +# (path, role) +PATHS = [ + ('/pbl_appcodes/api/pbl_appcodes_inject.dspy', 'logined'), + ('/pbl_appcodes/api/pbl_appcodes_list.dspy', 'logined'), + +] + + +def register(): + tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py') + done, missing = 0, [] + for path, role in PATHS: + if subprocess.call([sys.executable if os.environ.get('PY') else 'python3', + tool, role, path], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0: + done += 1 + else: + missing.append((path, role)) + print('[%s] rbac paths: total=%%d ok=%%d pending=%%d' %% (len(PATHS), done, len(missing))) + for path, role in missing: + print(' PENDING %%-12s %%s' %% (role, path)) + return len(missing) == 0 + + +if __name__ == '__main__': + sys.exit(0 if register() else 1) diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..598d06d --- /dev/null +++ b/skill/SKILL.md @@ -0,0 +1,20 @@ +# pbl_appcodes 模块技能(自动生成骨架 + 人工补充) + +## 定位 +PBL 枚举编码注入(6+2 组,幂等,零表) + +## 挂载 +`from pbl_appcodes.init import load_pbl_appcodes` → `load_pbl_appcodes()`(应用 app/pbls.py init() 中按序调用) + +## 数据表(0 张) +- 无(零表模块) + +## 契约接口(2 个,路径 `/pbl_appcodes/api/.dspy`) +- `pbl_appcodes_inject` +- `pbl_appcodes_list` + +## 陷阱 +- 库名一律 `ServerEnv().get_module_dbname('pbl_appcodes')`,禁止硬编码 DBNAME。 +- sqlor 只有 `C/U/D/R/I/sqlExe`;查询走 pbl_common.api 的 q_all/q_one(已适配)。 +- 所有读写强制带 `tenant_id`(pbl_common.api.tenant_id()),缺失即 fail-closed 报错。 +- 新增契约需同步三处:api.py 定义 + __init__.py 导出 + init.py env 注册 + scripts/load_path.py 路径。 diff --git a/wwwroot/api/pbl_appcodes_inject.dspy b/wwwroot/api/pbl_appcodes_inject.dspy new file mode 100644 index 0000000..e60be37 --- /dev/null +++ b/wwwroot/api/pbl_appcodes_inject.dspy @@ -0,0 +1,4 @@ +# pbl_appcodes/api/pbl_appcodes_inject.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py) +debug('pbl_appcodes/api/pbl_appcodes_inject.dspy: START params_kw={dict(params_kw)}') +data = await pbl_appcodes_inject(**params_kw) +return data diff --git a/wwwroot/api/pbl_appcodes_list.dspy b/wwwroot/api/pbl_appcodes_list.dspy new file mode 100644 index 0000000..48f5f54 --- /dev/null +++ b/wwwroot/api/pbl_appcodes_list.dspy @@ -0,0 +1,4 @@ +# pbl_appcodes/api/pbl_appcodes_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py) +debug('pbl_appcodes/api/pbl_appcodes_list.dspy: START params_kw={dict(params_kw)}') +data = await pbl_appcodes_list(**params_kw) +return data diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..499c9a6 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,91 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "padding": "20px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "PBL 枚举编码注入(6+2 组,幂等,零表)", + "fontSize": "24px" + } + }, + { + "widgettype": "ResponsableBox", + "options": { + "gap": "16px", + "minWidth": "250px" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "backgroundColor": "#FFFFFF", + "padding": "20px", + "cursor": "pointer" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.pbl_appcodes_content", + "options": { + "url": "{{entire_url('api/pbl_appcodes_inject.dspy')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "pbl_appcodes_inject" + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "backgroundColor": "#FFFFFF", + "padding": "20px", + "cursor": "pointer" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.pbl_appcodes_content", + "options": { + "url": "{{entire_url('api/pbl_appcodes_list.dspy')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "pbl_appcodes_list" + } + } + ] + } + ] + }, + { + "widgettype": "VBox", + "id": "pbl_appcodes_content", + "options": { + "width": "100%", + "flex": "1", + "marginTop": "20px" + } + } + ] +}